// ============================================================================= // workflows/planning.ts — `soly plan ` / `soly discuss ` handlers // ============================================================================= // // `soly plan ` — produce a PLAN.md for a phase or task. // Dual-mode: phases and tasks live side by side. // `soly discuss ` — discuss scope, requirements, and tradeoffs for a // phase before any planning starts (phase-only in v0.2) // // Both transform into LLM instructions that load the relevant workflow // markdown (plan-phase.md / plan-task.md / discuss-phase.md) inline — the // model does the planning itself, in the same session. No subagent plugin. // ============================================================================= import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { describePlanTarget, parsePlanName, type SolyCommand } from "./parser.ts"; import type { SolyState } from "../core.js"; import { extractPlanSummary, renderPlanSummaryInline, writeIterationContext, } from "../iteration.js"; /** 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): string { const raw = fs.readFileSync(planFilePath, "utf-8"); const summary = extractPlanSummary(raw); if (!summary) return "_(PLAN.md missing frontmatter or unparseable)_"; return renderPlanSummaryInline(summary); } /** Resolve /workflows-data/.md regardless of cwd. */ function loadWorkflowMarkdown(name: string): string | null { try { const here = path.dirname(fileURLToPath(import.meta.url)); const candidate = path.resolve(here, "..", "workflows-data", name); if (fs.existsSync(candidate)) return fs.readFileSync(candidate, "utf-8"); } catch { // fall through } return null; } export interface PlanningHandlerResult { handled: boolean; transformedText?: string; } function getPhaseForDiscuss( state: SolyState, args: string[], ): { phase: number; raw: string } | null { const raw = (args[0] ?? "").trim(); if (!raw) return null; const m = raw.match(/^(\d+)$/); if (!m) return null; const n = parseInt(m[1], 10); if (!state.phases.find((p) => p.number === n)) return null; return { phase: n, raw }; } export function buildPlanTransform(cmd: SolyCommand, state: SolyState): PlanningHandlerResult { if (!state.exists) { return { handled: true, transformedText: `soly plan: no .agents/ directory in cwd (${state.solyDir || ""}) — cannot plan.\n` + `Initialize a soly project first.`, }; } const projectRoot = path.dirname(state.solyDir); const target = describePlanTarget(cmd.args); if (!target) { const knownPhases = state.phases.map((p) => p.number).join(", ") || "(none)"; const knownTasks = state.tasks.map((t) => t.id).join(", ") || "(none)"; return { handled: true, transformedText: `soly plan: missing or malformed target.\n` + `Usage:\n` + ` soly plan — plan phase N\n` + ` soly plan — plan existing task\n` + ` soly plan --new-task --feature — create new task dir + PLAN.md\n` + ` soly plan --feature — plan all ready tasks in a feature\n` + `Known phases: ${knownPhases}\n` + `Known tasks: ${knownTasks}`, }; } // === PLAN MODE (new dual-mode: `/` plans live under .agents/plans/-/) === if (target.kind === "plan") { // Plan dir is always flattened: `-` if a prefix is // present (user typed `/` or the project default // applied), 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 { planBody = "_No PLAN.md yet — the LLM should ask the user for goal / steps / acceptance and write it from scratch._"; } const instruction = `soly plan ${target.raw} — fleshing out PLAN.md for plan. **Plan:** ${target.name} **Branch:** ${target.raw} **PLAN.md:** ${planFile} **Inline current PLAN.md body (so you have the existing TBD sections before reading the file):** \`\`\`markdown ${planBody.slice(0, 4000)}${planBody.length > 4000 ? "\n…(truncated)" : ""} \`\`\` **0-POINT CHECK.** Re-read .agents/docs/ (intent) before fleshing out the plan. **STUDY THE REPO.** Before writing PLAN.md, use \`soly_snippet(path, offset, limit)\` and \`soly_doc_search(query)\` to understand the area this plan will touch. Look at how similar features are implemented in this codebase (naming, file layout, error handling, test convention), and check \`## project layout\` from the system prompt for orientation. Extract enough context that the plan names concrete files and conventions, not abstractions. Surface ambiguities as \`ask_pro\` questions BEFORE writing PLAN.md. If ask_pro is available, use it ONCE to gather goal / steps / acceptance criteria (freeText questions, batched). Then write PLAN.md with the answers. If ask_pro is NOT available, write a sensible stub plan and tell the user to refine it via \`soly plan ${target.raw}\` again. Hard rules: - Update ONLY ${planFile}. Don't touch other plans or the project root. - Use Conventional Commits format: # Plan: ${target.raw} (h1), ## Goal, ## Steps, ## Acceptance. - Don't commit — the user reviews and commits.`; return { handled: true, transformedText: instruction }; } // === PHASE MODE (legacy — see W6 in the plans-instead-of-phases plan) === if (target.kind === "phase") { const deprecationNotice = "⚠️ PHASE MODE IS LEGACY. Phases have been replaced by plans (each plan = a git branch `/` with `.agents/plans//PLAN.md`).\n" + "To migrate an existing phase to a plan, run \`soly migrate phases-to-plans\` once.\n" + "For new work, use \`soly new /\` (e.g. \`soly new feat/auth-jwt\`).\n" + "This phase handler still works for backward compat but won't get new features.\n\n"; const phase = state.phases.find((p) => p.number === target.phase); if (!phase) { return { handled: true, transformedText: `soly plan: phase ${target.phase} not found.\n` + `Known phases: ${state.phases.map((p) => p.number).join(", ") || "(none)"}`, }; } const workflow = loadWorkflowMarkdown("plan-phase.md"); if (!workflow) { return { handled: true, transformedText: `soly plan: workflow markdown not found: workflows-data/plan-phase.md\n` + `This is an extension installation issue — reinstall soly.`, }; } // Write per-iteration context bundle (B2). const iter = writeIterationContext({ solyDir: state.solyDir, projectRoot, kind: "plan", phaseNumber: target.phase, }); const instruction = deprecationNotice + `soly plan ${target.raw} — planning phase ${target.phase} (${phase.name}). **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes) The planner reads this file first — it contains intent, STATE, ROADMAP row for this phase, phase CONTEXT, phase RESEARCH, and prior SUMMARYs. **0-POINT CHECK — read .agents/docs/ first.** These documents hold the project's INTENT — business context, design vision, what the user wants this app to be. Plans that ignore intent produce code that "works" but doesn't fit. If the plan would diverge from anything in .agents/docs/, surface that as a discussion point before committing to it. Phase directory: ${phase.dir} Current state: planCount=${phase.planCount}, context=${phase.contextExists}, research=${phase.researchExists} **Plan this now — inline, in THIS session. You are the planner. Produce the phase's tasks directly.** Break phase ${target.phase} into discrete tasks (unified model): write one PLAN.md per task at phases/-slug/tasks//PLAN.md with frontmatter, plus ${phase.contextExists ? "" : "CONTEXT.md / "}RESEARCH.md if missing. **FIRST ACTION — read the iteration context file:** \`\`\` ${iter.relPath} \`\`\` It contains intent, STATE, ROADMAP row, phase CONTEXT, phase RESEARCH, and prior SUMMARYs. Project root: ${projectRoot} Soly dir: ${state.solyDir} Phase dir: ${phase.dir} Follow the workflow below VERBATIM. === WORKFLOW: plan-phase.md === ${workflow} === END WORKFLOW === Hard rules: - Do not write production code. Planning only. - One task = one PLAN.md under .agents/phases/-/tasks// (task id = -<4hex>). - Each task PLAN.md starts with frontmatter: id, kind, status: ready, depends-on: [task ids], optional feature. The cross-task dependency graph must be acyclic. - Each task PLAN needs requirements, must_haves.truths, must_haves.artifacts, must_haves.key_links. - PATH DISCIPLINE: all task PLAN.md / phase CONTEXT.md / RESEARCH.md go under \`.agents/phases/-/\`. Never write to the project root. - Update .agents/STATE.md Current Position at the end. When done, summarize the created task ids + their dependency order (which are ready first) and any open questions, then suggest \`soly execute ${target.phase}\` (or ask the user to confirm before execution).`; return { handled: true, transformedText: instruction }; } // === TASK MODE (existing task — flesh out PLAN.md) === if (target.kind === "task") { const task = state.tasks.find((t) => t.id === target.taskId); if (!task) { return { handled: true, transformedText: `soly plan: task ${target.taskId} not found.\n` + `Known tasks: ${state.tasks.map((t) => t.id).join(", ") || "(none)"}\n` + `Tip: use the \`soly_list_tasks\` tool.`, }; } if (!task.planExists) { return { handled: true, transformedText: `soly plan: task ${target.taskId} has no PLAN.md at ${task.dir}/PLAN.md.\n` + `Use \`soly plan --new-task --feature ${task.feature}\` to create a different task, or write PLAN.md manually.`, }; } if (task.status === "done") { return { handled: true, transformedText: `soly plan: task ${target.taskId} is already \`status: done\` (SUMMARY.md exists).\n` + `To re-plan, change status to \`ready\` in PLAN.md frontmatter first.`, }; } const workflow = loadWorkflowMarkdown("plan-task.md"); if (!workflow) { return { handled: true, transformedText: `soly plan: workflow markdown not found: workflows-data/plan-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). const iter = writeIterationContext({ solyDir: state.solyDir, projectRoot, kind: "plan", taskId: task.id, feature: task.feature, }); const planFile = path.join(task.dir, "PLAN.md"); const inlineSummary = inlinePlanSummary(planFile); const instruction = `soly plan ${target.taskId} — fleshing out PLAN.md for existing task. **Task:** ${task.id} **Feature:** ${task.feature} **Kind:** ${task.kind} **Current status:** ${task.status} **PLAN.md path:** ${task.dir}/PLAN.md **Feature README:** ${featureDir}/README.md **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens) The planner reads this file first — it contains intent, STATE, the feature README, prior task SUMMARYs, and the current task PLAN (refine, don't re-derive). **Inline plan summary (so you have the must-haves even before reading the file):** ${inlineSummary} **0-POINT CHECK.** Re-read .agents/docs/ (intent) and .agents/features/${task.feature}/README.md (feature context) before refining the plan. This task already has PLAN.md. Your job is to flesh it out / improve it based on intent and feature context — not to start from scratch. **Refine this now — inline, in THIS session. You are the planner. Refine PLAN.md for an existing task, directly.** **FIRST ACTION — read the iteration context file:** \`\`\` ${iter.relPath} \`\`\` It contains intent, STATE, feature README, prior task SUMMARYs, and the current task PLAN (refine, don't re-derive). Project root: ${projectRoot} Soly dir: ${state.solyDir} Task dir: ${task.dir} Feature dir: ${featureDir} Follow the workflow below VERBATIM. === WORKFLOW: plan-task.md === ${workflow} === END WORKFLOW === Hard rules: - Do not write production code. Planning only. - Preserve the existing frontmatter (id, kind, feature, status, etc.) — only update if you find a bug. - If you change the plan body materially, commit it as \`chore(tasks): refine plan \`. - If you only add small clarifications, no commit needed (or include in same commit). - PATH DISCIPLINE: PLAN.md lives at \`.agents/features//tasks//PLAN.md\`. Never write to the project root. When done, summarize what was refined (what changed, open questions, dependencies discovered). Do not execute — planning only.`; return { handled: true, transformedText: instruction }; } // === NEW-TASK MODE (create task dir + PLAN.md skeleton) === if (target.kind === "new-task") { let feature = state.features.find((f) => f.name === target.feature); if (!feature) { // Auto-create the feature dir + README so the planner can immediately // write a PLAN.md. Idempotent; safe to run repeatedly. const featuresRoot = path.join(state.solyDir, "features"); const featureDir = path.join(featuresRoot, target.feature); const featureReadme = path.join(featureDir, "README.md"); try { fs.mkdirSync(path.join(featureDir, "tasks"), { recursive: true }); } catch (e) { return { handled: true, transformedText: `soly plan: could not create .agents/features/${target.feature}/tasks/ (${(e as Error).message}). ` + `Create it manually: \`mkdir -p .agents/features/${target.feature}/tasks/\``, }; } try { if (!fs.existsSync(featureReadme)) { fs.writeFileSync( featureReadme, `# Feature: ${target.feature}\n\nDescribe the feature's purpose here.\n`, "utf-8", ); } } catch (e) { return { handled: true, transformedText: `soly plan: created .agents/features/${target.feature}/tasks/ but failed to write README.md (${(e as Error).message}). ` + `Continue manually: \`touch .agents/features/${target.feature}/README.md\``, }; } // Re-read state so the planner sees the new feature feature = { name: target.feature, slug: target.feature, dir: featureDir, taskCount: 0, readmeExists: true, tasks: [] }; } const workflow = loadWorkflowMarkdown("plan-task.md"); if (!workflow) { return { handled: true, transformedText: `soly plan: workflow markdown not found: workflows-data/plan-task.md\n` + `This is an extension installation issue — reinstall soly.`, }; } const featureDir = feature.dir; // For new-task mode, the PLAN.md doesn't exist yet, so no iteration // bundle — the planner will write it as part of its work. We only // pass the feature README + intent + state path hints. const instruction = `soly plan --new-task ${target.slug} --feature ${target.feature} — creating new task. **Feature:** ${target.feature} **Slug:** ${target.slug} **Feature README:** ${featureDir}/README.md **0-POINT CHECK.** Re-read .agents/docs/ (intent) and .agents/features/${target.feature}/README.md (feature context) before planning. **Step 1 — generate task ID.** The task ID is \`-<4hex>\` (e.g. \`${target.slug}-a3f9\`). Generate 4 lowercase hex chars (use \`crypto.randomBytes(2).toString('hex')\` in node, or any 4-char [0-9a-f]{4} string if you don't have a shell handy). **Step 2 — create the dir:** \`\`\` mkdir -p .agents/features/${target.feature}/tasks/ \`\`\` **Step 3 — write PLAN.md** with the frontmatter below + the plan body. **Frontmatter (REQUIRED):** \`\`\`yaml --- id: kind: feature: ${target.feature} status: ready priority: parallelizable: depends-on: [] --- # Task: [body produced by the planner workflow below] \`\`\` **Do this now — inline, in THIS session. You are the planner. Create a new task dir + write PLAN.md with frontmatter, directly.** Project root: ${projectRoot} Soly dir: ${state.solyDir} Feature dir: ${featureDir} Target slug: ${target.slug} === WORKFLOW: plan-task.md === ${workflow} === END WORKFLOW === Hard rules: - Do not write production code. Planning only. - Generate the task id as \`<slug>-<4hex>\` (e.g. \`${target.slug}-a3f9\`) — use 4 lowercase hex chars. - Create the dir \`.agents/features/${target.feature}/tasks/<id>/\` first. - Write PLAN.md with the frontmatter (id, kind, feature, status: ready, priority, parallelizable, depends-on). - Pick a \`kind:\` value matching the work (be|fe|infra|docs|integration). - Pick a reasonable \`priority:\` (default: medium). - Leave \`depends-on:\` as \`[]\` unless you have a clear dep on an existing task. - Commit: \`chore(tasks): plan <id>\`. When done, show the user the created path, new task id + plan summary. They can then run \`soly execute <id>\`.`; return { handled: true, transformedText: instruction }; } // === FEATURE MODE (plan all ready tasks in a feature) === if (target.kind === "feature") { const feature = state.features.find((f) => f.name === target.feature); if (!feature) { return { handled: true, transformedText: `soly plan: feature "${target.feature}" not found.\n` + `Known features: ${state.features.map((f) => f.name).join(", ") || "(none)"}`, }; } const featureTasks = state.tasks.filter((t) => t.feature === target.feature); if (featureTasks.length === 0) { return { handled: true, transformedText: `soly plan: no tasks in feature "${target.feature}".\n` + `Use \`soly plan --new-task <slug> --feature ${target.feature}\` to create one.`, }; } // For "plan all tasks in feature" mode: we don't know which task to // bundle, so the planner will iterate per-task. The parent supplies // the high-level feature README + task list. Per-task iteration // bundles are written by the planner via the extension's write // function (call from a child tool if needed). const ready = featureTasks.filter((t) => t.status === "ready"); const done = featureTasks.filter((t) => t.status === "done"); const blocked = featureTasks.filter((t) => t.status === "blocked"); const inProgress = featureTasks.filter((t) => t.status === "in-progress"); if (ready.length === 0) { return { handled: true, transformedText: `soly plan --feature ${target.feature}: no tasks need planning.\n` + `Tasks: ${featureTasks.length} total, ${done.length} done, ${inProgress.length} in-progress, ${blocked.length} blocked.`, }; } return { handled: true, transformedText: `soly plan --feature ${target.feature}: ${ready.length} task(s) need planning.\n\n` + `Tasks:\n` + ready.map((t, i) => ` ${i + 1}. ${t.id} [${t.kind}] prio=${t.priority}`).join("\n") + `\n\nPlan them in order, inline in this session, using the plan-task.md workflow per task.`, }; } // Unreachable (describePlanTarget only returns the 4 kinds above) return { handled: true, transformedText: `soly plan: unknown target kind.`, }; } export function buildDiscussTransform( cmd: SolyCommand, state: SolyState, opts: { hasAskPro?: boolean } = {}, ): PlanningHandlerResult { const hasAskPro = opts.hasAskPro ?? false; if (!state.exists) { return { handled: true, transformedText: `soly discuss: no .agents/ directory in cwd (${state.solyDir || "<cwd>"}) — cannot discuss.\n` + `Initialize a soly project first.`, }; } const projectRoot = path.dirname(state.solyDir); const arg = (cmd.args[0] ?? "").trim(); if (!arg) { const known = state.phases.map((p) => p.number).join(", ") || "(none)"; return { handled: true, transformedText: `soly discuss: argument required.\n` + `Usage:\n` + ` soly discuss <N> — discuss phase N (legacy)\n` + ` soly discuss <type>/<name> — discuss plan <type>/<name> (e.g. feat/auth-jwt)\n` + `Known phases: ${known}`, }; } // Plan mode: `<slug>` or `<prefix>/<slug>` const planParsed = parsePlanName(arg); if (!("error" in planParsed)) { const dirSlug = planParsed.prefix ? `${planParsed.prefix}-${planParsed.name}` : planParsed.name; const planDirAbs = `${state.solyDir}/plans/${dirSlug}`; const planFile = `${planDirAbs}/PLAN.md`; let planBody: string; try { planBody = fs.readFileSync(planFile, "utf-8"); } catch { planBody = "_No PLAN.md yet._"; } const instruction = `soly discuss ${arg} — interactive discussion mode for plan. **Plan:** ${planParsed.name} **Branch:** ${arg} **PLAN.md:** ${planFile} **Inline current PLAN.md body:** \`\`\`markdown ${planBody.slice(0, 4000)}${planBody.length > 4000 ? "\n…(truncated)" : ""} \`\`\` **0-POINT CHECK.** Re-read .agents/docs/ (intent) before discussing. **STUDY THE REPO.** Use \`soly_snippet(path, offset, limit)\`, \`soly_doc_search(query)\`, and \`## project layout\` from the system prompt to understand the area the plan touches. The discussion should resolve real ambiguities in the existing code, not invent new ones. If the plan body mentions files you haven't read, read them first. Use \`soly_finish_discuss\` to capture the discussion outcome. The flow: 1. Read the plan above. 2. Identify ambiguities / open questions / scope clarifications. 3.${hasAskPro ? " Use \`ask_pro\` (batched freeText questions) to resolve them with the user." : " Ask the user ONE clear question at a time via the chat."} 4. Call \`soly_finish_discuss\` with the captured decision. 5. Tell the user the next step: \`soly plan ${arg}\` to update PLAN.md with the discussion outcome.`; return { handled: true, transformedText: instruction }; } // Phase mode (legacy) const target = getPhaseForDiscuss(state, cmd.args); if (!target) { const known = state.phases.map((p) => p.number).join(", ") || "(none)"; return { handled: true, transformedText: `soly discuss: bad argument "${arg}".\n` + `Usage: soly discuss <N> (e.g. "soly discuss 11")\n` + `Known phases: ${known}`, }; } const phase = state.phases.find((p) => p.number === target.phase)!; // Write per-iteration context bundle (B2). const iter = writeIterationContext({ solyDir: state.solyDir, projectRoot, kind: "discuss", phaseNumber: target.phase, }); const phaseDir = path.join(state.solyDir, "phases", phase.slug); const padded = String(target.phase).padStart(2, "0"); const contextPath = path.join(phaseDir, `${padded}-CONTEXT.md`); const checkpointPath = path.join(phaseDir, `${padded}-DISCUSS-CHECKPOINT.json`); // Optional workflow reference (background only — not a strict protocol anymore) const workflow = loadWorkflowMarkdown("discuss-phase.md"); // Resume / refine detection let resumeBlock = ""; if (fs.existsSync(checkpointPath)) { try { const ck = JSON.parse(fs.readFileSync(checkpointPath, "utf-8")) as { decisions?: Array<{ category: string; choice: string }>; areas_total?: number; areas_completed?: number[]; }; const decisions = ck.decisions ?? []; resumeBlock = `\n**RESUME MODE** — found checkpoint at \`${path.relative(projectRoot, checkpointPath)}\`:\n${decisions .map((d) => ` - ${d.category}: ${d.choice}`) .join("\n")}\n\nAcknowledge these as **Decisions Locked** at the top of your output, then continue with the next un-answered gray area. Resume from where the prior session left off.`; } catch { resumeBlock = `\n**RESUME MODE** — checkpoint file existed but was malformed; ignoring it and starting fresh.`; } } else if (fs.existsSync(contextPath)) { resumeBlock = `\n**REFINE MODE** — \`${padded}-CONTEXT.md\` already exists. Read it, list the existing decisions, and only ask about uncovered gray areas. Don't re-ask locked decisions.`; } const instruction = `soly discuss ${target.raw} — interactive discussion mode for phase ${target.phase} (${phase.name}). **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes) Read it first — it contains intent, STATE, ROADMAP, and any existing phase artifacts (CONTEXT, RESEARCH, prior SUMMARYs). It's your single source of truth. ${resumeBlock} **This is interactive.** Drive the discussion yourself, in this session, by asking the user a few questions one at a time. --- ${ hasAskPro ? `**PREFERRED PICKER: \`ask_pro\` (from the \`pi-ask\` extension)** is available in this session. This is a multi-question tabbed picker — one call shows all your questions as tabs, the user navigates with Tab/arrows and picks with 1-N. It returns all answers in one shot. This is much better UX than N separate \`soly_ask_user\` calls. **Pattern:** \`\`\` ask_pro({ questions: [ { header: "Auth", question: "Which auth approach?", options: [...], multiSelect: false }, { header: "Tokens", question: "Where to store tokens?", options: [...], allowOther: true }, { header: "Errors", question: "How to handle auth errors?", options: [...], multiSelect: true }, ] }) // → { answers: { 0: 1, 1: "Bearer in Authorization header", 2: [0, 2] } } // or { cancelled: true } \`\`\` - Each option: \`{ label, description?, recommended? }\`. Mark the best with \`recommended: true\` (shown as ⭐ in the UI). - For questions where the user might want a custom answer, add \`allowOther: true\` — the user gets a "Other…" option that opens a text input. - For multi-select questions (checkboxes), set \`multiSelect: true\`. The answer is an array of option indices (and/or strings if \`allowOther\` is on). - 2-4 options per question, max 6 questions per call. - If the user cancels, you get \`{cancelled: true}\` — treat that as "deferred, ask differently" or just end the discuss. - After getting answers, call \`soly_finish_discuss\` to write the canonical CONTEXT.md. If \`ask_pro\` is not available (rare — would mean the user uninstalled the \`pi-ask\` extension), fall back to \`soly_ask_user\` (one call per question, see the fallback section below).` : `**PICKER: \`soly_ask_user\`** is the available multi-choice picker in this session. **Pattern (one call per question, one at a time):** \`\`\` soly_ask_user({ title: "Q1: <category>", question: "<one short sentence>", options: [ "⭐ <recommended option> — <1 sentence why>", "<alternative 1>", "<alternative 2>", ], rationale: "<1–2 sentence note shown above the picker>", }) \`\`\` - Always include a recommended answer (⭐ first option) with 1-sentence rationale. - After each answer, briefly acknowledge ("OK, locking X. Next:") and call \`soly_ask_user\` for the next question. **Do NOT dump all questions at once.** - Never include "skip" / "you decide" as a default option. If a question is too hard, include a real option like \`"Defer — discuss in a future phase"\`. - Note: \`soly_ask_user\` does NOT support \`allowOther\` (no text input). For questions that might need a custom answer, include a "Other (describe)" option that says the user can type a free-text answer in their next chat message. **Tip:** the separate \`pi-ask\` extension provides a better UX (multi-question tabbed picker, \`allowOther\` text input). If it's not installed, \`soly_ask_user\` is the fallback.` } --- **Common flow (applies to both pickers):** 1. **Open with a 1–2 sentence framing** of what this phase delivers (grounded in ROADMAP + intent). No implementation details. Then show the locked decisions (if any from resume). Then say: "I have <N> questions about this phase. Let's go." 2. **Call the picker ONCE** (preferred) **or per-question** (fallback) as above. Always include a recommended answer (⭐ first option) with 1-sentence rationale. 3. **Save checkpoint after each answer** with \`soly_save_discuss_checkpoint({phase_number, decisions, areas_total, areas_completed})\` so the user can quit and resume. The final \`soly_finish_discuss\` will delete the checkpoint and write CONTEXT.md. 4. **After all questions captured, call \`soly_finish_discuss\`:** \`\`\` soly_finish_discuss({ phase_number: ${target.phase}, domain: "<1–2 paragraphs: what this phase delivers>", decisions: [ { category: "<cat>", choice: "<what was chosen>", rationale: "<why>" }, ... ], canonical_refs: [".agents/docs/<file>", ".agents/ROADMAP.md", ...], deferred_ideas: ["<scope creep for future phase>", ...], codebase_context: ["src/components/Card.tsx — has rounded/shadow variants, reuse", ...], }) \`\`\` 5. **Tell the user the next step** after \`soly_finish_discuss\` returns: \`soly plan ${target.phase}\`. --- **Available tools for this flow:** - ${hasAskPro ? "`ask_pro` — multi-question tabbed picker (PREFERRED)" : "`soly_ask_user` — single-question picker (fallback)"} - \`soly_save_discuss_checkpoint\` — save partial progress (use after each answer) - \`soly_finish_discuss\` — finalize: writes CONTEXT.md, deletes checkpoint - \`soly_read\`, \`soly_snippet\`, \`soly_doc_search\` — read .agents/ artifacts as needed (intent docs are already in your system prompt) - \`soly_log_decision\` — log to STATE.md Decisions table (use sparingly) - Standard pi tools: \`read\`, \`bash\`, \`grep\`, \`find\` for codebase context **Workflow reference (background only — not a strict protocol anymore):** ${workflow ? "```\n" + workflow.slice(0, 1500) + "\n[...truncated, see .pi/agent/extensions/soly/workflows-data/discuss-phase.md for full reference]\n```" : "_(workflow markdown missing)_"} --- **Hard rules:** - **One picker call** (ask_pro) **or N calls** (soly_ask_user) — never dump all questions as text in your reply. - Always include a recommended answer (⭐ first option) with 1-sentence rationale. - Use \`soly_save_discuss_checkpoint\` after each answer (so resume works). - Use \`soly_finish_discuss\` to finalize — don't just say "done". - **No scope creep.** Defer scope-creep items to \`deferred_ideas\`. - **No PLAN.md** from this flow — that's \`soly plan ${target.phase}\`. - **No SUMMARY.md** — that's from \`soly execute\`. - **No code edits.** Discussion only. - If intent docs (0-point) are silent on a constraint, ask the user — don't assume. Begin.`; return { handled: true, transformedText: instruction }; }