@kolisachint/hoocode-agent 0.5.53 → 0.5.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/core/system-prompt.d.ts.map +1 -1
  3. package/dist/core/system-prompt.js +11 -12
  4. package/dist/core/system-prompt.js.map +1 -1
  5. package/dist/core/tools/todo.d.ts.map +1 -1
  6. package/dist/core/tools/todo.js +9 -7
  7. package/dist/core/tools/todo.js.map +1 -1
  8. package/dist/extensions/core/ask-options.d.ts.map +1 -1
  9. package/dist/extensions/core/ask-options.js +1 -0
  10. package/dist/extensions/core/ask-options.js.map +1 -1
  11. package/dist/extensions/core/modes.d.ts.map +1 -1
  12. package/dist/extensions/core/modes.js +9 -0
  13. package/dist/extensions/core/modes.js.map +1 -1
  14. package/dist/extensions/core/permission-gate.d.ts.map +1 -1
  15. package/dist/extensions/core/permission-gate.js +40 -10
  16. package/dist/extensions/core/permission-gate.js.map +1 -1
  17. package/dist/init-templates.generated.d.ts.map +1 -1
  18. package/dist/init-templates.generated.js +6 -6
  19. package/dist/init-templates.generated.js.map +1 -1
  20. package/dist/modes/interactive/theme/colorsafe-dark.json +6 -6
  21. package/dist/modes/interactive/theme/colorsafe-light.json +5 -5
  22. package/dist/modes/interactive/theme/high-contrast-light.json +2 -2
  23. package/dist/modes/interactive/theme/light.json +2 -2
  24. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  25. package/dist/modes/interactive/theme/theme.js +122 -2
  26. package/dist/modes/interactive/theme/theme.js.map +1 -1
  27. package/dist/modes/interactive/theme/vox-cutout-light.json +1 -1
  28. package/dist/modes/interactive/theme/vox-light.json +1 -1
  29. package/dist/modes/interactive/theme/warm-dark.json +6 -6
  30. package/dist/modes/interactive/theme/warm-light.json +3 -3
  31. package/docs/modes.md +32 -6
  32. package/docs/themes.md +18 -1
  33. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  34. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  35. package/examples/extensions/sandbox/package.json +1 -1
  36. package/examples/extensions/with-deps/package.json +1 -1
  37. package/package.json +4 -4
  38. package/templates/default-config.json +6 -3
  39. package/templates/modes/ask/system.md +4 -7
  40. package/templates/modes/build/system.md +6 -8
  41. package/templates/modes/debug/system.md +7 -8
  42. package/templates/modes/plan/system.md +7 -13
  43. package/templates/prompts/task-main.md +5 -12
@@ -1 +1 @@
1
- {"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;AAyExC;;;;;;;;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,EAAE,EAAE,YAAY,GAAG,IAAI,CA6OhD","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 { 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(pi: 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\tpi.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\tpi.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// 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// - pi.addModeSearchPath (CLI flags + extension contributions)\n\t\tconst modePaths = mergeSearchPaths(config.mode_paths, pi.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\tpi.setActiveTools(modeCfg.enabled_tools);\n\t\t}\n\t});\n\n\t// ── before_agent_start ────────────────────────────────────────────────────\n\n\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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"]}
1
+ {"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,EAAE,EAAE,YAAY,GAAG,IAAI,CAsPhD","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(pi: 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\tpi.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\tpi.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// - pi.addModeSearchPath (CLI flags + extension contributions)\n\t\tconst modePaths = mergeSearchPaths(config.mode_paths, pi.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\tpi.setActiveTools(modeCfg.enabled_tools);\n\t\t}\n\t});\n\n\t// ── before_agent_start ────────────────────────────────────────────────────\n\n\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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"]}
@@ -14,6 +14,7 @@ import { join, relative } from "node:path";
14
14
  import { getHooCodeDir } from "../../config.js";
15
15
  import { isLightModeEnv } from "../../core/light.js";
16
16
  import { DEFAULT_MODE, DEFAULT_MODE_PROMPTS as MODE_DEFAULTS } from "../../core/mode-prompts.js";
17
+ import { SUBAGENT_DEPTH_ENV } from "../../core/subagent-depth.js";
17
18
  import { EMBEDDED_PROMPTS } from "../../init-templates.generated.js";
18
19
  import { mergeSearchPaths, readConfig, readMergedConfig, writeConfig } from "./config.js";
19
20
  import { AUTO_LOOP_DONE_TOKEN, LOOP_AUTO_CHANGED, LOOP_AUTO_START } from "./loop.js";
@@ -292,6 +293,14 @@ export function setupMode(pi) {
292
293
  if (isLightModeEnv()) {
293
294
  return;
294
295
  }
296
+ // A spawned subagent already carries its own system prompt (from its agent
297
+ // definition) and its own tool allowlist. Appending the parent's mode prompt
298
+ // on top contradicts it — a read-only `explore` child would be told to "read
299
+ // before editing" and "run tests after every change" — and spends tokens on
300
+ // rules for tools the child does not have. The pool sets this on every child.
301
+ if (process.env[SUBAGENT_DEPTH_ENV]) {
302
+ return;
303
+ }
295
304
  // Steps 1–3: merge global + project configs
296
305
  const config = readMergedConfig(ctx.cwd);
297
306
  // Step 4: resolve mode from the merged config
@@ -1 +1 @@
1
- {"version":3,"file":"modes.js","sourceRoot":"","sources":["../../../src/extensions/core/modes.ts"],"names":[],"mappings":"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","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 { 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(pi: 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\tpi.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\tpi.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// 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// - pi.addModeSearchPath (CLI flags + extension contributions)\n\t\tconst modePaths = mergeSearchPaths(config.mode_paths, pi.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\tpi.setActiveTools(modeCfg.enabled_tools);\n\t\t}\n\t});\n\n\t// ── before_agent_start ────────────────────────────────────────────────────\n\n\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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"]}
1
+ {"version":3,"file":"modes.js","sourceRoot":"","sources":["../../../src/extensions/core/modes.ts"],"names":[],"mappings":"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","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(pi: 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\tpi.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\tpi.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// - pi.addModeSearchPath (CLI flags + extension contributions)\n\t\tconst modePaths = mergeSearchPaths(config.mode_paths, pi.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\tpi.setActiveTools(modeCfg.enabled_tools);\n\t\t}\n\t});\n\n\t// ── before_agent_start ────────────────────────────────────────────────────\n\n\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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\tpi.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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"permission-gate.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/permission-gate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AA2ExC,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA0H1D","sourcesContent":["/**\n * Permission gate — prompts before bash/write/edit/webfetch/websearch; checks\n * modes.{mode}.auto_allow from the merged (global + project) config; persists\n * \"always\" choices back to the global config. Hard enforcement (denied tools,\n * enabled_tools allowlists, bash command patterns, .webtoolsignore hosts)\n * applies even without a UI.\n */\n\nimport type {\n\tExtensionAPI,\n\tExtensionContext,\n\tToolCallEvent,\n\tToolCallEventResult,\n} from \"../../core/extensions/types.js\";\nimport { isToolCallEventType } from \"../../core/extensions/types.js\";\nimport { blockedHostForUrl } from \"../../core/tools/webtools-shared.js\";\nimport { readConfig, readMergedConfig, writeConfig } from \"./config.js\";\n\nconst GATED_TOOLS = new Set([\"bash\", \"write\", \"edit\", \"webfetch\", \"websearch\"]);\n\n/**\n * Checks if a file path matches any of the allowed patterns.\n * Supports glob patterns with * and exact paths.\n */\nfunction matchesAllowedPath(filePath: string, allowedPatterns: string[]): boolean {\n\tif (allowedPatterns.length === 0) return true;\n\tfor (const pattern of allowedPatterns) {\n\t\t// Exact match\n\t\tif (pattern === filePath) return true;\n\t\t// Glob pattern matching for *\n\t\tif (pattern.includes(\"*\")) {\n\t\t\tconst regex = new RegExp(`^${pattern.replace(/\\*/g, \".*\")}$`);\n\t\t\tif (regex.test(filePath)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Tests a bash command string against a regex pattern string.\n * Returns false (no match) if the pattern is an invalid regex.\n */\nfunction matchesBashPattern(pattern: string, command: string): boolean {\n\ttry {\n\t\treturn new RegExp(pattern).test(command);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * The path an edit/write call targets.\n *\n * Both tools declare the argument as `path`; `file_path` is accepted only\n * because some models emit that name and the renderers have long taken either.\n * Reading `file_path` alone matched neither tool's schema, so the confirmation\n * prompt read \"edit (unknown)\" and `allowed_write_paths` compared against an\n * empty string - which matches no pattern, blocking every write in any mode\n * that configured one.\n */\nfunction mutationPath(input: unknown): string | undefined {\n\tconst args = input as { path?: unknown; file_path?: unknown } | undefined;\n\tif (typeof args?.path === \"string\" && args.path.length > 0) return args.path;\n\tif (typeof args?.file_path === \"string\" && args.file_path.length > 0) return args.file_path;\n\treturn undefined;\n}\n\nfunction describeTool(event: ToolCallEvent): string {\n\tif (isToolCallEventType(\"bash\", event)) {\n\t\treturn `$ ${event.input.command.replace(/\\s+/g, \" \").slice(0, 100)}`;\n\t}\n\tif (isToolCallEventType(\"edit\", event)) {\n\t\treturn `edit ${mutationPath(event.input) ?? \"(unknown)\"}`;\n\t}\n\tif (isToolCallEventType(\"write\", event)) {\n\t\treturn `write ${mutationPath(event.input) ?? \"(unknown)\"}`;\n\t}\n\tif (event.toolName === \"webfetch\") {\n\t\tconst url = (event.input as { url?: string }).url ?? \"(unknown)\";\n\t\treturn `webfetch ${url}`;\n\t}\n\tif (event.toolName === \"websearch\") {\n\t\tconst query = (event.input as { query?: string }).query ?? \"(unknown)\";\n\t\treturn `websearch \"${query}\"`;\n\t}\n\treturn event.toolName;\n}\n\nexport function setupPermissionGate(pi: ExtensionAPI): void {\n\tpi.on(\"tool_call\", async (event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallEventResult | undefined> => {\n\t\t// Use the merged config so project-local entries are respected\n\t\tconst config = readMergedConfig(ctx.cwd);\n\t\tconst mode = config.active_mode ?? \"build\";\n\t\tconst modeCfg = config.modes?.[mode];\n\n\t\t// ── Hard enforcement (always applies, regardless of UI) ───────────────────\n\n\t\t// Explicitly denied tools are blocked unconditionally\n\t\tif (modeCfg?.denied_tools?.includes(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason: `Tool \"${event.toolName}\" is denied in mode \"${mode}\".`,\n\t\t\t};\n\t\t}\n\n\t\t// enabled_tools acts as a strict allowlist: only listed tools may execute\n\t\tif (\n\t\t\tmodeCfg?.enabled_tools &&\n\t\t\tmodeCfg.enabled_tools.length > 0 &&\n\t\t\t!modeCfg.enabled_tools.includes(event.toolName)\n\t\t) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason:\n\t\t\t\t\t`Tool \"${event.toolName}\" is not enabled in mode \"${mode}\" ` +\n\t\t\t\t\t`(enabled: ${modeCfg.enabled_tools.join(\", \")}).`,\n\t\t\t};\n\t\t}\n\n\t\t// Bash command-level filtering\n\t\tif (isToolCallEventType(\"bash\", event)) {\n\t\t\tconst command = (event.input as { command?: string }).command ?? \"\";\n\n\t\t\t// denied_bash_commands: block if any pattern matches\n\t\t\tif (modeCfg?.denied_bash_commands?.length) {\n\t\t\t\tfor (const pattern of modeCfg.denied_bash_commands) {\n\t\t\t\t\tif (matchesBashPattern(pattern, command)) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tblock: true,\n\t\t\t\t\t\t\treason: `Bash command matches a denied pattern in mode \"${mode}\": ${pattern}`,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// allowed_bash_commands: block unless at least one pattern matches\n\t\t\tif (modeCfg?.allowed_bash_commands?.length) {\n\t\t\t\tconst permitted = modeCfg.allowed_bash_commands.some((p) => matchesBashPattern(p, command));\n\t\t\t\tif (!permitted) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tblock: true,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t`Bash command is not permitted in mode \"${mode}\". ` +\n\t\t\t\t\t\t\t`Allowed patterns: ${modeCfg.allowed_bash_commands.join(\", \")}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// webfetch host policy (.webtoolsignore). Hard enforcement, always applies:\n\t\t// a blocked host is denied even in headless runs. SSRF/private-address\n\t\t// blocking lives in the webtools binary; this is host allow/deny policy only.\n\t\tif (event.toolName === \"webfetch\") {\n\t\t\tconst url = (event.input as { url?: string }).url ?? \"\";\n\t\t\tconst blockedHost = url ? blockedHostForUrl(ctx.cwd, url) : undefined;\n\t\t\tif (blockedHost) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason: `Host \"${blockedHost}\" is blocked by .webtoolsignore policy.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// ── UI-based permission prompting (interactive sessions only) ─────────────\n\n\t\tif (!GATED_TOOLS.has(event.toolName) || !ctx.hasUI) return;\n\n\t\tconst autoAllow = modeCfg?.auto_allow ?? [];\n\n\t\t// Check allowed_write_paths for write/edit operations\n\t\tif ((event.toolName === \"write\" || event.toolName === \"edit\") && modeCfg?.allowed_write_paths) {\n\t\t\t// Absent path stays blocked: an unidentifiable write cannot be checked\n\t\t\t// against an allowlist, and refusing is the safe direction.\n\t\t\tconst filePath = mutationPath(event.input) ?? \"\";\n\t\t\tif (!matchesAllowedPath(filePath, modeCfg.allowed_write_paths)) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason:\n\t\t\t\t\t\t`Mode \"${mode}\" only allows writes to: ${modeCfg.allowed_write_paths.join(\", \")}. ` +\n\t\t\t\t\t\t`Attempted to ${event.toolName}: ${filePath}. ` +\n\t\t\t\t\t\t`Switch to \"/mode build\" to modify source files.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (autoAllow.includes(event.toolName)) return;\n\n\t\tconst choice = await ctx.ui.select(`Allow: ${describeTool(event)}`, [\n\t\t\t\"Yes (once)\",\n\t\t\t\"No (block)\",\n\t\t\t\"Always (add to auto-allow for this mode)\",\n\t\t]);\n\n\t\tif (!choice || choice.startsWith(\"No\")) {\n\t\t\treturn { block: true, reason: \"Denied by permission gate\" };\n\t\t}\n\n\t\tif (choice.startsWith(\"Always\")) {\n\t\t\t// Write \"always\" choices to the global config only\n\t\t\tconst latest = readConfig();\n\t\t\tconst currentMode = latest.active_mode ?? \"build\";\n\t\t\tlatest.modes ??= {};\n\t\t\tlatest.modes[currentMode] ??= {};\n\t\t\tlatest.modes[currentMode].auto_allow = Array.from(\n\t\t\t\tnew Set([...(latest.modes[currentMode].auto_allow ?? []), event.toolName]),\n\t\t\t);\n\t\t\twriteConfig(latest);\n\t\t\tctx.ui.notify(`\"${event.toolName}\" added to auto-allow for mode \"${currentMode}\"`, \"info\");\n\t\t}\n\t});\n}\n"]}
1
+ {"version":3,"file":"permission-gate.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/permission-gate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AAuGxC,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA0H1D","sourcesContent":["/**\n * Permission gate — prompts before bash/write/edit/webfetch/websearch; checks\n * modes.{mode}.auto_allow from the merged (global + project) config; persists\n * \"always\" choices back to the global config. Hard enforcement (denied tools,\n * enabled_tools allowlists, bash command patterns, .webtoolsignore hosts)\n * applies even without a UI.\n */\n\nimport { isAbsolute, relative } from \"node:path\";\nimport type {\n\tExtensionAPI,\n\tExtensionContext,\n\tToolCallEvent,\n\tToolCallEventResult,\n} from \"../../core/extensions/types.js\";\nimport { isToolCallEventType } from \"../../core/extensions/types.js\";\nimport { blockedHostForUrl } from \"../../core/tools/webtools-shared.js\";\nimport { readConfig, readMergedConfig, writeConfig } from \"./config.js\";\n\nconst GATED_TOOLS = new Set([\"bash\", \"write\", \"edit\", \"webfetch\", \"websearch\"]);\n\n/**\n * Checks if a file path matches any of the mode's `allowed_write_paths`.\n * Supports glob patterns with `*` and exact paths.\n *\n * Both sides are normalized to forward slashes first. Patterns are written with\n * `/`, but the paths a model is handed come from `relative()`, which yields\n * backslashes on Windows — and because this check runs *before* `auto_allow`, a\n * non-match is a hard block rather than a prompt. Without normalizing, a mode\n * that sets `allowed_write_paths` could not write anything at all on Windows.\n *\n * An absolute path is also tried in its cwd-relative form, so a model that\n * resolves the relative path it was given still matches. A target outside cwd\n * relativizes to `../…` and correctly fails to match.\n */\nfunction matchesAllowedPath(filePath: string, allowedPatterns: string[], cwd: string): boolean {\n\tif (allowedPatterns.length === 0) return true;\n\tif (!filePath) return false;\n\n\tconst toPosix = (value: string): string => value.replace(/\\\\/g, \"/\");\n\n\tconst candidates = new Set<string>([toPosix(filePath)]);\n\tif (isAbsolute(filePath)) {\n\t\tconst rel = toPosix(relative(cwd, filePath));\n\t\tif (rel) candidates.add(rel);\n\t}\n\n\tfor (const pattern of allowedPatterns) {\n\t\tconst normalizedPattern = toPosix(pattern);\n\t\tif (candidates.has(normalizedPattern)) return true;\n\t\tif (!normalizedPattern.includes(\"*\")) continue;\n\t\t// Escape regex metacharacters before letting `*` mean \"any run of chars\",\n\t\t// so a pattern like `.hoocode/plans/*` cannot also match `Xhoocode/...`.\n\t\tconst source = `^${normalizedPattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\")}$`;\n\t\tlet regex: RegExp;\n\t\ttry {\n\t\t\tregex = new RegExp(source);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const candidate of candidates) {\n\t\t\tif (regex.test(candidate)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Tests a bash command string against a regex pattern string.\n * Returns false (no match) if the pattern is an invalid regex.\n */\nfunction matchesBashPattern(pattern: string, command: string): boolean {\n\ttry {\n\t\treturn new RegExp(pattern).test(command);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * The path an edit/write call targets.\n *\n * Both tools declare the argument as `path`; `file_path` is accepted only\n * because some models emit that name and the renderers have long taken either.\n * Reading `file_path` alone matched neither tool's schema, so the confirmation\n * prompt read \"edit (unknown)\" and `allowed_write_paths` compared against an\n * empty string - which matches no pattern, blocking every write in any mode\n * that configured one.\n */\nfunction mutationPath(input: unknown): string | undefined {\n\tconst args = input as { path?: unknown; file_path?: unknown } | undefined;\n\tif (typeof args?.path === \"string\" && args.path.length > 0) return args.path;\n\tif (typeof args?.file_path === \"string\" && args.file_path.length > 0) return args.file_path;\n\treturn undefined;\n}\n\nfunction describeTool(event: ToolCallEvent): string {\n\tif (isToolCallEventType(\"bash\", event)) {\n\t\treturn `$ ${event.input.command.replace(/\\s+/g, \" \").slice(0, 100)}`;\n\t}\n\tif (isToolCallEventType(\"edit\", event)) {\n\t\treturn `edit ${mutationPath(event.input) ?? \"(unknown)\"}`;\n\t}\n\tif (isToolCallEventType(\"write\", event)) {\n\t\treturn `write ${mutationPath(event.input) ?? \"(unknown)\"}`;\n\t}\n\tif (event.toolName === \"webfetch\") {\n\t\tconst url = (event.input as { url?: string }).url ?? \"(unknown)\";\n\t\treturn `webfetch ${url}`;\n\t}\n\tif (event.toolName === \"websearch\") {\n\t\tconst query = (event.input as { query?: string }).query ?? \"(unknown)\";\n\t\treturn `websearch \"${query}\"`;\n\t}\n\treturn event.toolName;\n}\n\nexport function setupPermissionGate(pi: ExtensionAPI): void {\n\tpi.on(\"tool_call\", async (event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallEventResult | undefined> => {\n\t\t// Use the merged config so project-local entries are respected\n\t\tconst config = readMergedConfig(ctx.cwd);\n\t\tconst mode = config.active_mode ?? \"build\";\n\t\tconst modeCfg = config.modes?.[mode];\n\n\t\t// ── Hard enforcement (always applies, regardless of UI) ───────────────────\n\n\t\t// Explicitly denied tools are blocked unconditionally\n\t\tif (modeCfg?.denied_tools?.includes(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason: `Tool \"${event.toolName}\" is denied in mode \"${mode}\".`,\n\t\t\t};\n\t\t}\n\n\t\t// enabled_tools acts as a strict allowlist: only listed tools may execute\n\t\tif (\n\t\t\tmodeCfg?.enabled_tools &&\n\t\t\tmodeCfg.enabled_tools.length > 0 &&\n\t\t\t!modeCfg.enabled_tools.includes(event.toolName)\n\t\t) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason:\n\t\t\t\t\t`Tool \"${event.toolName}\" is not enabled in mode \"${mode}\" ` +\n\t\t\t\t\t`(enabled: ${modeCfg.enabled_tools.join(\", \")}).`,\n\t\t\t};\n\t\t}\n\n\t\t// Bash command-level filtering\n\t\tif (isToolCallEventType(\"bash\", event)) {\n\t\t\tconst command = (event.input as { command?: string }).command ?? \"\";\n\n\t\t\t// denied_bash_commands: block if any pattern matches\n\t\t\tif (modeCfg?.denied_bash_commands?.length) {\n\t\t\t\tfor (const pattern of modeCfg.denied_bash_commands) {\n\t\t\t\t\tif (matchesBashPattern(pattern, command)) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tblock: true,\n\t\t\t\t\t\t\treason: `Bash command matches a denied pattern in mode \"${mode}\": ${pattern}`,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// allowed_bash_commands: block unless at least one pattern matches\n\t\t\tif (modeCfg?.allowed_bash_commands?.length) {\n\t\t\t\tconst permitted = modeCfg.allowed_bash_commands.some((p) => matchesBashPattern(p, command));\n\t\t\t\tif (!permitted) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tblock: true,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t`Bash command is not permitted in mode \"${mode}\". ` +\n\t\t\t\t\t\t\t`Allowed patterns: ${modeCfg.allowed_bash_commands.join(\", \")}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// webfetch host policy (.webtoolsignore). Hard enforcement, always applies:\n\t\t// a blocked host is denied even in headless runs. SSRF/private-address\n\t\t// blocking lives in the webtools binary; this is host allow/deny policy only.\n\t\tif (event.toolName === \"webfetch\") {\n\t\t\tconst url = (event.input as { url?: string }).url ?? \"\";\n\t\t\tconst blockedHost = url ? blockedHostForUrl(ctx.cwd, url) : undefined;\n\t\t\tif (blockedHost) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason: `Host \"${blockedHost}\" is blocked by .webtoolsignore policy.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// ── UI-based permission prompting (interactive sessions only) ─────────────\n\n\t\tif (!GATED_TOOLS.has(event.toolName) || !ctx.hasUI) return;\n\n\t\tconst autoAllow = modeCfg?.auto_allow ?? [];\n\n\t\t// Check allowed_write_paths for write/edit operations\n\t\tif ((event.toolName === \"write\" || event.toolName === \"edit\") && modeCfg?.allowed_write_paths) {\n\t\t\t// Absent path stays blocked: an unidentifiable write cannot be checked\n\t\t\t// against an allowlist, and refusing is the safe direction.\n\t\t\tconst filePath = mutationPath(event.input) ?? \"\";\n\t\t\tif (!matchesAllowedPath(filePath, modeCfg.allowed_write_paths, ctx.cwd)) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason:\n\t\t\t\t\t\t`Mode \"${mode}\" only allows writes to: ${modeCfg.allowed_write_paths.join(\", \")}. ` +\n\t\t\t\t\t\t`Attempted to ${event.toolName}: ${filePath}. ` +\n\t\t\t\t\t\t`Switch to \"/mode build\" to modify source files.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (autoAllow.includes(event.toolName)) return;\n\n\t\tconst choice = await ctx.ui.select(`Allow: ${describeTool(event)}`, [\n\t\t\t\"Yes (once)\",\n\t\t\t\"No (block)\",\n\t\t\t\"Always (add to auto-allow for this mode)\",\n\t\t]);\n\n\t\tif (!choice || choice.startsWith(\"No\")) {\n\t\t\treturn { block: true, reason: \"Denied by permission gate\" };\n\t\t}\n\n\t\tif (choice.startsWith(\"Always\")) {\n\t\t\t// Write \"always\" choices to the global config only\n\t\t\tconst latest = readConfig();\n\t\t\tconst currentMode = latest.active_mode ?? \"build\";\n\t\t\tlatest.modes ??= {};\n\t\t\tlatest.modes[currentMode] ??= {};\n\t\t\tlatest.modes[currentMode].auto_allow = Array.from(\n\t\t\t\tnew Set([...(latest.modes[currentMode].auto_allow ?? []), event.toolName]),\n\t\t\t);\n\t\t\twriteConfig(latest);\n\t\t\tctx.ui.notify(`\"${event.toolName}\" added to auto-allow for mode \"${currentMode}\"`, \"info\");\n\t\t}\n\t});\n}\n"]}
@@ -5,25 +5,55 @@
5
5
  * enabled_tools allowlists, bash command patterns, .webtoolsignore hosts)
6
6
  * applies even without a UI.
7
7
  */
8
+ import { isAbsolute, relative } from "node:path";
8
9
  import { isToolCallEventType } from "../../core/extensions/types.js";
9
10
  import { blockedHostForUrl } from "../../core/tools/webtools-shared.js";
10
11
  import { readConfig, readMergedConfig, writeConfig } from "./config.js";
11
12
  const GATED_TOOLS = new Set(["bash", "write", "edit", "webfetch", "websearch"]);
12
13
  /**
13
- * Checks if a file path matches any of the allowed patterns.
14
- * Supports glob patterns with * and exact paths.
14
+ * Checks if a file path matches any of the mode's `allowed_write_paths`.
15
+ * Supports glob patterns with `*` and exact paths.
16
+ *
17
+ * Both sides are normalized to forward slashes first. Patterns are written with
18
+ * `/`, but the paths a model is handed come from `relative()`, which yields
19
+ * backslashes on Windows — and because this check runs *before* `auto_allow`, a
20
+ * non-match is a hard block rather than a prompt. Without normalizing, a mode
21
+ * that sets `allowed_write_paths` could not write anything at all on Windows.
22
+ *
23
+ * An absolute path is also tried in its cwd-relative form, so a model that
24
+ * resolves the relative path it was given still matches. A target outside cwd
25
+ * relativizes to `../…` and correctly fails to match.
15
26
  */
16
- function matchesAllowedPath(filePath, allowedPatterns) {
27
+ function matchesAllowedPath(filePath, allowedPatterns, cwd) {
17
28
  if (allowedPatterns.length === 0)
18
29
  return true;
30
+ if (!filePath)
31
+ return false;
32
+ const toPosix = (value) => value.replace(/\\/g, "/");
33
+ const candidates = new Set([toPosix(filePath)]);
34
+ if (isAbsolute(filePath)) {
35
+ const rel = toPosix(relative(cwd, filePath));
36
+ if (rel)
37
+ candidates.add(rel);
38
+ }
19
39
  for (const pattern of allowedPatterns) {
20
- // Exact match
21
- if (pattern === filePath)
40
+ const normalizedPattern = toPosix(pattern);
41
+ if (candidates.has(normalizedPattern))
22
42
  return true;
23
- // Glob pattern matching for *
24
- if (pattern.includes("*")) {
25
- const regex = new RegExp(`^${pattern.replace(/\*/g, ".*")}$`);
26
- if (regex.test(filePath))
43
+ if (!normalizedPattern.includes("*"))
44
+ continue;
45
+ // Escape regex metacharacters before letting `*` mean "any run of chars",
46
+ // so a pattern like `.hoocode/plans/*` cannot also match `Xhoocode/...`.
47
+ const source = `^${normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`;
48
+ let regex;
49
+ try {
50
+ regex = new RegExp(source);
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ for (const candidate of candidates) {
56
+ if (regex.test(candidate))
27
57
  return true;
28
58
  }
29
59
  }
@@ -151,7 +181,7 @@ export function setupPermissionGate(pi) {
151
181
  // Absent path stays blocked: an unidentifiable write cannot be checked
152
182
  // against an allowlist, and refusing is the safe direction.
153
183
  const filePath = mutationPath(event.input) ?? "";
154
- if (!matchesAllowedPath(filePath, modeCfg.allowed_write_paths)) {
184
+ if (!matchesAllowedPath(filePath, modeCfg.allowed_write_paths, ctx.cwd)) {
155
185
  return {
156
186
  block: true,
157
187
  reason: `Mode "${mode}" only allows writes to: ${modeCfg.allowed_write_paths.join(", ")}. ` +
@@ -1 +1 @@
1
- {"version":3,"file":"permission-gate.js","sourceRoot":"","sources":["../../../src/extensions/core/permission-gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Permission gate — prompts before bash/write/edit/webfetch/websearch; checks\n * modes.{mode}.auto_allow from the merged (global + project) config; persists\n * \"always\" choices back to the global config. Hard enforcement (denied tools,\n * enabled_tools allowlists, bash command patterns, .webtoolsignore hosts)\n * applies even without a UI.\n */\n\nimport type {\n\tExtensionAPI,\n\tExtensionContext,\n\tToolCallEvent,\n\tToolCallEventResult,\n} from \"../../core/extensions/types.js\";\nimport { isToolCallEventType } from \"../../core/extensions/types.js\";\nimport { blockedHostForUrl } from \"../../core/tools/webtools-shared.js\";\nimport { readConfig, readMergedConfig, writeConfig } from \"./config.js\";\n\nconst GATED_TOOLS = new Set([\"bash\", \"write\", \"edit\", \"webfetch\", \"websearch\"]);\n\n/**\n * Checks if a file path matches any of the allowed patterns.\n * Supports glob patterns with * and exact paths.\n */\nfunction matchesAllowedPath(filePath: string, allowedPatterns: string[]): boolean {\n\tif (allowedPatterns.length === 0) return true;\n\tfor (const pattern of allowedPatterns) {\n\t\t// Exact match\n\t\tif (pattern === filePath) return true;\n\t\t// Glob pattern matching for *\n\t\tif (pattern.includes(\"*\")) {\n\t\t\tconst regex = new RegExp(`^${pattern.replace(/\\*/g, \".*\")}$`);\n\t\t\tif (regex.test(filePath)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Tests a bash command string against a regex pattern string.\n * Returns false (no match) if the pattern is an invalid regex.\n */\nfunction matchesBashPattern(pattern: string, command: string): boolean {\n\ttry {\n\t\treturn new RegExp(pattern).test(command);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * The path an edit/write call targets.\n *\n * Both tools declare the argument as `path`; `file_path` is accepted only\n * because some models emit that name and the renderers have long taken either.\n * Reading `file_path` alone matched neither tool's schema, so the confirmation\n * prompt read \"edit (unknown)\" and `allowed_write_paths` compared against an\n * empty string - which matches no pattern, blocking every write in any mode\n * that configured one.\n */\nfunction mutationPath(input: unknown): string | undefined {\n\tconst args = input as { path?: unknown; file_path?: unknown } | undefined;\n\tif (typeof args?.path === \"string\" && args.path.length > 0) return args.path;\n\tif (typeof args?.file_path === \"string\" && args.file_path.length > 0) return args.file_path;\n\treturn undefined;\n}\n\nfunction describeTool(event: ToolCallEvent): string {\n\tif (isToolCallEventType(\"bash\", event)) {\n\t\treturn `$ ${event.input.command.replace(/\\s+/g, \" \").slice(0, 100)}`;\n\t}\n\tif (isToolCallEventType(\"edit\", event)) {\n\t\treturn `edit ${mutationPath(event.input) ?? \"(unknown)\"}`;\n\t}\n\tif (isToolCallEventType(\"write\", event)) {\n\t\treturn `write ${mutationPath(event.input) ?? \"(unknown)\"}`;\n\t}\n\tif (event.toolName === \"webfetch\") {\n\t\tconst url = (event.input as { url?: string }).url ?? \"(unknown)\";\n\t\treturn `webfetch ${url}`;\n\t}\n\tif (event.toolName === \"websearch\") {\n\t\tconst query = (event.input as { query?: string }).query ?? \"(unknown)\";\n\t\treturn `websearch \"${query}\"`;\n\t}\n\treturn event.toolName;\n}\n\nexport function setupPermissionGate(pi: ExtensionAPI): void {\n\tpi.on(\"tool_call\", async (event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallEventResult | undefined> => {\n\t\t// Use the merged config so project-local entries are respected\n\t\tconst config = readMergedConfig(ctx.cwd);\n\t\tconst mode = config.active_mode ?? \"build\";\n\t\tconst modeCfg = config.modes?.[mode];\n\n\t\t// ── Hard enforcement (always applies, regardless of UI) ───────────────────\n\n\t\t// Explicitly denied tools are blocked unconditionally\n\t\tif (modeCfg?.denied_tools?.includes(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason: `Tool \"${event.toolName}\" is denied in mode \"${mode}\".`,\n\t\t\t};\n\t\t}\n\n\t\t// enabled_tools acts as a strict allowlist: only listed tools may execute\n\t\tif (\n\t\t\tmodeCfg?.enabled_tools &&\n\t\t\tmodeCfg.enabled_tools.length > 0 &&\n\t\t\t!modeCfg.enabled_tools.includes(event.toolName)\n\t\t) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason:\n\t\t\t\t\t`Tool \"${event.toolName}\" is not enabled in mode \"${mode}\" ` +\n\t\t\t\t\t`(enabled: ${modeCfg.enabled_tools.join(\", \")}).`,\n\t\t\t};\n\t\t}\n\n\t\t// Bash command-level filtering\n\t\tif (isToolCallEventType(\"bash\", event)) {\n\t\t\tconst command = (event.input as { command?: string }).command ?? \"\";\n\n\t\t\t// denied_bash_commands: block if any pattern matches\n\t\t\tif (modeCfg?.denied_bash_commands?.length) {\n\t\t\t\tfor (const pattern of modeCfg.denied_bash_commands) {\n\t\t\t\t\tif (matchesBashPattern(pattern, command)) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tblock: true,\n\t\t\t\t\t\t\treason: `Bash command matches a denied pattern in mode \"${mode}\": ${pattern}`,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// allowed_bash_commands: block unless at least one pattern matches\n\t\t\tif (modeCfg?.allowed_bash_commands?.length) {\n\t\t\t\tconst permitted = modeCfg.allowed_bash_commands.some((p) => matchesBashPattern(p, command));\n\t\t\t\tif (!permitted) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tblock: true,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t`Bash command is not permitted in mode \"${mode}\". ` +\n\t\t\t\t\t\t\t`Allowed patterns: ${modeCfg.allowed_bash_commands.join(\", \")}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// webfetch host policy (.webtoolsignore). Hard enforcement, always applies:\n\t\t// a blocked host is denied even in headless runs. SSRF/private-address\n\t\t// blocking lives in the webtools binary; this is host allow/deny policy only.\n\t\tif (event.toolName === \"webfetch\") {\n\t\t\tconst url = (event.input as { url?: string }).url ?? \"\";\n\t\t\tconst blockedHost = url ? blockedHostForUrl(ctx.cwd, url) : undefined;\n\t\t\tif (blockedHost) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason: `Host \"${blockedHost}\" is blocked by .webtoolsignore policy.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// ── UI-based permission prompting (interactive sessions only) ─────────────\n\n\t\tif (!GATED_TOOLS.has(event.toolName) || !ctx.hasUI) return;\n\n\t\tconst autoAllow = modeCfg?.auto_allow ?? [];\n\n\t\t// Check allowed_write_paths for write/edit operations\n\t\tif ((event.toolName === \"write\" || event.toolName === \"edit\") && modeCfg?.allowed_write_paths) {\n\t\t\t// Absent path stays blocked: an unidentifiable write cannot be checked\n\t\t\t// against an allowlist, and refusing is the safe direction.\n\t\t\tconst filePath = mutationPath(event.input) ?? \"\";\n\t\t\tif (!matchesAllowedPath(filePath, modeCfg.allowed_write_paths)) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason:\n\t\t\t\t\t\t`Mode \"${mode}\" only allows writes to: ${modeCfg.allowed_write_paths.join(\", \")}. ` +\n\t\t\t\t\t\t`Attempted to ${event.toolName}: ${filePath}. ` +\n\t\t\t\t\t\t`Switch to \"/mode build\" to modify source files.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (autoAllow.includes(event.toolName)) return;\n\n\t\tconst choice = await ctx.ui.select(`Allow: ${describeTool(event)}`, [\n\t\t\t\"Yes (once)\",\n\t\t\t\"No (block)\",\n\t\t\t\"Always (add to auto-allow for this mode)\",\n\t\t]);\n\n\t\tif (!choice || choice.startsWith(\"No\")) {\n\t\t\treturn { block: true, reason: \"Denied by permission gate\" };\n\t\t}\n\n\t\tif (choice.startsWith(\"Always\")) {\n\t\t\t// Write \"always\" choices to the global config only\n\t\t\tconst latest = readConfig();\n\t\t\tconst currentMode = latest.active_mode ?? \"build\";\n\t\t\tlatest.modes ??= {};\n\t\t\tlatest.modes[currentMode] ??= {};\n\t\t\tlatest.modes[currentMode].auto_allow = Array.from(\n\t\t\t\tnew Set([...(latest.modes[currentMode].auto_allow ?? []), event.toolName]),\n\t\t\t);\n\t\t\twriteConfig(latest);\n\t\t\tctx.ui.notify(`\"${event.toolName}\" added to auto-allow for mode \"${currentMode}\"`, \"info\");\n\t\t}\n\t});\n}\n"]}
1
+ {"version":3,"file":"permission-gate.js","sourceRoot":"","sources":["../../../src/extensions/core/permission-gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Permission gate — prompts before bash/write/edit/webfetch/websearch; checks\n * modes.{mode}.auto_allow from the merged (global + project) config; persists\n * \"always\" choices back to the global config. Hard enforcement (denied tools,\n * enabled_tools allowlists, bash command patterns, .webtoolsignore hosts)\n * applies even without a UI.\n */\n\nimport { isAbsolute, relative } from \"node:path\";\nimport type {\n\tExtensionAPI,\n\tExtensionContext,\n\tToolCallEvent,\n\tToolCallEventResult,\n} from \"../../core/extensions/types.js\";\nimport { isToolCallEventType } from \"../../core/extensions/types.js\";\nimport { blockedHostForUrl } from \"../../core/tools/webtools-shared.js\";\nimport { readConfig, readMergedConfig, writeConfig } from \"./config.js\";\n\nconst GATED_TOOLS = new Set([\"bash\", \"write\", \"edit\", \"webfetch\", \"websearch\"]);\n\n/**\n * Checks if a file path matches any of the mode's `allowed_write_paths`.\n * Supports glob patterns with `*` and exact paths.\n *\n * Both sides are normalized to forward slashes first. Patterns are written with\n * `/`, but the paths a model is handed come from `relative()`, which yields\n * backslashes on Windows — and because this check runs *before* `auto_allow`, a\n * non-match is a hard block rather than a prompt. Without normalizing, a mode\n * that sets `allowed_write_paths` could not write anything at all on Windows.\n *\n * An absolute path is also tried in its cwd-relative form, so a model that\n * resolves the relative path it was given still matches. A target outside cwd\n * relativizes to `../…` and correctly fails to match.\n */\nfunction matchesAllowedPath(filePath: string, allowedPatterns: string[], cwd: string): boolean {\n\tif (allowedPatterns.length === 0) return true;\n\tif (!filePath) return false;\n\n\tconst toPosix = (value: string): string => value.replace(/\\\\/g, \"/\");\n\n\tconst candidates = new Set<string>([toPosix(filePath)]);\n\tif (isAbsolute(filePath)) {\n\t\tconst rel = toPosix(relative(cwd, filePath));\n\t\tif (rel) candidates.add(rel);\n\t}\n\n\tfor (const pattern of allowedPatterns) {\n\t\tconst normalizedPattern = toPosix(pattern);\n\t\tif (candidates.has(normalizedPattern)) return true;\n\t\tif (!normalizedPattern.includes(\"*\")) continue;\n\t\t// Escape regex metacharacters before letting `*` mean \"any run of chars\",\n\t\t// so a pattern like `.hoocode/plans/*` cannot also match `Xhoocode/...`.\n\t\tconst source = `^${normalizedPattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\")}$`;\n\t\tlet regex: RegExp;\n\t\ttry {\n\t\t\tregex = new RegExp(source);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const candidate of candidates) {\n\t\t\tif (regex.test(candidate)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Tests a bash command string against a regex pattern string.\n * Returns false (no match) if the pattern is an invalid regex.\n */\nfunction matchesBashPattern(pattern: string, command: string): boolean {\n\ttry {\n\t\treturn new RegExp(pattern).test(command);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * The path an edit/write call targets.\n *\n * Both tools declare the argument as `path`; `file_path` is accepted only\n * because some models emit that name and the renderers have long taken either.\n * Reading `file_path` alone matched neither tool's schema, so the confirmation\n * prompt read \"edit (unknown)\" and `allowed_write_paths` compared against an\n * empty string - which matches no pattern, blocking every write in any mode\n * that configured one.\n */\nfunction mutationPath(input: unknown): string | undefined {\n\tconst args = input as { path?: unknown; file_path?: unknown } | undefined;\n\tif (typeof args?.path === \"string\" && args.path.length > 0) return args.path;\n\tif (typeof args?.file_path === \"string\" && args.file_path.length > 0) return args.file_path;\n\treturn undefined;\n}\n\nfunction describeTool(event: ToolCallEvent): string {\n\tif (isToolCallEventType(\"bash\", event)) {\n\t\treturn `$ ${event.input.command.replace(/\\s+/g, \" \").slice(0, 100)}`;\n\t}\n\tif (isToolCallEventType(\"edit\", event)) {\n\t\treturn `edit ${mutationPath(event.input) ?? \"(unknown)\"}`;\n\t}\n\tif (isToolCallEventType(\"write\", event)) {\n\t\treturn `write ${mutationPath(event.input) ?? \"(unknown)\"}`;\n\t}\n\tif (event.toolName === \"webfetch\") {\n\t\tconst url = (event.input as { url?: string }).url ?? \"(unknown)\";\n\t\treturn `webfetch ${url}`;\n\t}\n\tif (event.toolName === \"websearch\") {\n\t\tconst query = (event.input as { query?: string }).query ?? \"(unknown)\";\n\t\treturn `websearch \"${query}\"`;\n\t}\n\treturn event.toolName;\n}\n\nexport function setupPermissionGate(pi: ExtensionAPI): void {\n\tpi.on(\"tool_call\", async (event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallEventResult | undefined> => {\n\t\t// Use the merged config so project-local entries are respected\n\t\tconst config = readMergedConfig(ctx.cwd);\n\t\tconst mode = config.active_mode ?? \"build\";\n\t\tconst modeCfg = config.modes?.[mode];\n\n\t\t// ── Hard enforcement (always applies, regardless of UI) ───────────────────\n\n\t\t// Explicitly denied tools are blocked unconditionally\n\t\tif (modeCfg?.denied_tools?.includes(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason: `Tool \"${event.toolName}\" is denied in mode \"${mode}\".`,\n\t\t\t};\n\t\t}\n\n\t\t// enabled_tools acts as a strict allowlist: only listed tools may execute\n\t\tif (\n\t\t\tmodeCfg?.enabled_tools &&\n\t\t\tmodeCfg.enabled_tools.length > 0 &&\n\t\t\t!modeCfg.enabled_tools.includes(event.toolName)\n\t\t) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason:\n\t\t\t\t\t`Tool \"${event.toolName}\" is not enabled in mode \"${mode}\" ` +\n\t\t\t\t\t`(enabled: ${modeCfg.enabled_tools.join(\", \")}).`,\n\t\t\t};\n\t\t}\n\n\t\t// Bash command-level filtering\n\t\tif (isToolCallEventType(\"bash\", event)) {\n\t\t\tconst command = (event.input as { command?: string }).command ?? \"\";\n\n\t\t\t// denied_bash_commands: block if any pattern matches\n\t\t\tif (modeCfg?.denied_bash_commands?.length) {\n\t\t\t\tfor (const pattern of modeCfg.denied_bash_commands) {\n\t\t\t\t\tif (matchesBashPattern(pattern, command)) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tblock: true,\n\t\t\t\t\t\t\treason: `Bash command matches a denied pattern in mode \"${mode}\": ${pattern}`,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// allowed_bash_commands: block unless at least one pattern matches\n\t\t\tif (modeCfg?.allowed_bash_commands?.length) {\n\t\t\t\tconst permitted = modeCfg.allowed_bash_commands.some((p) => matchesBashPattern(p, command));\n\t\t\t\tif (!permitted) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tblock: true,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t`Bash command is not permitted in mode \"${mode}\". ` +\n\t\t\t\t\t\t\t`Allowed patterns: ${modeCfg.allowed_bash_commands.join(\", \")}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// webfetch host policy (.webtoolsignore). Hard enforcement, always applies:\n\t\t// a blocked host is denied even in headless runs. SSRF/private-address\n\t\t// blocking lives in the webtools binary; this is host allow/deny policy only.\n\t\tif (event.toolName === \"webfetch\") {\n\t\t\tconst url = (event.input as { url?: string }).url ?? \"\";\n\t\t\tconst blockedHost = url ? blockedHostForUrl(ctx.cwd, url) : undefined;\n\t\t\tif (blockedHost) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason: `Host \"${blockedHost}\" is blocked by .webtoolsignore policy.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// ── UI-based permission prompting (interactive sessions only) ─────────────\n\n\t\tif (!GATED_TOOLS.has(event.toolName) || !ctx.hasUI) return;\n\n\t\tconst autoAllow = modeCfg?.auto_allow ?? [];\n\n\t\t// Check allowed_write_paths for write/edit operations\n\t\tif ((event.toolName === \"write\" || event.toolName === \"edit\") && modeCfg?.allowed_write_paths) {\n\t\t\t// Absent path stays blocked: an unidentifiable write cannot be checked\n\t\t\t// against an allowlist, and refusing is the safe direction.\n\t\t\tconst filePath = mutationPath(event.input) ?? \"\";\n\t\t\tif (!matchesAllowedPath(filePath, modeCfg.allowed_write_paths, ctx.cwd)) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason:\n\t\t\t\t\t\t`Mode \"${mode}\" only allows writes to: ${modeCfg.allowed_write_paths.join(\", \")}. ` +\n\t\t\t\t\t\t`Attempted to ${event.toolName}: ${filePath}. ` +\n\t\t\t\t\t\t`Switch to \"/mode build\" to modify source files.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (autoAllow.includes(event.toolName)) return;\n\n\t\tconst choice = await ctx.ui.select(`Allow: ${describeTool(event)}`, [\n\t\t\t\"Yes (once)\",\n\t\t\t\"No (block)\",\n\t\t\t\"Always (add to auto-allow for this mode)\",\n\t\t]);\n\n\t\tif (!choice || choice.startsWith(\"No\")) {\n\t\t\treturn { block: true, reason: \"Denied by permission gate\" };\n\t\t}\n\n\t\tif (choice.startsWith(\"Always\")) {\n\t\t\t// Write \"always\" choices to the global config only\n\t\t\tconst latest = readConfig();\n\t\t\tconst currentMode = latest.active_mode ?? \"build\";\n\t\t\tlatest.modes ??= {};\n\t\t\tlatest.modes[currentMode] ??= {};\n\t\t\tlatest.modes[currentMode].auto_allow = Array.from(\n\t\t\t\tnew Set([...(latest.modes[currentMode].auto_allow ?? []), event.toolName]),\n\t\t\t);\n\t\t\twriteConfig(latest);\n\t\t\tctx.ui.notify(`\"${event.toolName}\" added to auto-allow for mode \"${currentMode}\"`, \"info\");\n\t\t}\n\t});\n}\n"]}