// ============================================================================= // workflows/execute.ts — `soly execute ` / `soly execute ` handler // ============================================================================= // // Intercepts "soly execute 11" (phase) or "soly execute 11.02" (specific plan) // and transforms it into a detailed LLM instruction that carries the soly // execute workflow inline. // // We use `action: "transform"` (not `action: "handled"`) — the LLM receives // the request enriched with the full workflow context and executes it INLINE, // in the same session, applying the SOLY-specific close-out discipline // (commits, SUMMARY.md, STATE.md update). // // No external subagent plugin is involved: the model does the work itself. // The same instruction is emitted whether the request arrives as the plain // "soly execute …" verb (input hook) or via the `soly_workflow` tool. // ============================================================================= import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { describeExecuteTarget, type SolyCommand } from "./parser.ts"; import type { SolyState } from "../core.js"; import { extractPlanSummary, renderPlanSummaryInline, writeIterationContext, } from "../iteration.js"; import { buildVerificationPrompt, parseGoalAndAcceptance } from "./goal-verify.ts"; /** * Append the goal-verification step to a worker close-out block, gated on * whether PLAN.md at `planPath` actually has Goal + Acceptance sections. * If parsing fails (empty/missing sections), we skip the block rather than * inject a malformed prompt — the worker has nothing meaningful to verify. */ function withGoalCheck(planPath: string, suffix: string): string { try { const r = parseGoalAndAcceptance(planPath); if (!r.ok) return suffix; return buildVerificationPrompt(r, planPath) + suffix; } catch { return suffix; } } /** Resolve /workflows-data/.md regardless of cwd. */ function loadWorkflowMarkdown(name: string): string | null { try { const here = path.dirname(fileURLToPath(import.meta.url)); // workflows-data is sibling of workflows/ const candidate = path.resolve(here, "..", "workflows-data", name); if (fs.existsSync(candidate)) return fs.readFileSync(candidate, "utf-8"); } catch { // fileURLToPath may fail in some runtimes; fall through. } return null; } /** Build the inline plan summary block for the worker task (so it has the * must_haves / wave / requirements even before reading the iteration file). */ function inlinePlanSummary(planFilePath: string | null): string { if (!planFilePath) return "_(no PLAN.md located)_"; const raw = fs.readFileSync(planFilePath, "utf-8"); const summary = extractPlanSummary(raw); if (!summary) return "_(PLAN.md missing frontmatter or unparseable)_"; return renderPlanSummaryInline(summary); } export interface ExecuteHandlerResult { handled: boolean; transformedText?: string; } /** * Build the transformed LLM instruction for a `soly execute ...` command. * Returns { handled: false } if the args are malformed. * * `interactiveRules` is the list of relPaths marked `interactive: true` * — passed to the worker so it knows which rules are explicitly OUT of * scope (they describe the user-facing conversation, not the work). */ export function buildExecuteTransform( cmd: SolyCommand, state: SolyState, interactiveRules: string[] = [], ): ExecuteHandlerResult { if (!state.exists) { return { handled: true, transformedText: `soly: no .agents/ directory found in cwd (${state.solyDir || ""}) — cannot execute phase.\n` + `Initialize a soly project first (see soly quickstart) before running "soly execute".`, }; } const projectRoot = path.dirname(state.solyDir); const target = describeExecuteTarget(cmd.args); if (!target) { return { handled: true, transformedText: `soly execute: missing or malformed target.\n` + `Usage:\n` + ` soly execute — execute all plans in phase N\n` + ` soly execute — execute a specific plan\n` + ` soly execute — execute a specific task (new dual-mode)\n` + ` soly execute --all — execute all ready tasks (sequential in v0.1)\n` + ` soly execute --feature — execute all tasks in a feature (sequential in v0.1)`, }; } // === TASK MODE (new dual-mode) === if (target.kind === "task") { const task = state.tasks.find((t) => t.id === target.taskId); if (!task) { return { handled: true, transformedText: `soly execute: task ${target.taskId} not found in .agents/features/*/tasks/.\n` + `Known tasks: ${state.tasks.map((t) => t.id).join(", ") || "(none)"}\n` + `Tip: use the \`soly_list_tasks\` tool to see all available tasks.`, }; } if (!task.planExists) { return { handled: true, transformedText: `soly execute: task ${target.taskId} has no PLAN.md at ${task.dir}/PLAN.md.\n` + `Create a PLAN.md with frontmatter (id, kind, feature, status, depends-on) before executing.`, }; } if (task.status === "blocked") { return { handled: true, transformedText: `soly execute: task ${target.taskId} is \`status: blocked\`.\n` + `Resolve blockers in PLAN.md before executing.`, }; } if (task.status === "done") { return { handled: true, transformedText: `soly execute: task ${target.taskId} is already \`status: done\`.\n` + `SUMMARY.md exists at ${task.dir}/SUMMARY.md. To re-run, change status to \`ready\` in PLAN.md frontmatter.`, }; } // Check deps const unmetDeps = task.dependsOn.filter((depId) => { const dep = state.tasks.find((t) => t.id === depId); return !dep || dep.status !== "done"; }); if (unmetDeps.length > 0) { return { handled: true, transformedText: `soly execute: task ${target.taskId} has unmet dependencies: [${unmetDeps.join(", ")}].\n` + `Tasks must be \`status: done\` (have a SUMMARY.md) before their dependents can run.`, }; } const workflow = loadWorkflowMarkdown("execute-task.md"); if (!workflow) { return { handled: true, transformedText: `soly execute: workflow markdown not found: workflows-data/execute-task.md\n` + `This is an extension installation issue — reinstall soly.`, }; } const featureDir = path.dirname(path.dirname(task.dir)); // Write per-iteration context bundle (B2 of the soly design). // Worker reads this file first; no need to chase 6+ .agents/ files. const iter = writeIterationContext({ solyDir: state.solyDir, projectRoot, kind: "exec", taskId: task.id, feature: task.feature, }); const inlineSummary = inlinePlanSummary(path.join(task.dir, "PLAN.md")); const instruction = `soly execute ${target.taskId} — executing task. **Task:** ${task.id} **Feature:** ${task.feature} **Kind:** ${task.kind} **Status:** ${task.status} **Priority:** ${task.priority} **Depends-on:** [${task.dependsOn.join(", ") || "none"}] **Parallelizable:** ${task.parallelizable} **Dir:** ${task.dir} **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes) Read this file first — it contains intent, STATE, ROADMAP (n/a for tasks), the feature README, prior task SUMMARYs, and the current task PLAN. **0-POINT CHECK.** Re-read .agents/docs/ (intent) and .agents/features/${task.feature}/README.md before implementing. **STUDY THE REPO.** Use \`soly_snippet(path, offset, limit)\`, \`soly_doc_search(query)\`, and \`## project layout\` from the system prompt to understand the area before changing any file. Read at least: the module being modified, one similar feature for the pattern, and the corresponding test file. Do NOT change code based on assumptions about how the existing code works. **Execute this now — inline, in THIS session. You are soly-executor (single-task writer). Do the work directly; there is no separate worker to delegate to.** Your job: execute ONE task (atomic unit) and produce its SUMMARY.md. **FIRST ACTION — read the iteration context file:** \`\`\` ${iter.relPath} \`\`\` It contains intent, STATE, feature README, prior task SUMMARYs, and the current PLAN. Do NOT skip it. The must-haves below are also inlined so you have them even before reading the file. **Inline plan summary (from PLAN.md frontmatter + Must Haves):** ${inlineSummary} Project root: ${projectRoot} Soly dir: ${state.solyDir} Feature dir: ${featureDir} Task dir: ${task.dir} **0-POINT CHECK — read .agents/docs/ first.** These are the project's INTENT (business context, design vision). Re-read them before implementing. If you find a conflict between intent and PLAN.md, flag it instead of silently choosing one. **Follow the worker self-audit gate (see .agents/rules/process/worker-audit.md):** 1. Run \`dotnet build\` (or relevant build) — 0 warnings 2. Cross-check diff against .agents/rules/coding/* 3. Invoke \`analyzer-coach\` skill for any rule gaps 4. Loop until clean (max 3 iterations) 5. Commit (production-code commit(s)) 6. Write SUMMARY.md, commit it 7. Update PLAN.md frontmatter: \`status: done\` === WORKFLOW: execute-task.md === ${workflow} === END WORKFLOW === Hard rules: - Do not skip the close-out order: production commits -> SUMMARY commit -> status: done. - Do not modify any .agents/rules/ files. - Do not start a task whose \`depends-on:\` lists tasks that are not \`done\`. - PATH DISCIPLINE: all files YOU create must live under \`.agents/\` (iteration, handoff, etc.) or under the project's source dirs. Never write to the project root. - Report as you go: changed files, commands run with exit codes, validation evidence, surprises, decisions needing user approval. - Interactive-only rules are NOT in scope here: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}. When done, confirm the SUMMARY.md was written and the task flipped to \`status: done\`. Then suggest \`soly verify\` to self-review the change with fresh eyes before calling it done.`; return { handled: true, transformedText: withGoalCheck(path.join(task.dir, "PLAN.md"), instruction), }; } // === ALL / FEATURE (new dual-mode, sequential in v0.1) === if (target.kind === "all" || target.kind === "feature") { const allTasks = target.kind === "all" ? state.tasks : state.tasks.filter((t) => t.feature === target.feature); if (allTasks.length === 0) { return { handled: true, transformedText: target.kind === "all" ? `soly execute --all: no tasks found in .agents/features/*/tasks/.` : `soly execute --feature ${target.feature}: no tasks found for that feature.`, }; } const ready = allTasks.filter((t) => t.status === "ready"); const blocked = allTasks.filter((t) => t.status === "blocked"); const done = allTasks.filter((t) => t.status === "done"); if (ready.length === 0) { return { handled: true, transformedText: `soly execute: no ready tasks in scope.\n` + `Tasks: ${allTasks.length} total, ${done.length} done, ${blocked.length} blocked.`, }; } return { handled: true, transformedText: `soly execute ${target.kind === "all" ? "--all" : `--feature ${target.feature}`}: ${ready.length} task(s) ready.\n\n` + `**v0.1 limitation:** tasks run sequentially, not in parallel. Parallel mode is v0.2.\n\n` + `Ready tasks (in suggested order):\n` + ready.map((t, i) => ` ${i + 1}. ${t.id} [${t.kind}] prio=${t.priority}`).join("\n") + `\n\nExecute them one at a time in this order, inline in this session, using the task execution workflow (execute-task.md) per task.`, }; } // === PLAN MODE (new dual-mode: `/` plans live under .agents/plans/-/) === if (target.kind === "plan") { // Plan dir is always flattened: `-` if a prefix is // present, else just ``. const dirSlug = target.prefix ? `${target.prefix}-${target.name}` : target.name; const planDirAbs = `${state.solyDir}/plans/${dirSlug}`; const planFile = `${planDirAbs}/PLAN.md`; let planBody: string; try { planBody = fs.readFileSync(planFile, "utf-8"); } catch { return { handled: true, transformedText: `soly execute: plan ${target.raw} has no PLAN.md at ${planFile}.\n` + `Run \`soly plan ${target.raw}\` first to flesh it out.`, }; } const workflow = loadWorkflowMarkdown("execute-plan.md"); if (!workflow) { return { handled: true, transformedText: `soly execute: workflow markdown not found: workflows-data/execute-plan.md\n` + `This is an extension installation issue — reinstall soly.`, }; } // Write per-iteration context bundle so the worker has intent + STATE // + the plan body in one file. const iter = writeIterationContext({ solyDir: state.solyDir, projectRoot, kind: "exec", planName: target.name, }); const instruction = `soly execute ${target.raw} — executing plan. **Plan:** ${target.name} **Branch:** ${target.raw} **PLAN.md:** ${planFile} **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens) Read this file first — it contains intent, STATE, ROADMAP, the plan body inline, and any prior SUMMARYs for this plan (none on first run). **Inline plan body (so you have must-haves before reading the file):** \`\`\`markdown ${planBody.slice(0, 4000)}${planBody.length > 4000 ? "\n…(truncated)" : ""} \`\`\` **0-POINT CHECK.** Re-read .agents/docs/ (intent) before implementing. **STUDY THE REPO.** Use \`soly_snippet(path, offset, limit)\`, \`soly_doc_search(query)\`, and \`## project layout\` from the system prompt to map the area before editing. Read at least: the module(s) the plan touches, one adjacent feature for the convention, and any test files in the same area. Do NOT edit code on assumptions about how existing code is structured. **Corporate reviewer — gap-hunt the plan first.** Re-read the entire PLAN.md end-to-end. Identify concrete gaps (file paths not named, error handling missing, boundary cases unmentioned, no test file cited, no migration plan for existing instances, etc.). If you find ANY material gap, use \`ask_pro\` to surface it to the user BEFORE editing a single file — phrase each question with a recommended default + 2-3 alternatives. Only after gaps are resolved (or explicitly accepted by the user) may implementation begin. **Execute this now — inline, in THIS session. You are soly-executor. Execute the plan at \`${planFile}\` end-to-end. Do the work directly.** **FIRST ACTION — read the iteration context file:** \`\`\` ${iter.relPath} \`\`\` It contains intent, STATE, ROADMAP, and the full plan body. Do NOT skip it. Project root: ${projectRoot} Soly dir: ${state.solyDir} Plan dir: ${planDirAbs} **0-POINT CHECK — read .agents/docs/ first.** Follow the workflow below VERBATIM. === WORKFLOW: execute-plan.md === ${workflow} === END WORKFLOW === Hard rules: - All work happens on branch \`${target.raw}\`. Do not switch branches. - When the plan is fully executed and verified, write a SUMMARY.md next to PLAN.md summarizing what was done, what was deferred, and any deviations. - Do not commit unless the workflow tells you to; the user reviews and merges. When done, suggest \`soly verify\` to self-review with fresh eyes, then \`soly done ${target.raw}\` to open the PR.`; return { handled: true, transformedText: withGoalCheck(planFile, instruction), }; } // === PHASE MODE === if (target.kind !== "phase") { return { handled: false }; } const phase = state.phases.find((p) => p.number === target.phase); if (!phase) { return { handled: true, transformedText: `soly execute: phase ${target.phase} not found in .agents/phases/.\n` + `Known phases: ${state.phases.map((p) => p.number).join(", ") || "(none)"}`, }; } const isPlanLevel = target.plan != null; // Unified model: a phase with tasks/ executes those (dependency-ordered) via // the task-centric workflow; legacy phases (plan files, no tasks) keep the // wave/plan workflow. const phaseTasks = phase.tasks ?? []; const useTasks = !isPlanLevel && phaseTasks.length > 0; const workflowName = isPlanLevel ? "execute-plan.md" : useTasks ? "execute-task.md" : "execute-phase.md"; const workflow = loadWorkflowMarkdown(workflowName); if (!workflow) { return { handled: true, transformedText: `soly execute: workflow markdown not found: workflows-data/${workflowName}\n` + `This is an extension installation issue — reinstall soly.`, }; } // Write per-iteration context bundle (B2 of the soly design). // For plan-level execution: bundle includes the specific PLAN.md. // For phase-level execution: bundle includes all plan frontmatter summaries // (the phase workflow iterates waves on its own). const iter = writeIterationContext({ solyDir: state.solyDir, projectRoot, kind: "exec", phaseNumber: target.phase, planNumber: isPlanLevel ? (target.plan ?? undefined) : undefined, }); // For plan-level: find the PLAN.md. We always use the directory scan // (the conventional name would require knowing the slug suffix, which // is fragile and changed over time). Pattern: NN-MM-slug-PLAN.md. let planFileResolved: string | null = null; if (isPlanLevel && phase.dir) { const padded = String(target.plan).padStart(2, "0"); let entries: string[]; try { entries = fs.readdirSync(phase.dir); } catch { entries = []; } const re = new RegExp(`^\\d{2,}-${padded}-.+-PLAN\\.md$`); const match = entries.find((f) => re.test(f)); if (match) planFileResolved = path.join(phase.dir, match); } const inlineSummary = isPlanLevel ? inlinePlanSummary(planFileResolved) : "_(phase-level exec — iterate all PLAN.md files; each iteration has its own bundle)_"; // Build the LLM instruction. Keep it terse at the top, then dump the // workflow markdown verbatim so the LLM has full context. const targetDesc = isPlanLevel ? `phase ${target.phase} plan ${String(target.plan).padStart(2, "0")}` : useTasks ? `phase ${target.phase} (${phase.name}) — ${phaseTasks.length} task(s)` : `phase ${target.phase} (${phase.name}) — all ${phase.planCount} plan(s)`; const scopeBlock = isPlanLevel ? `Target: ONE plan = ${targetDesc}. The iteration context file lists the specific plan at section 6.` : useTasks ? `Target: ${targetDesc} — the unified task model. Execute the tasks under \`${phase.dir}/tasks/\`. Run only tasks whose \`depends-on\` are all satisfied (status: done), in dependency order — sequential is fine. Each task produces its own SUMMARY.md in its task dir and flips its PLAN.md frontmatter to \`status: done\`. The wave/plan language in the workflow below maps onto these tasks.` : `Target: ${targetDesc}. The iteration context file lists all plans (their frontmatter) in section 6, grouped by wave.`; const childRole = isPlanLevel ? `soly-executor (single-plan writer)` : `soly-executor (wave-based parallel phase executor)`; const instruction = `soly execute ${target.raw} — executing ${targetDesc}. **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes) Read this file first — it contains intent, STATE, ROADMAP row for this phase, phase CONTEXT, phase RESEARCH, prior SUMMARYs, ${isPlanLevel ? "and the current PLAN" : "and all PLAN frontmatter summaries"}, and (for exec) the Critical Anti-Patterns from .continue-here.md. **0-POINT CHECK — read .agents/docs/ first.** These are the project's INTENT docs. You are about to implement tasks; if the implementation diverges from intent, it will be wrong even if the tests pass. Re-read .agents/docs/ (and any intent docs linked from PLAN.md) before each plan. ${scopeBlock} **Execute this now — inline, in THIS session. You are ${childRole}. Do the work directly; there is no separate worker to delegate to.** Your job: ${isPlanLevel ? "execute ONE plan and produce its SUMMARY.md" : useTasks ? "execute the phase's ready tasks in dependency order, each producing its SUMMARY.md" : "execute ALL plans in this phase using wave-based sequential execution"}. **FIRST ACTION — read the iteration context file:** \`\`\` ${iter.relPath} \`\`\` It contains intent, STATE, ROADMAP, phase CONTEXT, phase RESEARCH, prior SUMMARYs, ${isPlanLevel ? "and the current PLAN" : "and the wave-grouped plan index"}, and (for exec) the Critical Anti-Patterns. ${isPlanLevel ? `**Inline plan summary (so you have must-haves even before reading the file):** ${inlineSummary}` : `**Note for phase-level exec:** for each plan you execute, you may write a new bundle via the extension (the parent will regenerate; you do not need to). Or, simpler: read the plan file directly from the source path listed in section 6. The must-haves of the current plan are in section 6 too.`} Project root: ${projectRoot} Soly dir: ${state.solyDir} Phase dir: ${phase.dir} **0-POINT CHECK — read .agents/docs/ first.** These are the project's INTENT (business context, design vision). Re-read them before implementing each plan. If you find a conflict between intent and PLAN.md, flag it instead of silently choosing one. Follow the workflow below VERBATIM — these are the user-approved soly instructions, not suggestions. === WORKFLOW: ${workflowName} === ${workflow} === END WORKFLOW === Hard rules: - Do not skip the close-out order: production commits -> SUMMARY commit -> STATE/ROADMAP update. - Do not modify any .agents/rules/ files. - PATH DISCIPLINE: all files YOU create must live under \`.agents/\` (e.g. .agents/iterations/, .agents/phases//, .agents/HANDOFF.json) or under the project's source dirs. Never write PLAN/SUMMARY/CONTEXT/RESEARCH/iteration files to the project root. - Report as you go: changed files, commands run with exit codes, validation evidence, surprises, and any decisions needing user approval. - Interactive-only rules are NOT in scope here: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}. They describe how the user-facing conversation should go, not how to execute work. When done, confirm STATE.md was updated. Then suggest \`soly verify\` to self-review the work with fresh eyes before calling the phase done.`; // Goal verification only makes sense for plan-level phase execution — // wave-based / task-based execution iterates many plans and has no // single PLAN.md to verify against. const phasePlanPath = isPlanLevel && planFileResolved ? planFileResolved : null; return { handled: true, transformedText: phasePlanPath ? withGoalCheck(phasePlanPath, instruction) : instruction, }; }