{"version":3,"file":"modes.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/modes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAGX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AA0ExC;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAGnH;AAMD,MAAM,WAAW,YAAY;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CA2BnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAwBlE;AAMD,qDAAqD;AACrD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;AAEjD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAKtE;AAiCD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAOrF;AAMD,mCAAmC;AACnC,MAAM,WAAW,cAAc;IAC9B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAatE;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC5B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CAWxF;AAMD,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAsPjD","sourcesContent":["/**\n * Mode system — resolves the active mode (ask/plan/build/debug), loads the\n * mode's system prompt, filters active tools, and exposes the /mode, /plan,\n * /grill, /goal, and /approve commands.\n *\n * Mode prompt search order (first hit wins):\n *   - `./.hoocode/modes/{mode}/system.md`\n *   - `~/.hoocode/modes/{mode}/system.md`\n *   - each configured/contributed external dir in declared order\n *   - built-in MODE_DEFAULTS for the four known modes\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport { getHooCodeDir } from \"../../config.js\";\nimport type {\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { isLightModeEnv } from \"../../core/light.js\";\nimport { DEFAULT_MODE, DEFAULT_MODE_PROMPTS as MODE_DEFAULTS } from \"../../core/mode-prompts.js\";\nimport { SUBAGENT_DEPTH_ENV } from \"../../core/subagent-depth.js\";\nimport { EMBEDDED_PROMPTS } from \"../../init-templates.generated.js\";\nimport { mergeSearchPaths, readConfig, readMergedConfig, writeConfig } from \"./config.js\";\nimport { AUTO_LOOP_DONE_TOKEN, LOOP_AUTO_CHANGED, LOOP_AUTO_START, type LoopAutoStartPayload } from \"./loop.js\";\n\nconst HOOCODE_DIR = getHooCodeDir();\n\n/**\n * Per-session plan file path. Keying on sessionId lets concurrent or resumed\n * plan sessions keep distinct plans instead of clobbering each other.\n */\nfunction getPlanPath(cwd: string, sessionId: string): string {\n\treturn join(cwd, \".hoocode\", \"plans\", `${sessionId}.md`);\n}\n\n/** Legacy single-file plan location, retained as a read-only fallback for /approve. */\nfunction getLegacyPlanPath(cwd: string): string {\n\treturn join(cwd, \".hoocode\", \"plan.md\");\n}\n\nfunction tryReadFile(path: string): string | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst text = readFileSync(path, \"utf8\").trim();\n\t\treturn text || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Outcome of reading the first usable plan file out of a candidate list. */\ntype PlanLoadResult =\n\t| { status: \"loaded\"; sections: PlanSections }\n\t| { status: \"missing\" }\n\t| { status: \"error\"; path: string };\n\n/**\n * Reads the first candidate plan file that exists and is non-empty, parsed into\n * sections. Missing and blank files fall through to the next candidate; an\n * unreadable one stops the walk so the caller can report the specific path.\n */\nfunction loadPlanSections(candidatePaths: string[]): PlanLoadResult {\n\tfor (const planPath of candidatePaths) {\n\t\tif (!existsSync(planPath)) continue;\n\t\ttry {\n\t\t\tconst raw = readFileSync(planPath, \"utf8\").trim();\n\t\t\tif (raw) return { status: \"loaded\", sections: parsePlanSections(raw) };\n\t\t} catch {\n\t\t\treturn { status: \"error\", path: planPath };\n\t\t}\n\t}\n\treturn { status: \"missing\" };\n}\n\n/**\n * Walks search dirs in precedence order and returns the first existing\n * `modes/{name}/system.md` content. Order: project → user → externalDirs.\n */\nfunction resolveModeFile(name: string, cwd: string, externalDirs: string[]): string | undefined {\n\tconst candidates: string[] = [\n\t\tjoin(cwd, \".hoocode\", \"modes\", name, \"system.md\"),\n\t\tjoin(HOOCODE_DIR, \"modes\", name, \"system.md\"),\n\t\t...externalDirs.map((dir) => join(dir, name, \"system.md\")),\n\t];\n\tfor (const candidate of candidates) {\n\t\tconst content = tryReadFile(candidate);\n\t\tif (content !== undefined) return content;\n\t}\n\treturn undefined;\n}\n\n/**\n * Returns the system prompt for the active mode.\n *\n * Search order (first hit wins):\n *   - `./.hoocode/modes/{mode}/system.md`\n *   - `~/.hoocode/modes/{mode}/system.md`\n *   - each of `externalDirs` in declared order (config + CLI + extension contributions)\n *   - built-in MODE_DEFAULTS for the four known modes\n */\nexport function buildSystemPrompt(mode: string, cwd: string, options?: { modePaths?: string[] }): string | undefined {\n\tconst modePaths = options?.modePaths ?? [];\n\treturn resolveModeFile(mode, cwd, modePaths) ?? MODE_DEFAULTS[mode];\n}\n\n// ============================================================================\n// Plan file: section parsing and step-by-step execution message\n// ============================================================================\n\nexport interface PlanSections {\n\tgoal?: string;\n\tfilesToModify?: string;\n\tnewFiles?: string;\n\ttests?: string;\n\tverification?: string;\n\t/** Original full text, used as fallback if no sections parsed */\n\traw: string;\n}\n\n/**\n * Parses `.hoocode/plan.md` into named sections.\n *\n * Recognises both ATX headings (`## Goal`) and bold labels (`**Goal**`).\n * Section names matched (case-insensitive): Goal, Files to modify, New files,\n * Tests, Verification.\n */\nexport function parsePlanSections(planContent: string): PlanSections {\n\tconst result: PlanSections = { raw: planContent };\n\n\t// Match `## Heading text` or `**Heading text**` followed by content until\n\t// the next heading of the same style.\n\tconst sectionPattern =\n\t\t/^(?:#{1,3}\\s+(.+?)|(?:\\*\\*(.+?)\\*\\*))\\s*\\n([\\s\\S]*?)(?=(?:^#{1,3}\\s+|\\*\\*[^*\\n]+\\*\\*\\s*\\n)|$)/gm;\n\n\tfor (const match of planContent.matchAll(sectionPattern)) {\n\t\tconst heading = (match[1] ?? match[2] ?? \"\").toLowerCase().trim();\n\t\tconst content = match[3].trim();\n\t\tif (!content) continue;\n\n\t\tif (/^goal/.test(heading)) {\n\t\t\tresult.goal = content;\n\t\t} else if (/files?\\s+to\\s+modif|^modif/.test(heading)) {\n\t\t\tresult.filesToModify = content;\n\t\t} else if (/new\\s+files?/.test(heading)) {\n\t\t\tresult.newFiles = content;\n\t\t} else if (/^tests?/.test(heading)) {\n\t\t\tresult.tests = content;\n\t\t} else if (/^verif/.test(heading)) {\n\t\t\tresult.verification = content;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Builds the user message sent to the agent when `/approve` is run.\n *\n * If the plan has recognisable sections, each is presented as a numbered step\n * so the agent works through them sequentially. Otherwise the raw plan is used.\n *\n * Execution order:\n *   1. Modify existing files\n *   2. Create new files\n *   3. Update / add tests\n *   4. Run verification commands\n */\nexport function buildApproveMessage(sections: PlanSections): string {\n\tconst steps: string[] = [];\n\n\tif (sections.goal) {\n\t\tsteps.push(`**Goal:** ${sections.goal}`);\n\t}\n\tif (sections.filesToModify) {\n\t\tsteps.push(`**Step 1 — Modify existing files:**\\n${sections.filesToModify}`);\n\t}\n\tif (sections.newFiles) {\n\t\tsteps.push(`**Step 2 — Create new files:**\\n${sections.newFiles}`);\n\t}\n\tif (sections.tests) {\n\t\tsteps.push(`**Step 3 — Update tests:**\\n${sections.tests}`);\n\t}\n\tif (sections.verification) {\n\t\tsteps.push(`**Step 4 — Verify:**\\n${sections.verification}`);\n\t}\n\n\tif (steps.length === 0) {\n\t\treturn `Execute the following plan:\\n\\n${sections.raw}`;\n\t}\n\n\treturn `Execute this plan step by step. Complete each step fully before moving to the next.\\n\\n${steps.join(\"\\n\\n\")}`;\n}\n\n// ============================================================================\n// Grill: plan interrogation\n// ============================================================================\n\n/** Which half (or both) of the grill flow to run. */\nexport type GrillTarget = \"both\" | \"me\" | \"plan\";\n\n/**\n * Parses the `/grill` argument into a target.\n *\n * Bare `/grill` runs both phases, `/grill me` only questions the user, and\n * `/grill plan` only critiques the plan. Anything else returns undefined so the\n * caller shows usage rather than guessing at intent.\n */\nexport function parseGrillTarget(args: string): GrillTarget | undefined {\n\tconst arg = args.trim().toLowerCase();\n\tif (!arg) return \"both\";\n\tif (arg === \"me\" || arg === \"plan\") return arg;\n\treturn undefined;\n}\n\n/**\n * Renders the parsed plan back into the message so the agent reviews a concrete\n * artifact rather than whatever it remembers writing. Falls back to the raw plan\n * text when no sections were recognised.\n */\nfunction renderPlanForReview(sections: PlanSections): string {\n\tconst parts: string[] = [];\n\tif (sections.goal) parts.push(`**Goal**\\n${sections.goal}`);\n\tif (sections.filesToModify) parts.push(`**Files to modify**\\n${sections.filesToModify}`);\n\tif (sections.newFiles) parts.push(`**New files**\\n${sections.newFiles}`);\n\tif (sections.tests) parts.push(`**Tests**\\n${sections.tests}`);\n\tif (sections.verification) parts.push(`**Verification**\\n${sections.verification}`);\n\treturn parts.length > 0 ? parts.join(\"\\n\\n\") : sections.raw;\n}\n\n/**\n * The `/grill` phases, read from the shipped prompt templates.\n *\n * These were two ~200-word string constants in this file. They are prose the\n * runtime injects verbatim - no interpolation, no branching, nothing a compiler\n * checks - so a `.md` under `templates/prompts/` is their right home, and the\n * existing build-time embed carries them in. Editing a prompt is now editing\n * prose rather than editing TypeScript that happens to contain prose.\n *\n * Each is trimmed: the files end with a newline and the phases are joined with\n * explicit separators below.\n */\nfunction grillPrompt(name: \"grill-me\" | \"grill-plan\" | \"grill-bridge\"): string {\n\treturn (EMBEDDED_PROMPTS[name] ?? \"\").trim();\n}\n\n/**\n * Builds the follow-up message injected by `/grill`.\n *\n * The two phases are ordered rather than alternative: underspecification sits\n * upstream of plan weakness, so critiquing a plan built on a misread request\n * produces a well-reviewed plan for the wrong job. Bare `/grill` therefore asks\n * first and folds the answers into the critique.\n */\nexport function buildGrillMessage(sections: PlanSections, target: GrillTarget): string {\n\tconst plan = renderPlanForReview(sections);\n\tconst me = grillPrompt(\"grill-me\");\n\tconst critique = grillPrompt(\"grill-plan\");\n\tif (target === \"me\") return `${me}\\n\\n---\\n\\n${plan}`;\n\tif (target === \"plan\") return `${critique}\\n\\n---\\n\\n${plan}`;\n\treturn `${me}\\n\\n${grillPrompt(\"grill-bridge\")}\\n\\n${critique}\\n\\n---\\n\\n${plan}`;\n}\n\n// ============================================================================\n// Goal: autonomous execution toward a completion condition\n// ============================================================================\n\n/** A parsed `/goal` invocation. */\nexport interface GoalInvocation {\n\t/** Explicit objective; falls back to the plan's Goal section when absent. */\n\tobjective?: string;\n\t/** Turn budget passed through to the autonomous loop. */\n\tmaxTurns?: number;\n}\n\n/**\n * Parses `/goal [--max-turns N] [objective]`.\n *\n * Returns undefined when `--max-turns` is present but not followed by a\n * non-negative integer, so the caller shows usage instead of silently running\n * with the default budget.\n */\nexport function parseGoalArgs(args: string): GoalInvocation | undefined {\n\tlet rest = args.trim();\n\tlet maxTurns: number | undefined;\n\n\tconst flag = /^--max-turns(?:\\s+(\\S+))?([\\s\\S]*)$/.exec(rest);\n\tif (flag) {\n\t\tconst raw = flag[1];\n\t\tif (!raw || !/^\\d+$/.test(raw)) return undefined;\n\t\tmaxTurns = Number(raw);\n\t\trest = flag[2].trim();\n\t}\n\n\treturn { objective: rest || undefined, maxTurns };\n}\n\n/** The two messages `/goal` hands to the autonomous loop. */\nexport interface GoalMessages {\n\t/** Delivered once, to start the run. */\n\ttask: string;\n\t/** Re-sent on each continuation to keep the target in view. */\n\tcontinuePrompt: string;\n}\n\n/**\n * Builds the messages for a `/goal` run.\n *\n * When the plan carries a Verification section it becomes the completion\n * condition, which is the difference between a loop that stops when it feels\n * finished and one that stops when something it can run says so. Both messages\n * restate the objective, because the loop's continuation prompt is all that\n * holds the target in place as the transcript grows.\n */\nexport function buildGoalMessages(objective: string, verification?: string): GoalMessages {\n\tconst condition = verification\n\t\t? `\\n\\nThe goal counts as complete only once this verification passes. Run it, and if it fails, fix what it reports and run it again:\\n\\n${verification}`\n\t\t: \"\";\n\n\treturn {\n\t\ttask: `Work autonomously toward this goal:\\n\\n${objective}${condition}`,\n\t\tcontinuePrompt:\n\t\t\t`Keep working toward the goal:\\n\\n${objective}${condition}\\n\\n` +\n\t\t\t`Reply with ${AUTO_LOOP_DONE_TOKEN} only when it is genuinely complete — not when it is merely close.`,\n\t};\n}\n\n// ============================================================================\n// setupMode\n// ============================================================================\n\nexport function setupMode(hoo: ExtensionAPI): void {\n\tlet cachedMode = DEFAULT_MODE;\n\tlet cachedSystemPrompt: string | undefined;\n\tlet cachedPlanPath: string | undefined;\n\n\t// `/grill me` asks the user a question via ask_options, which an unattended\n\t// `/loop auto` run has nobody to answer. Track the loop's broadcast state so\n\t// the command can drop its interrogation phase instead of stalling the run.\n\tlet autoLoopActive = false;\n\thoo.events.on(LOOP_AUTO_CHANGED, (data) => {\n\t\tautoLoopActive = (data as { active?: boolean } | undefined)?.active === true;\n\t});\n\n\t// ── session_start ─────────────────────────────────────────────────────────\n\t// Config resolution order:\n\t//   1. Read global config  (~/.hoocode/hoo-config.json)\n\t//   2. Read project config (./.hoocode/hoo-config.json) if present\n\t//   3. Merge — project scalars win; arrays are unioned\n\t//   4. Re-resolve active_mode from the merged result\n\n\thoo.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\t// Light mode strips every fixed prompt appendix. Leaving\n\t\t// cachedSystemPrompt unset makes before_agent_start a no-op, so no\n\t\t// `<!-- hoo-core: mode= -->` block is appended.\n\t\tif (isLightModeEnv()) {\n\t\t\treturn;\n\t\t}\n\n\t\t// A spawned subagent already carries its own system prompt (from its agent\n\t\t// definition) and its own tool allowlist. Appending the parent's mode prompt\n\t\t// on top contradicts it — a read-only `explore` child would be told to \"read\n\t\t// before editing\" and \"run tests after every change\" — and spends tokens on\n\t\t// rules for tools the child does not have. The pool sets this on every child.\n\t\tif (process.env[SUBAGENT_DEPTH_ENV]) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Steps 1–3: merge global + project configs\n\t\tconst config = readMergedConfig(ctx.cwd);\n\n\t\t// Step 4: resolve mode from the merged config\n\t\tcachedMode = config.active_mode ?? DEFAULT_MODE;\n\t\t// External search dirs come from two channels:\n\t\t//  - HooConfig.mode_paths (config-declared)\n\t\t//  - hoo.addModeSearchPath (CLI flags + extension contributions)\n\t\tconst modePaths = mergeSearchPaths(config.mode_paths, hoo.getModeSearchPaths());\n\t\tconst rawSystemPrompt = buildSystemPrompt(cachedMode, ctx.cwd, { modePaths });\n\n\t\t// Per-session plan path so concurrent sessions don't overwrite each other.\n\t\t// The `{{PLAN_PATH}}` token in plan-mode templates is substituted here.\n\t\tcachedPlanPath = getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\tconst relPlanPath = relative(ctx.cwd, cachedPlanPath) || cachedPlanPath;\n\t\tcachedSystemPrompt = rawSystemPrompt?.replace(/\\{\\{PLAN_PATH\\}\\}/g, relPlanPath);\n\n\t\t// Update footer with active mode\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.setMode(cachedMode);\n\t\t}\n\n\t\t// Apply tool filter from mode enabled_tools\n\t\tconst modeCfg = config.modes?.[cachedMode];\n\t\tif (modeCfg?.enabled_tools && modeCfg.enabled_tools.length > 0) {\n\t\t\thoo.setActiveTools(modeCfg.enabled_tools);\n\t\t}\n\t});\n\n\t// ── before_agent_start ────────────────────────────────────────────────────\n\n\thoo.on(\"before_agent_start\", (event: BeforeAgentStartEvent): BeforeAgentStartEventResult | undefined => {\n\t\tif (!cachedSystemPrompt) return;\n\t\treturn {\n\t\t\tsystemPrompt: `${event.systemPrompt}\\n\\n<!-- hoo-core: mode=${cachedMode} -->\\n${cachedSystemPrompt}`,\n\t\t};\n\t});\n\n\t// ── /mode command ─────────────────────────────────────────────────────────\n\n\tconst KNOWN_MODES = [\"ask\", \"plan\", \"build\", \"debug\"];\n\n\thoo.registerCommand(\"mode\", {\n\t\tdescription: \"Switch active mode. Usage: /mode <ask|plan|build|debug>\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\tKNOWN_MODES.filter((m) => m.startsWith(prefix)).map((m) => ({ value: m, label: m })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst name = args.trim();\n\t\t\tif (!name) {\n\t\t\t\tctx.ui.notify(`Active mode: ${cachedMode}`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = name === DEFAULT_MODE ? undefined : name;\n\t\t\twriteConfig(config);\n\t\t\tctx.ui.notify(`Mode set to \"${name}\" — reloading…`, \"info\");\n\t\t\tawait ctx.reload();\n\t\t},\n\t});\n\n\t// ── /plan command (shorthand for /mode plan) ──────────────────────────────\n\n\thoo.registerCommand(\"plan\", {\n\t\tdescription: \"Switch to plan mode. Shorthand for /mode plan.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = \"plan\";\n\t\t\twriteConfig(config);\n\t\t\tctx.ui.notify(`Mode set to \"plan\" — reloading…`, \"info\");\n\t\t\tawait ctx.reload();\n\t\t},\n\t});\n\n\t// ── /grill command ────────────────────────────────────────────────────────\n\t// Stress-tests the current plan in two phases: `me` surfaces the request's\n\t// unconfirmed assumptions as ask_options questions, `plan` critiques the plan\n\t// file itself. Unlike /approve this only injects a follow-up message — no\n\t// session switch, no mode change, no config write.\n\n\thoo.registerCommand(\"grill\", {\n\t\tdescription: \"Stress-test the current plan. Usage: /grill [me|plan]\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"me\", \"plan\"].filter((s) => s.startsWith(prefix)).map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst requested = parseGrillTarget(args);\n\t\t\tif (!requested) {\n\t\t\t\tctx.ui.notify(\"Usage: /grill [me|plan]\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Same lookup as /approve: per-session plan first, legacy file second.\n\t\t\tconst sessionPlanPath = cachedPlanPath ?? getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\t\tconst loaded = loadPlanSections([sessionPlanPath, getLegacyPlanPath(ctx.cwd)]);\n\t\t\tif (loaded.status === \"error\") {\n\t\t\t\tctx.ui.notify(`Could not read ${relative(ctx.cwd, loaded.path) || loaded.path}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (loaded.status === \"missing\") {\n\t\t\t\tconst relPlan = relative(ctx.cwd, sessionPlanPath) || sessionPlanPath;\n\t\t\t\tctx.ui.notify(`No plan to grill — ${relPlan} not found. Run /plan first.`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Nobody is present to answer during an autonomous loop, so keep the\n\t\t\t// half of the flow that still works rather than blocking on a question.\n\t\t\tlet target = requested;\n\t\t\tif (autoLoopActive && target !== \"plan\") {\n\t\t\t\ttarget = \"plan\";\n\t\t\t\tctx.ui.notify(\"Autonomous loop active — grilling the plan only, skipping questions.\", \"info\");\n\t\t\t}\n\n\t\t\thoo.sendUserMessage(buildGrillMessage(loaded.sections, target), { deliverAs: \"followUp\" });\n\t\t},\n\t});\n\n\t// ── /goal command ─────────────────────────────────────────────────────────\n\t// Works autonomously toward a completion condition by driving the loop in\n\t// loop.ts over the LOOP_AUTO_START channel. Linear by design: a single thread\n\t// of execution that iterates until it reports done or exhausts its budget —\n\t// there is no task graph here, and no subtask fan-out.\n\t//\n\t// The run inherits whatever the active mode already permits (enabled_tools,\n\t// allowed_bash_commands, allowed_write_paths, auto_allow), so how much rope\n\t// an unattended goal gets is a mode-config decision, not one made here.\n\n\thoo.registerCommand(\"goal\", {\n\t\tdescription: \"Work autonomously toward a goal. Usage: /goal [--max-turns N] [objective]\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst parsed = parseGoalArgs(args);\n\t\t\tif (!parsed) {\n\t\t\t\tctx.ui.notify(\"Usage: /goal [--max-turns N] [objective]\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// The plan supplies the objective when none was typed, and its\n\t\t\t// verification section becomes the completion condition either way.\n\t\t\tconst sessionPlanPath = cachedPlanPath ?? getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\t\tconst loaded = loadPlanSections([sessionPlanPath, getLegacyPlanPath(ctx.cwd)]);\n\t\t\tif (loaded.status === \"error\") {\n\t\t\t\tctx.ui.notify(`Could not read ${relative(ctx.cwd, loaded.path) || loaded.path}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst sections = loaded.status === \"loaded\" ? loaded.sections : undefined;\n\n\t\t\tconst objective = parsed.objective ?? sections?.goal;\n\t\t\tif (!objective) {\n\t\t\t\tconst relPlan = relative(ctx.cwd, sessionPlanPath) || sessionPlanPath;\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`No objective — pass one as /goal <objective>, or write a Goal section in ${relPlan}.`,\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { task, continuePrompt } = buildGoalMessages(objective, sections?.verification);\n\t\t\t// The loop announces itself on start, so nothing to notify here.\n\t\t\thoo.events.emit(LOOP_AUTO_START, {\n\t\t\t\ttask,\n\t\t\t\tmaxTurns: parsed.maxTurns,\n\t\t\t\tcontinuePrompt,\n\t\t\t} satisfies LoopAutoStartPayload);\n\t\t},\n\t});\n\n\t// ── /approve command ──────────────────────────────────────────────────────\n\t// Reads .hoocode/plan.md, parses it into named sections (Goal, Files to\n\t// modify, New files, Tests, Verification), switches to build mode, then\n\t// injects a step-by-step execution message into the new session.\n\n\thoo.registerCommand(\"approve\", {\n\t\tdescription: \"Approve the current plan and switch to build mode to execute it.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tif (cachedMode !== \"plan\") {\n\t\t\t\tctx.ui.notify(`/approve is only available in plan mode (current mode: \"${cachedMode}\")`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Prefer the per-session plan file, fall back to the legacy single file.\n\t\t\tconst sessionPlanPath = cachedPlanPath ?? getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\t\tconst loaded = loadPlanSections([sessionPlanPath, getLegacyPlanPath(ctx.cwd)]);\n\t\t\tif (loaded.status === \"error\") {\n\t\t\t\tctx.ui.notify(`Could not read ${relative(ctx.cwd, loaded.path) || loaded.path}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst approveMessage = loaded.status === \"loaded\" ? buildApproveMessage(loaded.sections) : undefined;\n\n\t\t\t// Switch global config to build mode\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = \"build\";\n\t\t\twriteConfig(config);\n\n\t\t\tif (approveMessage) {\n\t\t\t\t// Open a new build-mode session and deliver the parsed plan as the\n\t\t\t\t// first user message so the agent starts executing immediately\n\t\t\t\tawait ctx.newSession({\n\t\t\t\t\twithSession: async (replacedCtx) => {\n\t\t\t\t\t\tawait replacedCtx.sendUserMessage(approveMessage!, { deliverAs: \"followUp\" });\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst relPlan = relative(ctx.cwd, sessionPlanPath) || sessionPlanPath;\n\t\t\t\tctx.ui.notify(`Switched to build mode. No ${relPlan} found — describe what to build.`, \"info\");\n\t\t\t\tawait ctx.reload();\n\t\t\t}\n\t\t},\n\t});\n}\n"]}