@kolisachint/hoocode-agent 0.4.124 → 0.4.125

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 (30) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/core/settings-defaults.d.ts +1 -1
  3. package/dist/core/settings-defaults.d.ts.map +1 -1
  4. package/dist/core/settings-defaults.js +1 -1
  5. package/dist/core/settings-defaults.js.map +1 -1
  6. package/dist/core/settings-types.d.ts.map +1 -1
  7. package/dist/core/settings-types.js.map +1 -1
  8. package/dist/core/tools/plugins.d.ts.map +1 -1
  9. package/dist/core/tools/plugins.js +8 -0
  10. package/dist/core/tools/plugins.js.map +1 -1
  11. package/dist/extensions/core/hoo-core.d.ts +1 -0
  12. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  13. package/dist/extensions/core/hoo-core.js +3 -0
  14. package/dist/extensions/core/hoo-core.js.map +1 -1
  15. package/dist/extensions/core/prompt-reactive/nudges.d.ts +40 -0
  16. package/dist/extensions/core/prompt-reactive/nudges.d.ts.map +1 -0
  17. package/dist/extensions/core/prompt-reactive/nudges.js +163 -0
  18. package/dist/extensions/core/prompt-reactive/nudges.js.map +1 -0
  19. package/dist/extensions/core/prompt-reactive/policy.d.ts +69 -0
  20. package/dist/extensions/core/prompt-reactive/policy.d.ts.map +1 -0
  21. package/dist/extensions/core/prompt-reactive/policy.js +110 -0
  22. package/dist/extensions/core/prompt-reactive/policy.js.map +1 -0
  23. package/dist/main.d.ts.map +1 -1
  24. package/dist/main.js +3 -0
  25. package/dist/main.js.map +1 -1
  26. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  27. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  28. package/examples/extensions/sandbox/package.json +1 -1
  29. package/examples/extensions/with-deps/package.json +1 -1
  30. package/package.json +4 -4
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Runtime "plugin reuse nudge" — the reactive half of the plugin-productivity
3
+ * guidance.
4
+ *
5
+ * The static `promptGuidelines` on the plugin tools are injected into the system
6
+ * prompt once at session build and never re-surface when the work actually shows
7
+ * a reusable-facing cue. This extension closes that gap: it watches tool output
8
+ * and turn text for the cues in {@link REUSE_NUDGES}, and when one appears it
9
+ * attaches a matching, plugin-facing note to the *next* turn's context — an
10
+ * ephemeral message merged into the outgoing request via the `context` hook, so
11
+ * nothing is written into persisted history.
12
+ *
13
+ * Policy (see ./policy.ts for the cue table — the single source of truth):
14
+ * - Cues are read from `before_agent_start` (user prompt), `tool_execution_start`
15
+ * (tool args, e.g. content being written) and `tool_execution_end` (tool
16
+ * output, e.g. content being read).
17
+ * - First-hit arming: a matching cue arms its nudge immediately.
18
+ * - One note per turn, and at most once per category per session — so a cue
19
+ * that keeps appearing never turns into a nag.
20
+ * - The whole thing is gated on the autonomous-plugin-system flag
21
+ * (`enablePluginTools`, default off) and never blocks normal flow.
22
+ *
23
+ * Wired once from hoo-core (the single default composition root); the guard below
24
+ * makes double-registration a no-op for downstreams that compose extensions
25
+ * differently, and the static import keeps it bundled in the compiled binary.
26
+ */
27
+ import { armReuseNudge, clearArmedReuseNudges, isAutonomousPluginSystemEnabled, matchReuseNudges, } from "./policy.js";
28
+ /** Guards against double-registration when default extensions load more than once. */
29
+ const REGISTERED = Symbol.for("hoocode.promptReactiveNudges.registered");
30
+ /** Cap on how much tool text we scan per event — cues are short, files can be huge. */
31
+ const SCAN_CAP = 20_000;
32
+ /**
33
+ * Pull scannable text out of an arbitrary tool result or args object. Prefers
34
+ * `content[].text` blocks (the tool-result convention) and falls back to a
35
+ * bounded JSON stringify so cues in nested fields are still caught.
36
+ */
37
+ function extractText(value) {
38
+ if (value == null)
39
+ return "";
40
+ if (typeof value === "string")
41
+ return value.slice(0, SCAN_CAP);
42
+ if (typeof value === "object") {
43
+ const obj = value;
44
+ if (Array.isArray(obj.content)) {
45
+ const text = obj.content
46
+ .map((b) => b && typeof b === "object" && typeof b.text === "string"
47
+ ? b.text
48
+ : "")
49
+ .join("\n");
50
+ if (text)
51
+ return text.slice(0, SCAN_CAP);
52
+ }
53
+ try {
54
+ return JSON.stringify(value).slice(0, SCAN_CAP);
55
+ }
56
+ catch {
57
+ return "";
58
+ }
59
+ }
60
+ return String(value).slice(0, SCAN_CAP);
61
+ }
62
+ /**
63
+ * Merge an ephemeral reuse note into the outgoing messages. Appends it as a text
64
+ * block on the final user turn (valid alongside tool-result blocks) so the model
65
+ * sees it right before it responds; falls back to a fresh user message when the
66
+ * last message isn't a user turn. The input array is never mutated.
67
+ */
68
+ function injectNote(messages, note) {
69
+ const block = { type: "text", text: note };
70
+ const out = messages.slice();
71
+ const last = out[out.length - 1];
72
+ if (last && last.role === "user") {
73
+ const content = last.content;
74
+ const merged = typeof content === "string"
75
+ ? [{ type: "text", text: content }, block]
76
+ : Array.isArray(content)
77
+ ? [...content, block]
78
+ : null;
79
+ if (merged) {
80
+ out[out.length - 1] = { ...last, content: merged };
81
+ return out;
82
+ }
83
+ }
84
+ out.push({ role: "user", content: [block], timestamp: Date.now() });
85
+ return out;
86
+ }
87
+ /** Wrap a nudge snippet so it reads as a system aside rather than user text. */
88
+ function formatNote(nudge) {
89
+ return `[reuse-nudge] ${nudge.snippet}`;
90
+ }
91
+ /**
92
+ * Install the runtime reuse-nudge extension. Idempotent — a second call on the
93
+ * same `pi` is a no-op, so composing default extensions twice is harmless.
94
+ */
95
+ export function setupPromptReactiveNudges(pi, options = {}) {
96
+ const guarded = pi;
97
+ if (guarded[REGISTERED])
98
+ return;
99
+ guarded[REGISTERED] = true;
100
+ const isEnabled = options.isEnabled ?? isAutonomousPluginSystemEnabled;
101
+ // Session-scoped state. Categories fire once per session; the pending queue
102
+ // holds armed-but-not-yet-injected nudges; injectedThisTurn caps one per turn.
103
+ const deliveredCategories = new Set();
104
+ const pending = [];
105
+ let injectedThisTurn = false;
106
+ // Cached enablement, recomputed lazily and reset on session start.
107
+ let enabledCache;
108
+ const enabled = (cwd) => {
109
+ if (enabledCache === undefined)
110
+ enabledCache = isEnabled(cwd);
111
+ return enabledCache;
112
+ };
113
+ const enqueue = (text, cwd) => {
114
+ if (!enabled(cwd))
115
+ return;
116
+ for (const nudge of matchReuseNudges(text)) {
117
+ if (deliveredCategories.has(nudge.category))
118
+ continue;
119
+ if (pending.some((p) => p.category === nudge.category))
120
+ continue;
121
+ pending.push(nudge);
122
+ }
123
+ };
124
+ pi.on("session_start", (_event) => {
125
+ deliveredCategories.clear();
126
+ pending.length = 0;
127
+ injectedThisTurn = false;
128
+ enabledCache = undefined;
129
+ clearArmedReuseNudges();
130
+ });
131
+ pi.on("turn_start", (_event) => {
132
+ injectedThisTurn = false;
133
+ });
134
+ pi.on("before_agent_start", (event, ctx) => {
135
+ enqueue(event.prompt, ctx.cwd);
136
+ });
137
+ pi.on("tool_execution_start", (event, ctx) => {
138
+ enqueue(extractText(event.args), ctx.cwd);
139
+ });
140
+ pi.on("tool_execution_end", (event, ctx) => {
141
+ if (event.isError)
142
+ return;
143
+ enqueue(extractText(event.result), ctx.cwd);
144
+ });
145
+ // The injection point: fires before each provider request. transformContext
146
+ // output is request-scoped (never written back to agent state), so the note
147
+ // is ephemeral by construction.
148
+ pi.on("context", (event, ctx) => {
149
+ if (!enabled(ctx.cwd) || injectedThisTurn)
150
+ return undefined;
151
+ // Drop any that raced to "delivered" via another path, then take the oldest.
152
+ while (pending.length > 0 && deliveredCategories.has(pending[0].category))
153
+ pending.shift();
154
+ const nudge = pending.shift();
155
+ if (!nudge)
156
+ return undefined;
157
+ deliveredCategories.add(nudge.category);
158
+ armReuseNudge(nudge);
159
+ injectedThisTurn = true;
160
+ return { messages: injectNote(event.messages, formatNote(nudge)) };
161
+ });
162
+ }
163
+ //# sourceMappingURL=nudges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nudges.js","sourceRoot":"","sources":["../../../../src/extensions/core/prompt-reactive/nudges.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAcH,OAAO,EACN,aAAa,EACb,qBAAqB,EACrB,+BAA+B,EAC/B,gBAAgB,GAEhB,MAAM,aAAa,CAAC;AAErB,sFAAsF;AACtF,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;AAEzE,yFAAuF;AACvF,MAAM,QAAQ,GAAG,MAAM,CAAC;AAUxB;;;;GAIG;AACH,SAAS,WAAW,CAAC,KAAc,EAAU;IAC5C,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC/D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,KAA8B,CAAC;QAC3C,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO;iBACtB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACV,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAQ,CAAwB,CAAC,IAAI,KAAK,QAAQ;gBAC/E,CAAC,CAAE,CAAsB,CAAC,IAAI;gBAC9B,CAAC,CAAC,EAAE,CACL;iBACA,IAAI,CAAC,IAAI,CAAC,CAAC;YACb,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC;YACJ,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,EAAE,CAAC;QACX,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,CACxC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,QAAwB,EAAE,IAAY,EAAkB;IAC3E,MAAM,KAAK,GAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACxD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAqD,CAAC;IACrF,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,MAAM,MAAM,GACX,OAAO,OAAO,KAAK,QAAQ;YAC1B,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAiB,EAAE,KAAK,CAAC;YACzD,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;gBACvB,CAAC,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC;gBACrB,CAAC,CAAC,IAAI,CAAC;QACV,IAAI,MAAM,EAAE,CAAC;YACZ,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,GAAI,IAAe,EAAE,OAAO,EAAE,MAAM,EAAkB,CAAC;YAC/E,OAAO,GAAG,CAAC;QACZ,CAAC;IACF,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAkB,CAAC,CAAC;IACpF,OAAO,GAAG,CAAC;AAAA,CACX;AAED,gFAAgF;AAChF,SAAS,UAAU,CAAC,KAAiB,EAAU;IAC9C,OAAO,iBAAiB,KAAK,CAAC,OAAO,EAAE,CAAC;AAAA,CACxC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,EAAgB,EAAE,OAAO,GAAgC,EAAE,EAAQ;IAC5G,MAAM,OAAO,GAAG,EAAwC,CAAC;IACzD,IAAI,OAAO,CAAC,UAAU,CAAC;QAAE,OAAO;IAChC,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IAE3B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,+BAA+B,CAAC;IAEvE,4EAA4E;IAC5E,+EAA+E;IAC/E,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9C,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAC7B,mEAAmE;IACnE,IAAI,YAAiC,CAAC;IAEtC,MAAM,OAAO,GAAG,CAAC,GAAW,EAAW,EAAE,CAAC;QACzC,IAAI,YAAY,KAAK,SAAS;YAAE,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAC9D,OAAO,YAAY,CAAC;IAAA,CACpB,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,GAAW,EAAQ,EAAE,CAAC;QACpD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO;QAC1B,KAAK,MAAM,KAAK,IAAI,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5C,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACtD,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACjE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IAAA,CACD,CAAC;IAEF,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,MAAyB,EAAE,EAAE,CAAC;QACrD,mBAAmB,CAAC,KAAK,EAAE,CAAC;QAC5B,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QACnB,gBAAgB,GAAG,KAAK,CAAC;QACzB,YAAY,GAAG,SAAS,CAAC;QACzB,qBAAqB,EAAE,CAAC;IAAA,CACxB,CAAC,CAAC;IAEH,EAAE,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAsB,EAAE,EAAE,CAAC;QAC/C,gBAAgB,GAAG,KAAK,CAAC;IAAA,CACzB,CAAC,CAAC;IAEH,EAAE,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,KAA4B,EAAE,GAAqB,EAAE,EAAE,CAAC;QACpF,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAAA,CAC/B,CAAC,CAAC;IAEH,EAAE,CAAC,EAAE,CAAC,sBAAsB,EAAE,CAAC,KAA8B,EAAE,GAAqB,EAAE,EAAE,CAAC;QACxF,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAAA,CAC1C,CAAC,CAAC;IAEH,EAAE,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,KAA4B,EAAE,GAAqB,EAAE,EAAE,CAAC;QACpF,IAAI,KAAK,CAAC,OAAO;YAAE,OAAO;QAC1B,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAAA,CAC5C,CAAC,CAAC;IAEH,4EAA4E;IAC5E,4EAA4E;IAC5E,gCAAgC;IAChC,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAmB,EAAE,GAAqB,EAAE,EAAE,CAAC;QAChE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,gBAAgB;YAAE,OAAO,SAAS,CAAC;QAC5D,6EAA6E;QAC7E,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC;YAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC5F,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxC,aAAa,CAAC,KAAK,CAAC,CAAC;QACrB,gBAAgB,GAAG,IAAI,CAAC;QACxB,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,KAAK,CAAC,QAAQ,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;IAAA,CACnE,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * Runtime \"plugin reuse nudge\" — the reactive half of the plugin-productivity\n * guidance.\n *\n * The static `promptGuidelines` on the plugin tools are injected into the system\n * prompt once at session build and never re-surface when the work actually shows\n * a reusable-facing cue. This extension closes that gap: it watches tool output\n * and turn text for the cues in {@link REUSE_NUDGES}, and when one appears it\n * attaches a matching, plugin-facing note to the *next* turn's context — an\n * ephemeral message merged into the outgoing request via the `context` hook, so\n * nothing is written into persisted history.\n *\n * Policy (see ./policy.ts for the cue table — the single source of truth):\n * - Cues are read from `before_agent_start` (user prompt), `tool_execution_start`\n * (tool args, e.g. content being written) and `tool_execution_end` (tool\n * output, e.g. content being read).\n * - First-hit arming: a matching cue arms its nudge immediately.\n * - One note per turn, and at most once per category per session — so a cue\n * that keeps appearing never turns into a nag.\n * - The whole thing is gated on the autonomous-plugin-system flag\n * (`enablePluginTools`, default off) and never blocks normal flow.\n *\n * Wired once from hoo-core (the single default composition root); the guard below\n * makes double-registration a no-op for downstreams that compose extensions\n * differently, and the static import keeps it bundled in the compiled binary.\n */\n\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { TextContent } from \"@kolisachint/hoocode-ai\";\nimport type {\n\tBeforeAgentStartEvent,\n\tContextEvent,\n\tExtensionAPI,\n\tExtensionContext,\n\tSessionStartEvent,\n\tToolExecutionEndEvent,\n\tToolExecutionStartEvent,\n\tTurnStartEvent,\n} from \"../../../core/extensions/types.js\";\nimport {\n\tarmReuseNudge,\n\tclearArmedReuseNudges,\n\tisAutonomousPluginSystemEnabled,\n\tmatchReuseNudges,\n\ttype ReuseNudge,\n} from \"./policy.js\";\n\n/** Guards against double-registration when default extensions load more than once. */\nconst REGISTERED = Symbol.for(\"hoocode.promptReactiveNudges.registered\");\n\n/** Cap on how much tool text we scan per event — cues are short, files can be huge. */\nconst SCAN_CAP = 20_000;\n\nexport interface PromptReactiveNudgesOptions {\n\t/**\n\t * Enablement gate. Defaults to the shared autonomous-plugin-system flag so the\n\t * reactive nudge and the plugin tool surface flip together. Injectable for tests.\n\t */\n\tisEnabled?: (cwd: string) => boolean;\n}\n\n/**\n * Pull scannable text out of an arbitrary tool result or args object. Prefers\n * `content[].text` blocks (the tool-result convention) and falls back to a\n * bounded JSON stringify so cues in nested fields are still caught.\n */\nfunction extractText(value: unknown): string {\n\tif (value == null) return \"\";\n\tif (typeof value === \"string\") return value.slice(0, SCAN_CAP);\n\tif (typeof value === \"object\") {\n\t\tconst obj = value as { content?: unknown };\n\t\tif (Array.isArray(obj.content)) {\n\t\t\tconst text = obj.content\n\t\t\t\t.map((b) =>\n\t\t\t\t\tb && typeof b === \"object\" && typeof (b as { text?: unknown }).text === \"string\"\n\t\t\t\t\t\t? (b as { text: string }).text\n\t\t\t\t\t\t: \"\",\n\t\t\t\t)\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (text) return text.slice(0, SCAN_CAP);\n\t\t}\n\t\ttry {\n\t\t\treturn JSON.stringify(value).slice(0, SCAN_CAP);\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\treturn String(value).slice(0, SCAN_CAP);\n}\n\n/**\n * Merge an ephemeral reuse note into the outgoing messages. Appends it as a text\n * block on the final user turn (valid alongside tool-result blocks) so the model\n * sees it right before it responds; falls back to a fresh user message when the\n * last message isn't a user turn. The input array is never mutated.\n */\nfunction injectNote(messages: AgentMessage[], note: string): AgentMessage[] {\n\tconst block: TextContent = { type: \"text\", text: note };\n\tconst out = messages.slice();\n\tconst last = out[out.length - 1] as { role?: string; content?: unknown } | undefined;\n\tif (last && last.role === \"user\") {\n\t\tconst content = last.content;\n\t\tconst merged =\n\t\t\ttypeof content === \"string\"\n\t\t\t\t? [{ type: \"text\", text: content } as TextContent, block]\n\t\t\t\t: Array.isArray(content)\n\t\t\t\t\t? [...content, block]\n\t\t\t\t\t: null;\n\t\tif (merged) {\n\t\t\tout[out.length - 1] = { ...(last as object), content: merged } as AgentMessage;\n\t\t\treturn out;\n\t\t}\n\t}\n\tout.push({ role: \"user\", content: [block], timestamp: Date.now() } as AgentMessage);\n\treturn out;\n}\n\n/** Wrap a nudge snippet so it reads as a system aside rather than user text. */\nfunction formatNote(nudge: ReuseNudge): string {\n\treturn `[reuse-nudge] ${nudge.snippet}`;\n}\n\n/**\n * Install the runtime reuse-nudge extension. Idempotent — a second call on the\n * same `pi` is a no-op, so composing default extensions twice is harmless.\n */\nexport function setupPromptReactiveNudges(pi: ExtensionAPI, options: PromptReactiveNudgesOptions = {}): void {\n\tconst guarded = pi as unknown as Record<symbol, boolean>;\n\tif (guarded[REGISTERED]) return;\n\tguarded[REGISTERED] = true;\n\n\tconst isEnabled = options.isEnabled ?? isAutonomousPluginSystemEnabled;\n\n\t// Session-scoped state. Categories fire once per session; the pending queue\n\t// holds armed-but-not-yet-injected nudges; injectedThisTurn caps one per turn.\n\tconst deliveredCategories = new Set<string>();\n\tconst pending: ReuseNudge[] = [];\n\tlet injectedThisTurn = false;\n\t// Cached enablement, recomputed lazily and reset on session start.\n\tlet enabledCache: boolean | undefined;\n\n\tconst enabled = (cwd: string): boolean => {\n\t\tif (enabledCache === undefined) enabledCache = isEnabled(cwd);\n\t\treturn enabledCache;\n\t};\n\n\tconst enqueue = (text: string, cwd: string): void => {\n\t\tif (!enabled(cwd)) return;\n\t\tfor (const nudge of matchReuseNudges(text)) {\n\t\t\tif (deliveredCategories.has(nudge.category)) continue;\n\t\t\tif (pending.some((p) => p.category === nudge.category)) continue;\n\t\t\tpending.push(nudge);\n\t\t}\n\t};\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent) => {\n\t\tdeliveredCategories.clear();\n\t\tpending.length = 0;\n\t\tinjectedThisTurn = false;\n\t\tenabledCache = undefined;\n\t\tclearArmedReuseNudges();\n\t});\n\n\tpi.on(\"turn_start\", (_event: TurnStartEvent) => {\n\t\tinjectedThisTurn = false;\n\t});\n\n\tpi.on(\"before_agent_start\", (event: BeforeAgentStartEvent, ctx: ExtensionContext) => {\n\t\tenqueue(event.prompt, ctx.cwd);\n\t});\n\n\tpi.on(\"tool_execution_start\", (event: ToolExecutionStartEvent, ctx: ExtensionContext) => {\n\t\tenqueue(extractText(event.args), ctx.cwd);\n\t});\n\n\tpi.on(\"tool_execution_end\", (event: ToolExecutionEndEvent, ctx: ExtensionContext) => {\n\t\tif (event.isError) return;\n\t\tenqueue(extractText(event.result), ctx.cwd);\n\t});\n\n\t// The injection point: fires before each provider request. transformContext\n\t// output is request-scoped (never written back to agent state), so the note\n\t// is ephemeral by construction.\n\tpi.on(\"context\", (event: ContextEvent, ctx: ExtensionContext) => {\n\t\tif (!enabled(ctx.cwd) || injectedThisTurn) return undefined;\n\t\t// Drop any that raced to \"delivered\" via another path, then take the oldest.\n\t\twhile (pending.length > 0 && deliveredCategories.has(pending[0]!.category)) pending.shift();\n\t\tconst nudge = pending.shift();\n\t\tif (!nudge) return undefined;\n\t\tdeliveredCategories.add(nudge.category);\n\t\tarmReuseNudge(nudge);\n\t\tinjectedThisTurn = true;\n\t\treturn { messages: injectNote(event.messages, formatNote(nudge)) };\n\t});\n}\n"]}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Prompt-reactive reuse policy — the single source of truth for the runtime
3
+ * "plugin reuse nudge".
4
+ *
5
+ * The plugin-productivity guidance in `core/tools/plugins.ts` and
6
+ * `core/tools/propose-plugin.ts` ships as static `promptGuidelines` folded into
7
+ * the system prompt once at session build. That text is never re-surfaced when
8
+ * the work actually shows an actionable cue, so the model tends to repeat the
9
+ * same advice by hand instead of reaching for the plugin layer.
10
+ *
11
+ * This module holds the reactive policy that fixes that:
12
+ * - {@link REUSE_NUDGES} the curated cue → nudge table
13
+ * - {@link matchReuseNudges} scan runtime text (tool output, turn text) for cues
14
+ * - the armed registry nudges that fired this process, so the plugin
15
+ * layer can surface them even when no tool asked
16
+ * - {@link isAutonomousPluginSystemEnabled}
17
+ * the one enablement gate shared with the
18
+ * tool-attach path in main.ts (settings.json
19
+ * `enablePluginTools`)
20
+ *
21
+ * The extension in `./nudges.ts` wires this policy to tool/turn events; the
22
+ * policy itself stays pure and independently testable.
23
+ *
24
+ * Conservative by construction: the patterns are specific (a false positive
25
+ * becoming a nag is the real risk), each nudge fires at most once per category
26
+ * per session, and every nudge points back at the plugin layer rather than
27
+ * re-stating advice.
28
+ */
29
+ /** A single reactive reuse cue and the plugin-facing nudge it arms. */
30
+ export interface ReuseNudge {
31
+ /** Stable id, also used as the de-dupe key so a nudge fires once per session. */
32
+ id: string;
33
+ /** Human-facing category label (grouping for the one-per-category cap). */
34
+ category: string;
35
+ /** Specific cue matched against runtime text. Kept tight to avoid false positives. */
36
+ pattern: RegExp;
37
+ /** The reusable-facing note injected into the turn; always points at the plugin layer. */
38
+ snippet: string;
39
+ }
40
+ /**
41
+ * Curated cue → nudge table. Seeded from the static plugin-reuse guidance, but
42
+ * keyed on concrete cues that show up in real work (style/policy directives,
43
+ * format standardization, explicit capability gaps). Patterns are deliberately
44
+ * specific — first-hit arming (see nudges.ts) only stays safe if the cue is
45
+ * unambiguous.
46
+ */
47
+ export declare const REUSE_NUDGES: readonly ReuseNudge[];
48
+ /** Record that a nudge fired this session so the plugin layer can surface it. */
49
+ export declare function armReuseNudge(nudge: ReuseNudge): void;
50
+ /** Reuse nudges that have fired this session, in insertion order. */
51
+ export declare function getArmedReuseNudges(): ReuseNudge[];
52
+ /** Reset the armed registry (called on session start). */
53
+ export declare function clearArmedReuseNudges(): void;
54
+ /**
55
+ * Scan a blob of runtime text (tool output, turn text, user prompt) and return
56
+ * every reuse nudge whose cue matches. Pure — arming/de-duping is the caller's
57
+ * job.
58
+ */
59
+ export declare function matchReuseNudges(text: string | undefined | null): ReuseNudge[];
60
+ /**
61
+ * The one enablement gate for the whole autonomous plugin system.
62
+ *
63
+ * Reads settings.json `enablePluginTools` (default false) — the same setting
64
+ * that gates whether the plugin lifecycle tools are attached to the top-level
65
+ * agent in main.ts. Keeping both the tool surface and the reactive nudge behind
66
+ * a single flag is the "whole autonomous plugin system, off by default" switch.
67
+ */
68
+ export declare function isAutonomousPluginSystemEnabled(cwd: string): boolean;
69
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../../../src/extensions/core/prompt-reactive/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH,uEAAuE;AACvE,MAAM,WAAW,UAAU;IAC1B,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAC;IACX,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EA6B7C,CAAC;AAWF,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAErD;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,IAAI,UAAU,EAAE,CAElD;AAED,0DAA0D;AAC1D,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,UAAU,EAAE,CAG9E;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOpE","sourcesContent":["/**\n * Prompt-reactive reuse policy — the single source of truth for the runtime\n * \"plugin reuse nudge\".\n *\n * The plugin-productivity guidance in `core/tools/plugins.ts` and\n * `core/tools/propose-plugin.ts` ships as static `promptGuidelines` folded into\n * the system prompt once at session build. That text is never re-surfaced when\n * the work actually shows an actionable cue, so the model tends to repeat the\n * same advice by hand instead of reaching for the plugin layer.\n *\n * This module holds the reactive policy that fixes that:\n * - {@link REUSE_NUDGES} the curated cue → nudge table\n * - {@link matchReuseNudges} scan runtime text (tool output, turn text) for cues\n * - the armed registry nudges that fired this process, so the plugin\n * layer can surface them even when no tool asked\n * - {@link isAutonomousPluginSystemEnabled}\n * the one enablement gate shared with the\n * tool-attach path in main.ts (settings.json\n * `enablePluginTools`)\n *\n * The extension in `./nudges.ts` wires this policy to tool/turn events; the\n * policy itself stays pure and independently testable.\n *\n * Conservative by construction: the patterns are specific (a false positive\n * becoming a nag is the real risk), each nudge fires at most once per category\n * per session, and every nudge points back at the plugin layer rather than\n * re-stating advice.\n */\n\nimport { getAgentDir } from \"../../../config.js\";\nimport { SettingsManager } from \"../../../core/settings-manager.js\";\n\n/** A single reactive reuse cue and the plugin-facing nudge it arms. */\nexport interface ReuseNudge {\n\t/** Stable id, also used as the de-dupe key so a nudge fires once per session. */\n\tid: string;\n\t/** Human-facing category label (grouping for the one-per-category cap). */\n\tcategory: string;\n\t/** Specific cue matched against runtime text. Kept tight to avoid false positives. */\n\tpattern: RegExp;\n\t/** The reusable-facing note injected into the turn; always points at the plugin layer. */\n\tsnippet: string;\n}\n\n/**\n * Curated cue → nudge table. Seeded from the static plugin-reuse guidance, but\n * keyed on concrete cues that show up in real work (style/policy directives,\n * format standardization, explicit capability gaps). Patterns are deliberately\n * specific — first-hit arming (see nudges.ts) only stays safe if the cue is\n * unambiguous.\n */\nexport const REUSE_NUDGES: readonly ReuseNudge[] = [\n\t{\n\t\tid: \"style-active-voice\",\n\t\tcategory: \"writing-style\",\n\t\tpattern: /\\b(?:in\\s+)?active voice\\b|\\bavoid(?:ing)?\\s+passive voice\\b/i,\n\t\tsnippet:\n\t\t\t'A writing-style rule is in play (\"active voice\"). If this is a recurring convention, it can live as a reusable skill/command instead of being re-applied by hand — SearchPlugins for one, or author it with ProposePlugin.',\n\t},\n\t{\n\t\tid: \"style-avoid-repetition\",\n\t\tcategory: \"writing-style\",\n\t\tpattern: /\\bavoid(?:ing)?\\s+repetition\\b|\\bdon['’]?t\\s+repeat\\b|\\bavoid\\s+repeat(?:ing|s)?\\b/i,\n\t\tsnippet:\n\t\t\t'A writing-style rule is in play (\"avoid repetition\"). A reusable skill/command can encode this convention rather than restating it each time — SearchPlugins for one, or author it with ProposePlugin.',\n\t},\n\t{\n\t\tid: \"format-prefer-json\",\n\t\tcategory: \"output-format\",\n\t\tpattern: /\\bprefer\\s+JSON\\b|\\buse\\s+JSON\\b|\\boutput\\s+(?:as\\s+)?JSON\\b|\\breturn\\s+(?:as\\s+)?JSON\\b/i,\n\t\tsnippet:\n\t\t\t\"An output-format convention is in play (JSON). Standardizing a format is exactly the kind of thing a reusable skill/plugin captures — SearchPlugins for one, or author it with ProposePlugin.\",\n\t},\n\t{\n\t\tid: \"capability-gap\",\n\t\tcategory: \"capability-gap\",\n\t\tpattern: /\\bcapability gap\\b|\\bno tool for\\b|\\blacks? a tool\\b|\\bwish (?:I|we) had a tool\\b/i,\n\t\tsnippet:\n\t\t\t\"This reads like a capability gap. Before hand-rolling it, SearchPlugins for a plugin that fills it (installable and usable this same turn), or author one with ProposePlugin.\",\n\t},\n];\n\n// ── Armed registry (process-scoped) ──────────────────────────────────────────\n//\n// When a nudge fires, its id is recorded here so the plugin layer (SearchPlugins)\n// can surface \"there is already a reuse candidate for this work\" even if the\n// model never explicitly asked. The CLI is single-process per session, so a\n// module-level set is the natural home; nudges.ts clears it on session start.\n\nconst armed = new Map<string, ReuseNudge>();\n\n/** Record that a nudge fired this session so the plugin layer can surface it. */\nexport function armReuseNudge(nudge: ReuseNudge): void {\n\tarmed.set(nudge.id, nudge);\n}\n\n/** Reuse nudges that have fired this session, in insertion order. */\nexport function getArmedReuseNudges(): ReuseNudge[] {\n\treturn [...armed.values()];\n}\n\n/** Reset the armed registry (called on session start). */\nexport function clearArmedReuseNudges(): void {\n\tarmed.clear();\n}\n\n/**\n * Scan a blob of runtime text (tool output, turn text, user prompt) and return\n * every reuse nudge whose cue matches. Pure — arming/de-duping is the caller's\n * job.\n */\nexport function matchReuseNudges(text: string | undefined | null): ReuseNudge[] {\n\tif (!text) return [];\n\treturn REUSE_NUDGES.filter((n) => n.pattern.test(text));\n}\n\n/**\n * The one enablement gate for the whole autonomous plugin system.\n *\n * Reads settings.json `enablePluginTools` (default false) — the same setting\n * that gates whether the plugin lifecycle tools are attached to the top-level\n * agent in main.ts. Keeping both the tool surface and the reactive nudge behind\n * a single flag is the \"whole autonomous plugin system, off by default\" switch.\n */\nexport function isAutonomousPluginSystemEnabled(cwd: string): boolean {\n\ttry {\n\t\treturn SettingsManager.create(cwd, getAgentDir()).getEnablePluginTools();\n\t} catch {\n\t\t// Fail closed: if settings can't be read, treat the autonomous system as off.\n\t\treturn false;\n\t}\n}\n"]}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Prompt-reactive reuse policy — the single source of truth for the runtime
3
+ * "plugin reuse nudge".
4
+ *
5
+ * The plugin-productivity guidance in `core/tools/plugins.ts` and
6
+ * `core/tools/propose-plugin.ts` ships as static `promptGuidelines` folded into
7
+ * the system prompt once at session build. That text is never re-surfaced when
8
+ * the work actually shows an actionable cue, so the model tends to repeat the
9
+ * same advice by hand instead of reaching for the plugin layer.
10
+ *
11
+ * This module holds the reactive policy that fixes that:
12
+ * - {@link REUSE_NUDGES} the curated cue → nudge table
13
+ * - {@link matchReuseNudges} scan runtime text (tool output, turn text) for cues
14
+ * - the armed registry nudges that fired this process, so the plugin
15
+ * layer can surface them even when no tool asked
16
+ * - {@link isAutonomousPluginSystemEnabled}
17
+ * the one enablement gate shared with the
18
+ * tool-attach path in main.ts (settings.json
19
+ * `enablePluginTools`)
20
+ *
21
+ * The extension in `./nudges.ts` wires this policy to tool/turn events; the
22
+ * policy itself stays pure and independently testable.
23
+ *
24
+ * Conservative by construction: the patterns are specific (a false positive
25
+ * becoming a nag is the real risk), each nudge fires at most once per category
26
+ * per session, and every nudge points back at the plugin layer rather than
27
+ * re-stating advice.
28
+ */
29
+ import { getAgentDir } from "../../../config.js";
30
+ import { SettingsManager } from "../../../core/settings-manager.js";
31
+ /**
32
+ * Curated cue → nudge table. Seeded from the static plugin-reuse guidance, but
33
+ * keyed on concrete cues that show up in real work (style/policy directives,
34
+ * format standardization, explicit capability gaps). Patterns are deliberately
35
+ * specific — first-hit arming (see nudges.ts) only stays safe if the cue is
36
+ * unambiguous.
37
+ */
38
+ export const REUSE_NUDGES = [
39
+ {
40
+ id: "style-active-voice",
41
+ category: "writing-style",
42
+ pattern: /\b(?:in\s+)?active voice\b|\bavoid(?:ing)?\s+passive voice\b/i,
43
+ snippet: 'A writing-style rule is in play ("active voice"). If this is a recurring convention, it can live as a reusable skill/command instead of being re-applied by hand — SearchPlugins for one, or author it with ProposePlugin.',
44
+ },
45
+ {
46
+ id: "style-avoid-repetition",
47
+ category: "writing-style",
48
+ pattern: /\bavoid(?:ing)?\s+repetition\b|\bdon['’]?t\s+repeat\b|\bavoid\s+repeat(?:ing|s)?\b/i,
49
+ snippet: 'A writing-style rule is in play ("avoid repetition"). A reusable skill/command can encode this convention rather than restating it each time — SearchPlugins for one, or author it with ProposePlugin.',
50
+ },
51
+ {
52
+ id: "format-prefer-json",
53
+ category: "output-format",
54
+ pattern: /\bprefer\s+JSON\b|\buse\s+JSON\b|\boutput\s+(?:as\s+)?JSON\b|\breturn\s+(?:as\s+)?JSON\b/i,
55
+ snippet: "An output-format convention is in play (JSON). Standardizing a format is exactly the kind of thing a reusable skill/plugin captures — SearchPlugins for one, or author it with ProposePlugin.",
56
+ },
57
+ {
58
+ id: "capability-gap",
59
+ category: "capability-gap",
60
+ pattern: /\bcapability gap\b|\bno tool for\b|\blacks? a tool\b|\bwish (?:I|we) had a tool\b/i,
61
+ snippet: "This reads like a capability gap. Before hand-rolling it, SearchPlugins for a plugin that fills it (installable and usable this same turn), or author one with ProposePlugin.",
62
+ },
63
+ ];
64
+ // ── Armed registry (process-scoped) ──────────────────────────────────────────
65
+ //
66
+ // When a nudge fires, its id is recorded here so the plugin layer (SearchPlugins)
67
+ // can surface "there is already a reuse candidate for this work" even if the
68
+ // model never explicitly asked. The CLI is single-process per session, so a
69
+ // module-level set is the natural home; nudges.ts clears it on session start.
70
+ const armed = new Map();
71
+ /** Record that a nudge fired this session so the plugin layer can surface it. */
72
+ export function armReuseNudge(nudge) {
73
+ armed.set(nudge.id, nudge);
74
+ }
75
+ /** Reuse nudges that have fired this session, in insertion order. */
76
+ export function getArmedReuseNudges() {
77
+ return [...armed.values()];
78
+ }
79
+ /** Reset the armed registry (called on session start). */
80
+ export function clearArmedReuseNudges() {
81
+ armed.clear();
82
+ }
83
+ /**
84
+ * Scan a blob of runtime text (tool output, turn text, user prompt) and return
85
+ * every reuse nudge whose cue matches. Pure — arming/de-duping is the caller's
86
+ * job.
87
+ */
88
+ export function matchReuseNudges(text) {
89
+ if (!text)
90
+ return [];
91
+ return REUSE_NUDGES.filter((n) => n.pattern.test(text));
92
+ }
93
+ /**
94
+ * The one enablement gate for the whole autonomous plugin system.
95
+ *
96
+ * Reads settings.json `enablePluginTools` (default false) — the same setting
97
+ * that gates whether the plugin lifecycle tools are attached to the top-level
98
+ * agent in main.ts. Keeping both the tool surface and the reactive nudge behind
99
+ * a single flag is the "whole autonomous plugin system, off by default" switch.
100
+ */
101
+ export function isAutonomousPluginSystemEnabled(cwd) {
102
+ try {
103
+ return SettingsManager.create(cwd, getAgentDir()).getEnablePluginTools();
104
+ }
105
+ catch {
106
+ // Fail closed: if settings can't be read, treat the autonomous system as off.
107
+ return false;
108
+ }
109
+ }
110
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../../../src/extensions/core/prompt-reactive/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAcpE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAA0B;IAClD;QACC,EAAE,EAAE,oBAAoB;QACxB,QAAQ,EAAE,eAAe;QACzB,OAAO,EAAE,+DAA+D;QACxE,OAAO,EACN,8NAA4N;KAC7N;IACD;QACC,EAAE,EAAE,wBAAwB;QAC5B,QAAQ,EAAE,eAAe;QACzB,OAAO,EAAE,uFAAqF;QAC9F,OAAO,EACN,0MAAwM;KACzM;IACD;QACC,EAAE,EAAE,oBAAoB;QACxB,QAAQ,EAAE,eAAe;QACzB,OAAO,EAAE,2FAA2F;QACpG,OAAO,EACN,iMAA+L;KAChM;IACD;QACC,EAAE,EAAE,gBAAgB;QACpB,QAAQ,EAAE,gBAAgB;QAC1B,OAAO,EAAE,oFAAoF;QAC7F,OAAO,EACN,+KAA+K;KAChL;CACD,CAAC;AAEF,wKAAgF;AAChF,EAAE;AACF,kFAAkF;AAClF,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAE9E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;AAE5C,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,KAAiB,EAAQ;IACtD,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAAA,CAC3B;AAED,qEAAqE;AACrE,MAAM,UAAU,mBAAmB,GAAiB;IACnD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC3B;AAED,0DAA0D;AAC1D,MAAM,UAAU,qBAAqB,GAAS;IAC7C,KAAK,CAAC,KAAK,EAAE,CAAC;AAAA,CACd;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAA+B,EAAgB;IAC/E,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAAA,CACxD;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,+BAA+B,CAAC,GAAW,EAAW;IACrE,IAAI,CAAC;QACJ,OAAO,eAAe,CAAC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC,oBAAoB,EAAE,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACR,8EAA8E;QAC9E,OAAO,KAAK,CAAC;IACd,CAAC;AAAA,CACD","sourcesContent":["/**\n * Prompt-reactive reuse policy — the single source of truth for the runtime\n * \"plugin reuse nudge\".\n *\n * The plugin-productivity guidance in `core/tools/plugins.ts` and\n * `core/tools/propose-plugin.ts` ships as static `promptGuidelines` folded into\n * the system prompt once at session build. That text is never re-surfaced when\n * the work actually shows an actionable cue, so the model tends to repeat the\n * same advice by hand instead of reaching for the plugin layer.\n *\n * This module holds the reactive policy that fixes that:\n * - {@link REUSE_NUDGES} the curated cue → nudge table\n * - {@link matchReuseNudges} scan runtime text (tool output, turn text) for cues\n * - the armed registry nudges that fired this process, so the plugin\n * layer can surface them even when no tool asked\n * - {@link isAutonomousPluginSystemEnabled}\n * the one enablement gate shared with the\n * tool-attach path in main.ts (settings.json\n * `enablePluginTools`)\n *\n * The extension in `./nudges.ts` wires this policy to tool/turn events; the\n * policy itself stays pure and independently testable.\n *\n * Conservative by construction: the patterns are specific (a false positive\n * becoming a nag is the real risk), each nudge fires at most once per category\n * per session, and every nudge points back at the plugin layer rather than\n * re-stating advice.\n */\n\nimport { getAgentDir } from \"../../../config.js\";\nimport { SettingsManager } from \"../../../core/settings-manager.js\";\n\n/** A single reactive reuse cue and the plugin-facing nudge it arms. */\nexport interface ReuseNudge {\n\t/** Stable id, also used as the de-dupe key so a nudge fires once per session. */\n\tid: string;\n\t/** Human-facing category label (grouping for the one-per-category cap). */\n\tcategory: string;\n\t/** Specific cue matched against runtime text. Kept tight to avoid false positives. */\n\tpattern: RegExp;\n\t/** The reusable-facing note injected into the turn; always points at the plugin layer. */\n\tsnippet: string;\n}\n\n/**\n * Curated cue → nudge table. Seeded from the static plugin-reuse guidance, but\n * keyed on concrete cues that show up in real work (style/policy directives,\n * format standardization, explicit capability gaps). Patterns are deliberately\n * specific — first-hit arming (see nudges.ts) only stays safe if the cue is\n * unambiguous.\n */\nexport const REUSE_NUDGES: readonly ReuseNudge[] = [\n\t{\n\t\tid: \"style-active-voice\",\n\t\tcategory: \"writing-style\",\n\t\tpattern: /\\b(?:in\\s+)?active voice\\b|\\bavoid(?:ing)?\\s+passive voice\\b/i,\n\t\tsnippet:\n\t\t\t'A writing-style rule is in play (\"active voice\"). If this is a recurring convention, it can live as a reusable skill/command instead of being re-applied by hand — SearchPlugins for one, or author it with ProposePlugin.',\n\t},\n\t{\n\t\tid: \"style-avoid-repetition\",\n\t\tcategory: \"writing-style\",\n\t\tpattern: /\\bavoid(?:ing)?\\s+repetition\\b|\\bdon['’]?t\\s+repeat\\b|\\bavoid\\s+repeat(?:ing|s)?\\b/i,\n\t\tsnippet:\n\t\t\t'A writing-style rule is in play (\"avoid repetition\"). A reusable skill/command can encode this convention rather than restating it each time — SearchPlugins for one, or author it with ProposePlugin.',\n\t},\n\t{\n\t\tid: \"format-prefer-json\",\n\t\tcategory: \"output-format\",\n\t\tpattern: /\\bprefer\\s+JSON\\b|\\buse\\s+JSON\\b|\\boutput\\s+(?:as\\s+)?JSON\\b|\\breturn\\s+(?:as\\s+)?JSON\\b/i,\n\t\tsnippet:\n\t\t\t\"An output-format convention is in play (JSON). Standardizing a format is exactly the kind of thing a reusable skill/plugin captures — SearchPlugins for one, or author it with ProposePlugin.\",\n\t},\n\t{\n\t\tid: \"capability-gap\",\n\t\tcategory: \"capability-gap\",\n\t\tpattern: /\\bcapability gap\\b|\\bno tool for\\b|\\blacks? a tool\\b|\\bwish (?:I|we) had a tool\\b/i,\n\t\tsnippet:\n\t\t\t\"This reads like a capability gap. Before hand-rolling it, SearchPlugins for a plugin that fills it (installable and usable this same turn), or author one with ProposePlugin.\",\n\t},\n];\n\n// ── Armed registry (process-scoped) ──────────────────────────────────────────\n//\n// When a nudge fires, its id is recorded here so the plugin layer (SearchPlugins)\n// can surface \"there is already a reuse candidate for this work\" even if the\n// model never explicitly asked. The CLI is single-process per session, so a\n// module-level set is the natural home; nudges.ts clears it on session start.\n\nconst armed = new Map<string, ReuseNudge>();\n\n/** Record that a nudge fired this session so the plugin layer can surface it. */\nexport function armReuseNudge(nudge: ReuseNudge): void {\n\tarmed.set(nudge.id, nudge);\n}\n\n/** Reuse nudges that have fired this session, in insertion order. */\nexport function getArmedReuseNudges(): ReuseNudge[] {\n\treturn [...armed.values()];\n}\n\n/** Reset the armed registry (called on session start). */\nexport function clearArmedReuseNudges(): void {\n\tarmed.clear();\n}\n\n/**\n * Scan a blob of runtime text (tool output, turn text, user prompt) and return\n * every reuse nudge whose cue matches. Pure — arming/de-duping is the caller's\n * job.\n */\nexport function matchReuseNudges(text: string | undefined | null): ReuseNudge[] {\n\tif (!text) return [];\n\treturn REUSE_NUDGES.filter((n) => n.pattern.test(text));\n}\n\n/**\n * The one enablement gate for the whole autonomous plugin system.\n *\n * Reads settings.json `enablePluginTools` (default false) — the same setting\n * that gates whether the plugin lifecycle tools are attached to the top-level\n * agent in main.ts. Keeping both the tool surface and the reactive nudge behind\n * a single flag is the \"whole autonomous plugin system, off by default\" switch.\n */\nexport function isAutonomousPluginSystemEnabled(cwd: string): boolean {\n\ttry {\n\t\treturn SettingsManager.create(cwd, getAgentDir()).getEnablePluginTools();\n\t} catch {\n\t\t// Fail closed: if settings can't be read, treat the autonomous system as off.\n\t\treturn false;\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AA8gBjF,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACxC;AAYD,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBAyZ/D","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport { resolve } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { type ImageContent, modelsAreEqual } from \"@kolisachint/hoocode-ai\";\nimport { ProcessTerminal, setKeybindings, TUI } from \"@kolisachint/hoocode-tui\";\nimport chalk from \"chalk\";\nimport { type Args, type Mode, parseArgs, printHelp } from \"./cli/args.js\";\nimport { processFileArguments } from \"./cli/file-processor.js\";\nimport { buildInitialMessage } from \"./cli/initial-message.js\";\nimport { listModels } from \"./cli/list-models.js\";\nimport { selectSession } from \"./cli/session-picker.js\";\nimport { ENV_SESSION_DIR, expandTildePath, getAgentDir, VERSION } from \"./config.js\";\nimport { setAgentCliPaths } from \"./core/agent-manifest-paths.js\";\nimport { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from \"./core/agent-session-runtime.js\";\nimport {\n\ttype AgentSessionRuntimeDiagnostic,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n} from \"./core/agent-session-services.js\";\nimport { formatNoModelsAvailableMessage } from \"./core/auth-guidance.js\";\nimport { AuthStorage } from \"./core/auth-storage.js\";\nimport { exportFromFile } from \"./core/export-html/index.js\";\nimport type { ExtensionAPI, ExtensionFactory } from \"./core/extensions/types.js\";\nimport { KeybindingsManager } from \"./core/keybindings.js\";\nimport type { ModelRegistry } from \"./core/model-registry.js\";\nimport { resolveCliModel, resolveModelScope, type ScopedModel } from \"./core/model-resolver.js\";\nimport { restoreStdout, takeOverStdout } from \"./core/output-guard.js\";\nimport type { CreateAgentSessionOptions } from \"./core/sdk.js\";\nimport {\n\tformatMissingSessionCwdPrompt,\n\tgetMissingSessionCwdIssue,\n\tMissingSessionCwdError,\n\ttype SessionCwdIssue,\n} from \"./core/session-cwd.js\";\nimport { SessionManager } from \"./core/session-manager.js\";\nimport { SettingsManager } from \"./core/settings-manager.js\";\nimport {\n\tcanSpawnSubagent,\n\tDEFER_MCP_SCHEMAS_ENV,\n\tDELEGATE_ALLOW_ENV,\n\tNESTED_CONCURRENCY_ENV,\n\tresolveMaxSubagentDepth,\n\tresolveNestedConcurrency,\n\tSUBAGENT_MAX_DEPTH_ENV,\n} from \"./core/subagent-depth.js\";\nimport { printTimings, resetTimings, time } from \"./core/timings.js\";\nimport { createPluginLifecycleToolDefinitions } from \"./core/tools/plugins.js\";\nimport { createProposePluginToolDefinitions } from \"./core/tools/propose-plugin.js\";\nimport {\n\tbuildTaskMainPrompt,\n\tcreateTaskOutputToolDefinition,\n\tcreateTaskToolDefinition,\n} from \"./core/tools/subagent.js\";\nimport { createTodoWriteToolDefinition } from \"./core/tools/todo.js\";\nimport { WARM_SUBAGENTS_ENV } from \"./core/warm-subagent-pool-instance.js\";\n// Static import (not dynamic) so `bun build --compile` statically reaches\n// hoo-core from the compiled entry chain (src/bun/cli.ts -> src/cli.ts ->\n// main.ts) and bundles it into the standalone binary. The node CLI reaches it\n// via this same path through DEFAULT_EXTENSION_FACTORIES below.\nimport hooCore from \"./extensions/core/hoo-core.js\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.js\";\nimport { InteractiveMode, runPrintMode, runRpcMode } from \"./modes/index.js\";\nimport { ExtensionSelectorComponent } from \"./modes/interactive/components/extension-selector.js\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.js\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.js\";\nimport { handleResourcesCommand } from \"./resources-cli.js\";\nimport { isLocalPath } from \"./utils/paths.js\";\n\n/**\n * Read all content from piped stdin.\n * Returns undefined if stdin is a TTY (interactive terminal).\n */\nasync function readPipedStdin(): Promise<string | undefined> {\n\t// If stdin is a TTY, we're running interactively - don't read stdin\n\tif (process.stdin.isTTY) {\n\t\treturn undefined;\n\t}\n\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tprocess.stdin.setEncoding(\"utf8\");\n\t\tprocess.stdin.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tprocess.stdin.on(\"end\", () => {\n\t\t\tresolve(data.trim() || undefined);\n\t\t});\n\t\tprocess.stdin.resume();\n\t});\n}\n\nfunction collectSettingsDiagnostics(\n\tsettingsManager: SettingsManager,\n\tcontext: string,\n): AgentSessionRuntimeDiagnostic[] {\n\treturn settingsManager.drainErrors().map(({ scope, error }) => ({\n\t\ttype: \"warning\",\n\t\tmessage: `(${context}, ${scope} settings) ${error.message}`,\n\t}));\n}\n\nfunction reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void {\n\tfor (const diagnostic of diagnostics) {\n\t\tconst color = diagnostic.type === \"error\" ? chalk.red : diagnostic.type === \"warning\" ? chalk.yellow : chalk.dim;\n\t\tconst prefix = diagnostic.type === \"error\" ? \"Error: \" : diagnostic.type === \"warning\" ? \"Warning: \" : \"\";\n\t\tconsole.error(color(`${prefix}${diagnostic.message}`));\n\t}\n}\n\nfunction isTruthyEnvFlag(value: string | undefined): boolean {\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\ntype AppMode = \"interactive\" | \"print\" | \"json\" | \"rpc\";\n\nfunction resolveAppMode(parsed: Args, stdinIsTTY: boolean): AppMode {\n\tif (parsed.mode === \"rpc\") {\n\t\treturn \"rpc\";\n\t}\n\tif (parsed.mode === \"json\") {\n\t\treturn \"json\";\n\t}\n\tif (parsed.print || !stdinIsTTY) {\n\t\treturn \"print\";\n\t}\n\treturn \"interactive\";\n}\n\nfunction toPrintOutputMode(appMode: AppMode): Exclude<Mode, \"rpc\"> {\n\treturn appMode === \"json\" ? \"json\" : \"text\";\n}\n\nasync function prepareInitialMessage(\n\tparsed: Args,\n\tautoResizeImages: boolean,\n\tstdinContent?: string,\n): Promise<{\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn buildInitialMessage({ parsed, stdinContent });\n\t}\n\n\tconst { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });\n\treturn buildInitialMessage({\n\t\tparsed,\n\t\tfileText: text,\n\t\tfileImages: images,\n\t\tstdinContent,\n\t});\n}\n\n/** Result from resolving a session argument */\ntype ResolvedSession =\n\t| { type: \"path\"; path: string } // Direct file path\n\t| { type: \"local\"; path: string } // Found in current project\n\t| { type: \"global\"; path: string; cwd: string } // Found in different project\n\t| { type: \"not_found\"; arg: string }; // Not found anywhere\n\n/**\n * Resolve a session argument to a file path.\n * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.\n */\nasync function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {\n\t// If it looks like a file path, use as-is\n\tif (sessionArg.includes(\"/\") || sessionArg.includes(\"\\\\\") || sessionArg.endsWith(\".jsonl\")) {\n\t\treturn { type: \"path\", path: sessionArg };\n\t}\n\n\t// Try to match as session ID in current project first\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatches = localSessions.filter((s) => s.id.startsWith(sessionArg));\n\n\tif (localMatches.length >= 1) {\n\t\treturn { type: \"local\", path: localMatches[0].path };\n\t}\n\n\t// Try global search across all projects\n\tconst allSessions = await SessionManager.listAll();\n\tconst globalMatches = allSessions.filter((s) => s.id.startsWith(sessionArg));\n\n\tif (globalMatches.length >= 1) {\n\t\tconst match = globalMatches[0];\n\t\treturn { type: \"global\", path: match.path, cwd: match.cwd };\n\t}\n\n\t// Not found anywhere\n\treturn { type: \"not_found\", arg: sessionArg };\n}\n\n/** Prompt user for yes/no confirmation */\nasync function promptConfirm(message: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\trl.question(`${message} [y/N] `, (answer) => {\n\t\t\trl.close();\n\t\t\tresolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n\t\t});\n\t});\n}\n\nfunction validateForkFlags(parsed: Args): void {\n\tif (!parsed.fork) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t\tparsed.noSession ? \"--no-session\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.forkFrom(sourcePath, cwd, sessionDir);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nasync function createSessionManager(\n\tparsed: Args,\n\tcwd: string,\n\tsessionDir: string | undefined,\n\tsettingsManager: SettingsManager,\n): Promise<SessionManager> {\n\tif (parsed.noSession) {\n\t\treturn SessionManager.inMemory();\n\t}\n\n\tif (parsed.fork) {\n\t\tconst resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\tcase \"global\":\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.session) {\n\t\tconst resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\t\treturn SessionManager.open(resolved.path, sessionDir);\n\n\t\t\tcase \"global\": {\n\t\t\t\tconsole.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));\n\t\t\t\tconst shouldFork = await promptConfirm(\"Fork this session into current directory?\");\n\t\t\t\tif (!shouldFork) {\n\t\t\t\t\tconsole.log(chalk.dim(\"Aborted.\"));\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\t\t\t}\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.resume) {\n\t\tinitTheme(settingsManager.getTheme(), true);\n\t\ttry {\n\t\t\tconst selectedPath = await selectSession(\n\t\t\t\t(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),\n\t\t\t\tSessionManager.listAll,\n\t\t\t);\n\t\t\tif (!selectedPath) {\n\t\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\treturn SessionManager.open(selectedPath, sessionDir);\n\t\t} finally {\n\t\t\tstopThemeWatcher();\n\t\t}\n\t}\n\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd, sessionDir);\n\t}\n\n\treturn SessionManager.create(cwd, sessionDir);\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\thasExistingSession: boolean,\n\tmodelRegistry: ModelRegistry,\n\tsettingsManager: SettingsManager,\n): {\n\toptions: CreateAgentSessionOptions;\n\tcliThinkingFromModel: boolean;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n} {\n\tconst options: CreateAgentSessionOptions = {};\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tlet cliThinkingFromModel = false;\n\n\t// Model from CLI\n\t// - supports --provider <name> --model <pattern>\n\t// - supports --model <provider>/<pattern>\n\tif (parsed.model) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider: parsed.provider,\n\t\t\tcliModel: parsed.model,\n\t\t\tmodelRegistry,\n\t\t});\n\t\tif (resolved.warning) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: resolved.warning });\n\t\t}\n\t\tif (resolved.error) {\n\t\t\tdiagnostics.push({ type: \"error\", message: resolved.error });\n\t\t}\n\t\tif (resolved.model) {\n\t\t\toptions.model = resolved.model;\n\t\t\t// Allow \"--model <pattern>:<thinking>\" as a shorthand.\n\t\t\t// Explicit --thinking still takes precedence (applied later).\n\t\t\tif (!parsed.thinking && resolved.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = resolved.thinkingLevel;\n\t\t\t\tcliThinkingFromModel = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!options.model && scopedModels.length > 0 && !hasExistingSession) {\n\t\t// Check if saved default is in scoped models - use it if so, otherwise first scoped model\n\t\tconst savedProvider = settingsManager.getDefaultProvider();\n\t\tconst savedModelId = settingsManager.getDefaultModel();\n\t\tconst savedModel = savedProvider && savedModelId ? modelRegistry.find(savedProvider, savedModelId) : undefined;\n\t\tconst savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;\n\n\t\tif (savedInScope) {\n\t\t\toptions.model = savedInScope.model;\n\t\t\t// Use thinking level from scoped model config if explicitly set\n\t\t\tif (!parsed.thinking && savedInScope.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = savedInScope.thinkingLevel;\n\t\t\t}\n\t\t} else {\n\t\t\toptions.model = scopedModels[0].model;\n\t\t\t// Use thinking level from first scoped model if explicitly set\n\t\t\tif (!parsed.thinking && scopedModels[0].thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Thinking level from CLI (takes precedence over scoped model thinking levels set above)\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\t// Keep thinking level undefined when not explicitly set in the model pattern.\n\t// Undefined means \"inherit current session thinking level\" during cycling.\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels.map((sm) => ({\n\t\t\tmodel: sm.model,\n\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t}));\n\t}\n\n\t// API key from CLI - set in authStorage\n\t// (handled by caller before createAgentSession)\n\n\t// Tools\n\tif (parsed.noTools) {\n\t\toptions.noTools = \"all\";\n\t} else if (parsed.noBuiltinTools) {\n\t\toptions.noTools = \"builtin\";\n\t}\n\tif (parsed.tools) {\n\t\toptions.tools = [...parsed.tools];\n\t}\n\tif (parsed.disallowedTools) {\n\t\toptions.disallowedTools = [...parsed.disallowedTools];\n\t}\n\t// Web tools (webfetch + websearch): opt-in via --enable-webtools flag or the\n\t// enableWebTools setting. They are registered as base tools but inactive by\n\t// default; this adds them to the default active set.\n\tif (parsed.enableWebTools ?? settingsManager.getEnableWebTools()) {\n\t\toptions.enableWebTools = true;\n\t}\n\t// Browser tools (browser_run + browser_continue): opt-in via --enable-browsertools\n\t// flag or the enableBrowserTools setting. Registered as base tools but inactive by\n\t// default; this adds them to the default active set.\n\tif (parsed.enableBrowserTools ?? settingsManager.getEnableBrowserTools()) {\n\t\toptions.enableBrowserTools = true;\n\t}\n\t// Live preview for browser_run: defaults the streamed viewer on and auto-opens\n\t// it. Applied as a runtime settings override so the session's tool factory reads\n\t// it via the shared settingsManager (no extra option plumbing).\n\tif (parsed.enableBrowserLivePreview) {\n\t\tsettingsManager.applyOverrides({ enableBrowserLivePreview: true });\n\t}\n\t// Document tools (DocRead/DocEdit/DocWrite + the DocScan/DocGrep/DocPeek\n\t// discovery loop): opt-in via --enable-filetools flag or the enableFileTools\n\t// setting. Registered as base tools but inactive by default; this adds them to\n\t// the default active set.\n\tif (parsed.enableFileTools ?? settingsManager.getEnableFileTools()) {\n\t\toptions.enableFileTools = true;\n\t\t// DocRead/DocEdit/DocWrite drive a lossless id-based extract -> patch ->\n\t\t// reconstruct flow that depends on precise, well-formed tool calls. Models\n\t\t// that are weak at tool calling tend to mangle the id-based patches and\n\t\t// corrupt documents, so surface a one-time heads-up when these are enabled.\n\t\tdiagnostics.push({\n\t\t\ttype: \"warning\",\n\t\t\tmessage:\n\t\t\t\t\"Document tools (DocRead/DocEdit/DocWrite) are enabled. They require precise, id-based patches; \" +\n\t\t\t\t\"use a model that is strong at tool calling, or these edits can corrupt files.\",\n\t\t});\n\t}\n\n\t// Optional Task (subagent) tool: opt-in via --enable-subagents flag or the enableSubagent setting.\n\t// Registered as a custom tool; respects --tools/--no-tools allowlists like any other tool.\n\t//\n\t// Nesting is bounded by the tree-wide cap (maxSubagentDepth, default 1). The root\n\t// seeds the cap into the environment so every descendant agrees on one value; the\n\t// Task tool is registered only while this process's depth is below that cap. At the\n\t// default cap this reproduces the original guard exactly: subagents (depth >= 1) get\n\t// no Task tool and cannot recursively dispatch.\n\tconst isSubagentChild = parsed.taskId !== undefined;\n\tif (process.env[SUBAGENT_MAX_DEPTH_ENV] === undefined) {\n\t\t// The root seeds the tree-wide cap; the --max-subagent-depth flag overrides the\n\t\t// setting. resolveMaxSubagentDepth clamps it to the supported range so the seeded\n\t\t// env (and everything that inherits it) carries a sane value. Descendants inherit\n\t\t// it via the environment (env already set => keep it).\n\t\tprocess.env[SUBAGENT_MAX_DEPTH_ENV] = String(\n\t\t\tresolveMaxSubagentDepth(parsed.maxSubagentDepth ?? settingsManager.getMaxSubagentDepth()),\n\t\t);\n\t}\n\tif (process.env[NESTED_CONCURRENCY_ENV] === undefined) {\n\t\t// Seed the nested-pool concurrency from settings so descendants agree on one value.\n\t\tprocess.env[NESTED_CONCURRENCY_ENV] = String(\n\t\t\tresolveNestedConcurrency(settingsManager.getNestedSubagentConcurrency()),\n\t\t);\n\t}\n\t// Scoped delegation: --delegate-allow is the authoritative restriction for this\n\t// process. Set it from the flag, or clear any inherited value so a restricted\n\t// parent's scope never leaks into a child that wasn't given its own.\n\tif (parsed.delegateAllow && parsed.delegateAllow.length > 0) {\n\t\tprocess.env[DELEGATE_ALLOW_ENV] = parsed.delegateAllow.join(\",\");\n\t} else {\n\t\tdelete process.env[DELEGATE_ALLOW_ENV];\n\t}\n\tif (canSpawnSubagent() && (parsed.subagent ?? settingsManager.getEnableSubagent())) {\n\t\toptions.customTools = [\n\t\t\t...(options.customTools ?? []),\n\t\t\tcreateTaskToolDefinition(),\n\t\t\tcreateTaskOutputToolDefinition(),\n\t\t];\n\t\t// Warm subagents (experimental): dispatch eligible foreground subagents on\n\t\t// reused RPC workers to skip the cold-boot. Root-only — the Task tool exists\n\t\t// only where canSpawnSubagent holds and never inside a spawned child — and\n\t\t// carried via env so the Task tool reads it without threading a setting.\n\t\tif (!isSubagentChild && (parsed.warmSubagents ?? settingsManager.getWarmSubagents())) {\n\t\t\tprocess.env[WARM_SUBAGENTS_ENV] = \"1\";\n\t\t}\n\t}\n\n\t// Optional TodoWrite tool: opt-in via --enable-todowrite flag or the\n\t// enableTodoWrite setting. Never registered inside a spawned subagent child —\n\t// its todos would otherwise leak into the parent's \"main\" task group in the pane.\n\tif (!isSubagentChild && (parsed.todoWrite ?? settingsManager.getEnableTodoWrite())) {\n\t\toptions.customTools = [...(options.customTools ?? []), createTodoWriteToolDefinition()];\n\t}\n\n\t// Deferred MCP tool schemas (default on; disable via deferMcpSchemas=false): set\n\t// for the top-level agent only. Subagent\n\t// children clear this env (see subagent-pool) so a child that needs MCP resolves\n\t// its allowlisted tools eagerly at dispatch.\n\tif (!isSubagentChild && settingsManager.getDeferMcpSchemas()) {\n\t\tprocess.env[DEFER_MCP_SCHEMAS_ENV] = \"1\";\n\t}\n\n\t// Plugin lifecycle tools (SearchPlugins, InstallPlugin, ...). Top-level agent\n\t// only: these are capability-acquisition tools and must never be available to\n\t// a spawned subagent child (privilege-amplification guardrail, spec §3).\n\tif (!isSubagentChild && settingsManager.getEnablePluginTools()) {\n\t\toptions.customTools = [\n\t\t\t...(options.customTools ?? []),\n\t\t\t...createPluginLifecycleToolDefinitions(),\n\t\t\t...createProposePluginToolDefinitions(),\n\t\t];\n\t}\n\n\treturn { options, cliThinkingFromModel, diagnostics };\n}\n\nfunction resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {\n\treturn paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));\n}\n\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n): Promise<string | undefined> {\n\tinitTheme(settingsManager.getTheme());\n\tsetKeybindings(KeybindingsManager.create());\n\n\treturn new Promise((resolve) => {\n\t\tconst ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());\n\t\tui.setClearOnShrink(settingsManager.getClearOnShrink());\n\n\t\tlet settled = false;\n\t\tconst finish = (result: string | undefined) => {\n\t\t\tif (settled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tui.stop();\n\t\t\tresolve(result);\n\t\t};\n\n\t\tconst selector = new ExtensionSelectorComponent(\n\t\t\tformatMissingSessionCwdPrompt(issue),\n\t\t\t[\"Continue\", \"Cancel\"],\n\t\t\t(option) => finish(option === \"Continue\" ? issue.fallbackCwd : undefined),\n\t\t\t() => finish(undefined),\n\t\t\t{ tui: ui },\n\t\t);\n\t\tui.addChild(selector);\n\t\tui.setFocus(selector);\n\t\tui.start();\n\t});\n}\n\nexport interface MainOptions {\n\textensionFactories?: ExtensionFactory[];\n}\n\n/**\n * Built-in extension factories loaded when the caller supplies none. This is\n * the single source of truth for the app's built-ins (hoo-core: /loop, /plugin,\n * /mode, /cost, scaffold commands, the MCP loader + remote-MCP/OAuth flow).\n * Both entry points — the node CLI (bin/hoocode.js) and the compiled binary\n * (src/cli.ts) — call main() without factories and inherit this default.\n * Downstream embedders that pass their own extensionFactories override it.\n */\nconst DEFAULT_EXTENSION_FACTORIES: ExtensionFactory[] = [hooCore];\n\nexport async function main(args: string[], options?: MainOptions) {\n\tresetTimings();\n\tconst offlineMode = args.includes(\"--offline\") || isTruthyEnvFlag(process.env.HOOCODE_OFFLINE);\n\tif (offlineMode) {\n\t\tprocess.env.HOOCODE_OFFLINE = \"1\";\n\t\tprocess.env.HOOCODE_SKIP_VERSION_CHECK = \"1\";\n\t}\n\n\tif (await handlePackageCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleConfigCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleResourcesCommand(args)) {\n\t\treturn;\n\t}\n\n\tconst parsed = parseArgs(args);\n\tif (parsed.diagnostics.length > 0) {\n\t\tfor (const d of parsed.diagnostics) {\n\t\t\tconst color = d.type === \"error\" ? chalk.red : chalk.yellow;\n\t\t\tconsole.error(color(`${d.type === \"error\" ? \"Error\" : \"Warning\"}: ${d.message}`));\n\t\t}\n\t\tif (parsed.diagnostics.some((d) => d.type === \"error\")) {\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"parseArgs\");\n\tlet appMode = resolveAppMode(parsed, process.stdin.isTTY);\n\tconst shouldTakeOverStdout = appMode !== \"interactive\";\n\tif (shouldTakeOverStdout) {\n\t\ttakeOverStdout();\n\t}\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.export) {\n\t\tlet result: string;\n\t\ttry {\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tresult = await exportFromFile(parsed.export, outputPath);\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconsole.log(`Exported to: ${result}`);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tvalidateForkFlags(parsed);\n\n\t// Run migrations (pass cwd for project-local migrations)\n\tconst { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());\n\ttime(\"runMigrations\");\n\n\tconst cwd = process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst startupSettingsManager = SettingsManager.create(cwd, agentDir);\n\treportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, \"startup session lookup\"));\n\n\t// Decide the final runtime cwd before creating cwd-bound runtime services.\n\t// --session and --resume may select a session from another project, so project-local\n\t// settings, resources, provider registrations, and models must be resolved only after\n\t// the target session cwd is known. The startup-cwd settings manager is used only for\n\t// sessionDir lookup during session selection.\n\tconst envSessionDir = process.env[ENV_SESSION_DIR];\n\tconst sessionDir =\n\t\tparsed.sessionDir ??\n\t\t(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??\n\t\tstartupSettingsManager.getSessionDir();\n\tlet sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);\n\t\t\tif (!selectedCwd) {\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tsessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);\n\t\t} else {\n\t\t\tconsole.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"createSessionManager\");\n\n\tconst resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);\n\tconst resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);\n\tconst resolvedAgentPaths = resolveCliPaths(cwd, parsed.agents);\n\tconst resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);\n\tconst resolvedSlashCommandPaths = resolveCliPaths(cwd, parsed.slashCommands);\n\tconst resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);\n\n\t// Populate the module-level CLI agent store before any session is created.\n\tsetAgentCliPaths(resolvedAgentPaths ?? []);\n\n\t// Synthetic factory: feed CLI --mode-path values into the extension runtime\n\t// so hoo-core (and any other extension that reads pi.getModeSearchPaths)\n\t// sees them alongside extension-registered dirs.\n\tconst cliModePaths = parsed.modePaths ?? [];\n\tconst cliResourcePathFactories: ExtensionFactory[] =\n\t\tcliModePaths.length === 0\n\t\t\t? []\n\t\t\t: [\n\t\t\t\t\tObject.assign(\n\t\t\t\t\t\t(pi: ExtensionAPI) => {\n\t\t\t\t\t\t\tfor (const p of cliModePaths) pi.addModeSearchPath(p);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ internal: true },\n\t\t\t\t\t),\n\t\t\t\t];\n\t// `??` (not `[...a, ...b]`) so an explicit list — from bin/hoocode.js or a\n\t// downstream embedder — fully replaces the default and hoo-core is never\n\t// registered twice.\n\tconst allExtensionFactories: ExtensionFactory[] = [\n\t\t...cliResourcePathFactories,\n\t\t...(options?.extensionFactories ?? DEFAULT_EXTENSION_FACTORIES),\n\t];\n\tconst authStorage = AuthStorage.create();\n\t// A spawned subagent (run with a task id) is a single-shot, non-interactive\n\t// process: it renders no TUI and its prompt is a plain task, never a slash\n\t// command. Themes, slash commands, and prompt templates are dead weight in that\n\t// path, so skip loading them to trim the child's cold-boot cost. Skills, context\n\t// files, and extensions (which carry the core tools) are kept — they affect the\n\t// subagent's actual work.\n\tconst isSubagentBoot = parsed.taskId !== undefined;\n\tconst createRuntime: CreateAgentSessionRuntimeFactory = async ({\n\t\tcwd,\n\t\tagentDir,\n\t\tsessionManager,\n\t\tsessionStartEvent,\n\t}) => {\n\t\tconst services = await createAgentSessionServices({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tauthStorage,\n\t\t\textensionFlagValues: parsed.unknownFlags,\n\t\t\tresourceLoaderOptions: {\n\t\t\t\tadditionalExtensionPaths: resolvedExtensionPaths,\n\t\t\t\tadditionalSkillPaths: resolvedSkillPaths,\n\t\t\t\tadditionalPromptTemplatePaths: resolvedPromptTemplatePaths,\n\t\t\t\tadditionalSlashCommandPaths: resolvedSlashCommandPaths,\n\t\t\t\tadditionalThemePaths: resolvedThemePaths,\n\t\t\t\tnoExtensions: parsed.noExtensions,\n\t\t\t\tnoSkills: parsed.noSkills,\n\t\t\t\tnoPromptTemplates: parsed.noPromptTemplates || isSubagentBoot,\n\t\t\t\tnoSlashCommands: parsed.noSlashCommands || isSubagentBoot,\n\t\t\t\tnoThemes: parsed.noThemes || isSubagentBoot,\n\t\t\t\tnoContextFiles: parsed.noContextFiles,\n\t\t\t\tsystemPrompt: parsed.systemPrompt,\n\t\t\t\textensionFactories: allExtensionFactories,\n\t\t\t},\n\t\t});\n\t\tconst { settingsManager, modelRegistry, resourceLoader } = services;\n\t\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [\n\t\t\t...services.diagnostics,\n\t\t\t...collectSettingsDiagnostics(settingsManager, \"runtime creation\"),\n\t\t\t...resourceLoader.getExtensions().errors.map(({ path, error }) => ({\n\t\t\t\ttype: \"error\" as const,\n\t\t\t\tmessage: `Failed to load extension \"${path}\": ${error}`,\n\t\t\t})),\n\t\t];\n\n\t\t// When subagent tooling is enabled, append the main session subagent instructions.\n\t\tif (parsed.subagent ?? settingsManager.getEnableSubagent()) {\n\t\t\tresourceLoader.addAppendSystemPrompt(buildTaskMainPrompt());\n\t\t}\n\n\t\tconst modelPatterns = parsed.models ?? settingsManager.getEnabledModels();\n\t\tconst scopedModels =\n\t\t\tmodelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRegistry) : [];\n\t\tconst {\n\t\t\toptions: sessionOptions,\n\t\t\tcliThinkingFromModel,\n\t\t\tdiagnostics: sessionOptionDiagnostics,\n\t\t} = buildSessionOptions(\n\t\t\tparsed,\n\t\t\tscopedModels,\n\t\t\tsessionManager.buildSessionContext().messages.length > 0,\n\t\t\tmodelRegistry,\n\t\t\tsettingsManager,\n\t\t);\n\t\tdiagnostics.push(...sessionOptionDiagnostics);\n\n\t\tif (parsed.apiKey) {\n\t\t\tif (!sessionOptions.model) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: \"--api-key requires a model to be specified via --model, --provider/--model, or --models\",\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tauthStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);\n\t\t\t}\n\t\t}\n\n\t\tconst created = await createAgentSessionFromServices({\n\t\t\tservices,\n\t\t\tsessionManager,\n\t\t\tsessionStartEvent,\n\t\t\tmodel: sessionOptions.model,\n\t\t\tthinkingLevel: sessionOptions.thinkingLevel,\n\t\t\tscopedModels: sessionOptions.scopedModels,\n\t\t\ttools: sessionOptions.tools,\n\t\t\tnoTools: sessionOptions.noTools,\n\t\t\tcustomTools: sessionOptions.customTools,\n\t\t\tenableWebTools: sessionOptions.enableWebTools,\n\t\t\tenableBrowserTools: sessionOptions.enableBrowserTools,\n\t\t\tenableFileTools: sessionOptions.enableFileTools,\n\t\t});\n\t\tconst cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;\n\t\tif (created.session.model && cliThinkingOverride) {\n\t\t\tcreated.session.setThinkingLevel(created.session.thinkingLevel);\n\t\t}\n\n\t\treturn {\n\t\t\t...created,\n\t\t\tservices,\n\t\t\tdiagnostics,\n\t\t};\n\t};\n\ttime(\"createRuntime\");\n\tconst runtime = await createAgentSessionRuntime(createRuntime, {\n\t\tcwd: sessionManager.getCwd(),\n\t\tagentDir,\n\t\tsessionManager,\n\t});\n\tconst { services, session, modelFallbackMessage } = runtime;\n\tconst { settingsManager, modelRegistry, resourceLoader } = services;\n\n\tif (parsed.help) {\n\t\tconst extensionFlags = resourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.flatMap((extension) => Array.from(extension.flags.values()));\n\t\tprintHelp(extensionFlags);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRegistry, searchPattern);\n\t\tprocess.exit(0);\n\t}\n\n\t// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC\n\tlet stdinContent: string | undefined;\n\tif (appMode !== \"rpc\") {\n\t\tstdinContent = await readPipedStdin();\n\t\tif (stdinContent !== undefined && appMode === \"interactive\") {\n\t\t\tappMode = \"print\";\n\t\t}\n\t}\n\ttime(\"readPipedStdin\");\n\n\tconst { initialMessage, initialImages } = await prepareInitialMessage(\n\t\tparsed,\n\t\tsettingsManager.getImageAutoResize(),\n\t\tstdinContent,\n\t);\n\ttime(\"prepareInitialMessage\");\n\tinitTheme(settingsManager.getTheme(), appMode === \"interactive\");\n\ttime(\"initTheme\");\n\n\t// Show deprecation warnings in interactive mode\n\tif (appMode === \"interactive\" && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\tconst scopedModels = [...session.scopedModels];\n\ttime(\"resolveModelScope\");\n\treportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tprocess.exit(1);\n\t}\n\ttime(\"createAgentSession\");\n\n\tif (appMode !== \"interactive\" && !session.model) {\n\t\tconsole.error(chalk.red(formatNoModelsAvailableMessage()));\n\t\tprocess.exit(1);\n\t}\n\n\tconst startupBenchmark = isTruthyEnvFlag(process.env.HOOCODE_STARTUP_BENCHMARK);\n\tif (startupBenchmark && appMode !== \"interactive\") {\n\t\tconsole.error(chalk.red(\"Error: HOOCODE_STARTUP_BENCHMARK only supports interactive mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tif (appMode === \"rpc\") {\n\t\tprintTimings();\n\t\tawait runRpcMode(runtime);\n\t} else if (appMode === \"interactive\") {\n\t\tif (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) {\n\t\t\tconst modelList = scopedModels\n\t\t\t\t.map((sm) => {\n\t\t\t\t\tconst thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : \"\";\n\t\t\t\t\treturn `${sm.model.id}${thinkingStr}`;\n\t\t\t\t})\n\t\t\t\t.join(\", \");\n\t\t\tconsole.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray(\"(Ctrl+P to cycle)\")}`));\n\t\t}\n\n\t\tconst interactiveMode = new InteractiveMode(runtime, {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages: parsed.messages,\n\t\t\tverbose: parsed.verbose,\n\t\t});\n\n\t\t// Optional hooteams bridge. `--team auto` discovers a config, spawns a\n\t\t// local hooteams child on a free port, and proceeds as if its URL had\n\t\t// been passed; the child is reaped on exit (clean or signal) via a\n\t\t// process \"exit\" hook plus the explicit stop below.\n\t\tlet autoTeam: { url: string; stop(): Promise<void> } | undefined;\n\t\tlet teamUrl = parsed.team;\n\t\tif (teamUrl === \"auto\") {\n\t\t\tconst { startAutoTeam } = await import(\"./core/team-auto.js\");\n\t\t\ttry {\n\t\t\t\tautoTeam = await startAutoTeam(process.cwd(), { log: (message) => console.log(chalk.dim(message)) });\n\t\t\t\tteamUrl = autoTeam.url;\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(chalk.red(error instanceof Error ? error.message : String(error)));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\n\t\t// Team mirror + client. Fire-and-forget: connect failures and drops warn\n\t\t// in the background and never block the main agent. Warnings go through\n\t\t// the chat, not console.error: a raw stderr write while the TUI owns the\n\t\t// screen scribbles over the render and can leave the editor looking\n\t\t// frozen. The same connection powers team focus / nudge / attach.\n\t\tlet teamView: { stop(): void } | undefined;\n\t\tif (teamUrl) {\n\t\t\tconst { connectTeamView } = await import(\"./core/team-view.js\");\n\t\t\tconst teamClient = connectTeamView(teamUrl, {\n\t\t\t\twarn: (message) => interactiveMode.showWarning(message),\n\t\t\t});\n\t\t\tteamView = teamClient;\n\t\t\tinteractiveMode.attachTeamClient(teamClient);\n\t\t}\n\t\tif (startupBenchmark) {\n\t\t\tawait interactiveMode.init();\n\t\t\ttime(\"interactiveMode.init\");\n\t\t\tprintTimings();\n\t\t\tteamView?.stop();\n\t\t\tawait autoTeam?.stop();\n\t\t\tinteractiveMode.stop();\n\t\t\tstopThemeWatcher();\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tprintTimings();\n\t\ttry {\n\t\t\tawait interactiveMode.run();\n\t\t} finally {\n\t\t\tteamView?.stop();\n\t\t\tawait autoTeam?.stop();\n\t\t}\n\t} else {\n\t\tprintTimings();\n\t\tconst exitCode = await runPrintMode(runtime, {\n\t\t\tmode: toPrintOutputMode(appMode),\n\t\t\tmessages: parsed.messages,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\ttaskId: parsed.taskId,\n\t\t\tmaxTurns: parsed.maxTurns,\n\t\t});\n\t\tstopThemeWatcher();\n\t\trestoreStdout();\n\t\tif (exitCode !== 0) {\n\t\t\tprocess.exitCode = exitCode;\n\t\t}\n\t\t// Spawned subagents (run with a task id) must exit promptly once their work is\n\t\t// done and result.json is written. The child's runtime can leave handles open\n\t\t// (e.g. MCP client connections) that keep the event loop alive, so a natural\n\t\t// exit may never happen. When that occurs the parent lifeguard SIGKILLs the idle\n\t\t// child at the 60s heartbeat threshold and misreports an already-completed task\n\t\t// as \"stalled\". Force a clean exit after draining output to avoid that false stall.\n\t\tconst ranAsSubagent = typeof parsed.taskId === \"string\" && parsed.taskId.length > 0;\n\t\tif (ranAsSubagent) {\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tprocess.exit(exitCode);\n\t\t}\n\t\treturn;\n\t}\n}\n"]}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAihBjF,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACxC;AAYD,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBAyZ/D","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport { resolve } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { type ImageContent, modelsAreEqual } from \"@kolisachint/hoocode-ai\";\nimport { ProcessTerminal, setKeybindings, TUI } from \"@kolisachint/hoocode-tui\";\nimport chalk from \"chalk\";\nimport { type Args, type Mode, parseArgs, printHelp } from \"./cli/args.js\";\nimport { processFileArguments } from \"./cli/file-processor.js\";\nimport { buildInitialMessage } from \"./cli/initial-message.js\";\nimport { listModels } from \"./cli/list-models.js\";\nimport { selectSession } from \"./cli/session-picker.js\";\nimport { ENV_SESSION_DIR, expandTildePath, getAgentDir, VERSION } from \"./config.js\";\nimport { setAgentCliPaths } from \"./core/agent-manifest-paths.js\";\nimport { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from \"./core/agent-session-runtime.js\";\nimport {\n\ttype AgentSessionRuntimeDiagnostic,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n} from \"./core/agent-session-services.js\";\nimport { formatNoModelsAvailableMessage } from \"./core/auth-guidance.js\";\nimport { AuthStorage } from \"./core/auth-storage.js\";\nimport { exportFromFile } from \"./core/export-html/index.js\";\nimport type { ExtensionAPI, ExtensionFactory } from \"./core/extensions/types.js\";\nimport { KeybindingsManager } from \"./core/keybindings.js\";\nimport type { ModelRegistry } from \"./core/model-registry.js\";\nimport { resolveCliModel, resolveModelScope, type ScopedModel } from \"./core/model-resolver.js\";\nimport { restoreStdout, takeOverStdout } from \"./core/output-guard.js\";\nimport type { CreateAgentSessionOptions } from \"./core/sdk.js\";\nimport {\n\tformatMissingSessionCwdPrompt,\n\tgetMissingSessionCwdIssue,\n\tMissingSessionCwdError,\n\ttype SessionCwdIssue,\n} from \"./core/session-cwd.js\";\nimport { SessionManager } from \"./core/session-manager.js\";\nimport { SettingsManager } from \"./core/settings-manager.js\";\nimport {\n\tcanSpawnSubagent,\n\tDEFER_MCP_SCHEMAS_ENV,\n\tDELEGATE_ALLOW_ENV,\n\tNESTED_CONCURRENCY_ENV,\n\tresolveMaxSubagentDepth,\n\tresolveNestedConcurrency,\n\tSUBAGENT_MAX_DEPTH_ENV,\n} from \"./core/subagent-depth.js\";\nimport { printTimings, resetTimings, time } from \"./core/timings.js\";\nimport { createPluginLifecycleToolDefinitions } from \"./core/tools/plugins.js\";\nimport { createProposePluginToolDefinitions } from \"./core/tools/propose-plugin.js\";\nimport {\n\tbuildTaskMainPrompt,\n\tcreateTaskOutputToolDefinition,\n\tcreateTaskToolDefinition,\n} from \"./core/tools/subagent.js\";\nimport { createTodoWriteToolDefinition } from \"./core/tools/todo.js\";\nimport { WARM_SUBAGENTS_ENV } from \"./core/warm-subagent-pool-instance.js\";\n// Static import (not dynamic) so `bun build --compile` statically reaches\n// hoo-core from the compiled entry chain (src/bun/cli.ts -> src/cli.ts ->\n// main.ts) and bundles it into the standalone binary. The node CLI reaches it\n// via this same path through DEFAULT_EXTENSION_FACTORIES below.\nimport hooCore from \"./extensions/core/hoo-core.js\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.js\";\nimport { InteractiveMode, runPrintMode, runRpcMode } from \"./modes/index.js\";\nimport { ExtensionSelectorComponent } from \"./modes/interactive/components/extension-selector.js\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.js\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.js\";\nimport { handleResourcesCommand } from \"./resources-cli.js\";\nimport { isLocalPath } from \"./utils/paths.js\";\n\n/**\n * Read all content from piped stdin.\n * Returns undefined if stdin is a TTY (interactive terminal).\n */\nasync function readPipedStdin(): Promise<string | undefined> {\n\t// If stdin is a TTY, we're running interactively - don't read stdin\n\tif (process.stdin.isTTY) {\n\t\treturn undefined;\n\t}\n\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tprocess.stdin.setEncoding(\"utf8\");\n\t\tprocess.stdin.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tprocess.stdin.on(\"end\", () => {\n\t\t\tresolve(data.trim() || undefined);\n\t\t});\n\t\tprocess.stdin.resume();\n\t});\n}\n\nfunction collectSettingsDiagnostics(\n\tsettingsManager: SettingsManager,\n\tcontext: string,\n): AgentSessionRuntimeDiagnostic[] {\n\treturn settingsManager.drainErrors().map(({ scope, error }) => ({\n\t\ttype: \"warning\",\n\t\tmessage: `(${context}, ${scope} settings) ${error.message}`,\n\t}));\n}\n\nfunction reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void {\n\tfor (const diagnostic of diagnostics) {\n\t\tconst color = diagnostic.type === \"error\" ? chalk.red : diagnostic.type === \"warning\" ? chalk.yellow : chalk.dim;\n\t\tconst prefix = diagnostic.type === \"error\" ? \"Error: \" : diagnostic.type === \"warning\" ? \"Warning: \" : \"\";\n\t\tconsole.error(color(`${prefix}${diagnostic.message}`));\n\t}\n}\n\nfunction isTruthyEnvFlag(value: string | undefined): boolean {\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\ntype AppMode = \"interactive\" | \"print\" | \"json\" | \"rpc\";\n\nfunction resolveAppMode(parsed: Args, stdinIsTTY: boolean): AppMode {\n\tif (parsed.mode === \"rpc\") {\n\t\treturn \"rpc\";\n\t}\n\tif (parsed.mode === \"json\") {\n\t\treturn \"json\";\n\t}\n\tif (parsed.print || !stdinIsTTY) {\n\t\treturn \"print\";\n\t}\n\treturn \"interactive\";\n}\n\nfunction toPrintOutputMode(appMode: AppMode): Exclude<Mode, \"rpc\"> {\n\treturn appMode === \"json\" ? \"json\" : \"text\";\n}\n\nasync function prepareInitialMessage(\n\tparsed: Args,\n\tautoResizeImages: boolean,\n\tstdinContent?: string,\n): Promise<{\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn buildInitialMessage({ parsed, stdinContent });\n\t}\n\n\tconst { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });\n\treturn buildInitialMessage({\n\t\tparsed,\n\t\tfileText: text,\n\t\tfileImages: images,\n\t\tstdinContent,\n\t});\n}\n\n/** Result from resolving a session argument */\ntype ResolvedSession =\n\t| { type: \"path\"; path: string } // Direct file path\n\t| { type: \"local\"; path: string } // Found in current project\n\t| { type: \"global\"; path: string; cwd: string } // Found in different project\n\t| { type: \"not_found\"; arg: string }; // Not found anywhere\n\n/**\n * Resolve a session argument to a file path.\n * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.\n */\nasync function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {\n\t// If it looks like a file path, use as-is\n\tif (sessionArg.includes(\"/\") || sessionArg.includes(\"\\\\\") || sessionArg.endsWith(\".jsonl\")) {\n\t\treturn { type: \"path\", path: sessionArg };\n\t}\n\n\t// Try to match as session ID in current project first\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatches = localSessions.filter((s) => s.id.startsWith(sessionArg));\n\n\tif (localMatches.length >= 1) {\n\t\treturn { type: \"local\", path: localMatches[0].path };\n\t}\n\n\t// Try global search across all projects\n\tconst allSessions = await SessionManager.listAll();\n\tconst globalMatches = allSessions.filter((s) => s.id.startsWith(sessionArg));\n\n\tif (globalMatches.length >= 1) {\n\t\tconst match = globalMatches[0];\n\t\treturn { type: \"global\", path: match.path, cwd: match.cwd };\n\t}\n\n\t// Not found anywhere\n\treturn { type: \"not_found\", arg: sessionArg };\n}\n\n/** Prompt user for yes/no confirmation */\nasync function promptConfirm(message: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\trl.question(`${message} [y/N] `, (answer) => {\n\t\t\trl.close();\n\t\t\tresolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n\t\t});\n\t});\n}\n\nfunction validateForkFlags(parsed: Args): void {\n\tif (!parsed.fork) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t\tparsed.noSession ? \"--no-session\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.forkFrom(sourcePath, cwd, sessionDir);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nasync function createSessionManager(\n\tparsed: Args,\n\tcwd: string,\n\tsessionDir: string | undefined,\n\tsettingsManager: SettingsManager,\n): Promise<SessionManager> {\n\tif (parsed.noSession) {\n\t\treturn SessionManager.inMemory();\n\t}\n\n\tif (parsed.fork) {\n\t\tconst resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\tcase \"global\":\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.session) {\n\t\tconst resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\t\treturn SessionManager.open(resolved.path, sessionDir);\n\n\t\t\tcase \"global\": {\n\t\t\t\tconsole.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));\n\t\t\t\tconst shouldFork = await promptConfirm(\"Fork this session into current directory?\");\n\t\t\t\tif (!shouldFork) {\n\t\t\t\t\tconsole.log(chalk.dim(\"Aborted.\"));\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\t\t\t}\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.resume) {\n\t\tinitTheme(settingsManager.getTheme(), true);\n\t\ttry {\n\t\t\tconst selectedPath = await selectSession(\n\t\t\t\t(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),\n\t\t\t\tSessionManager.listAll,\n\t\t\t);\n\t\t\tif (!selectedPath) {\n\t\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\treturn SessionManager.open(selectedPath, sessionDir);\n\t\t} finally {\n\t\t\tstopThemeWatcher();\n\t\t}\n\t}\n\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd, sessionDir);\n\t}\n\n\treturn SessionManager.create(cwd, sessionDir);\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\thasExistingSession: boolean,\n\tmodelRegistry: ModelRegistry,\n\tsettingsManager: SettingsManager,\n): {\n\toptions: CreateAgentSessionOptions;\n\tcliThinkingFromModel: boolean;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n} {\n\tconst options: CreateAgentSessionOptions = {};\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tlet cliThinkingFromModel = false;\n\n\t// Model from CLI\n\t// - supports --provider <name> --model <pattern>\n\t// - supports --model <provider>/<pattern>\n\tif (parsed.model) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider: parsed.provider,\n\t\t\tcliModel: parsed.model,\n\t\t\tmodelRegistry,\n\t\t});\n\t\tif (resolved.warning) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: resolved.warning });\n\t\t}\n\t\tif (resolved.error) {\n\t\t\tdiagnostics.push({ type: \"error\", message: resolved.error });\n\t\t}\n\t\tif (resolved.model) {\n\t\t\toptions.model = resolved.model;\n\t\t\t// Allow \"--model <pattern>:<thinking>\" as a shorthand.\n\t\t\t// Explicit --thinking still takes precedence (applied later).\n\t\t\tif (!parsed.thinking && resolved.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = resolved.thinkingLevel;\n\t\t\t\tcliThinkingFromModel = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!options.model && scopedModels.length > 0 && !hasExistingSession) {\n\t\t// Check if saved default is in scoped models - use it if so, otherwise first scoped model\n\t\tconst savedProvider = settingsManager.getDefaultProvider();\n\t\tconst savedModelId = settingsManager.getDefaultModel();\n\t\tconst savedModel = savedProvider && savedModelId ? modelRegistry.find(savedProvider, savedModelId) : undefined;\n\t\tconst savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;\n\n\t\tif (savedInScope) {\n\t\t\toptions.model = savedInScope.model;\n\t\t\t// Use thinking level from scoped model config if explicitly set\n\t\t\tif (!parsed.thinking && savedInScope.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = savedInScope.thinkingLevel;\n\t\t\t}\n\t\t} else {\n\t\t\toptions.model = scopedModels[0].model;\n\t\t\t// Use thinking level from first scoped model if explicitly set\n\t\t\tif (!parsed.thinking && scopedModels[0].thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Thinking level from CLI (takes precedence over scoped model thinking levels set above)\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\t// Keep thinking level undefined when not explicitly set in the model pattern.\n\t// Undefined means \"inherit current session thinking level\" during cycling.\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels.map((sm) => ({\n\t\t\tmodel: sm.model,\n\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t}));\n\t}\n\n\t// API key from CLI - set in authStorage\n\t// (handled by caller before createAgentSession)\n\n\t// Tools\n\tif (parsed.noTools) {\n\t\toptions.noTools = \"all\";\n\t} else if (parsed.noBuiltinTools) {\n\t\toptions.noTools = \"builtin\";\n\t}\n\tif (parsed.tools) {\n\t\toptions.tools = [...parsed.tools];\n\t}\n\tif (parsed.disallowedTools) {\n\t\toptions.disallowedTools = [...parsed.disallowedTools];\n\t}\n\t// Web tools (webfetch + websearch): opt-in via --enable-webtools flag or the\n\t// enableWebTools setting. They are registered as base tools but inactive by\n\t// default; this adds them to the default active set.\n\tif (parsed.enableWebTools ?? settingsManager.getEnableWebTools()) {\n\t\toptions.enableWebTools = true;\n\t}\n\t// Browser tools (browser_run + browser_continue): opt-in via --enable-browsertools\n\t// flag or the enableBrowserTools setting. Registered as base tools but inactive by\n\t// default; this adds them to the default active set.\n\tif (parsed.enableBrowserTools ?? settingsManager.getEnableBrowserTools()) {\n\t\toptions.enableBrowserTools = true;\n\t}\n\t// Live preview for browser_run: defaults the streamed viewer on and auto-opens\n\t// it. Applied as a runtime settings override so the session's tool factory reads\n\t// it via the shared settingsManager (no extra option plumbing).\n\tif (parsed.enableBrowserLivePreview) {\n\t\tsettingsManager.applyOverrides({ enableBrowserLivePreview: true });\n\t}\n\t// Document tools (DocRead/DocEdit/DocWrite + the DocScan/DocGrep/DocPeek\n\t// discovery loop): opt-in via --enable-filetools flag or the enableFileTools\n\t// setting. Registered as base tools but inactive by default; this adds them to\n\t// the default active set.\n\tif (parsed.enableFileTools ?? settingsManager.getEnableFileTools()) {\n\t\toptions.enableFileTools = true;\n\t\t// DocRead/DocEdit/DocWrite drive a lossless id-based extract -> patch ->\n\t\t// reconstruct flow that depends on precise, well-formed tool calls. Models\n\t\t// that are weak at tool calling tend to mangle the id-based patches and\n\t\t// corrupt documents, so surface a one-time heads-up when these are enabled.\n\t\tdiagnostics.push({\n\t\t\ttype: \"warning\",\n\t\t\tmessage:\n\t\t\t\t\"Document tools (DocRead/DocEdit/DocWrite) are enabled. They require precise, id-based patches; \" +\n\t\t\t\t\"use a model that is strong at tool calling, or these edits can corrupt files.\",\n\t\t});\n\t}\n\n\t// Optional Task (subagent) tool: opt-in via --enable-subagents flag or the enableSubagent setting.\n\t// Registered as a custom tool; respects --tools/--no-tools allowlists like any other tool.\n\t//\n\t// Nesting is bounded by the tree-wide cap (maxSubagentDepth, default 1). The root\n\t// seeds the cap into the environment so every descendant agrees on one value; the\n\t// Task tool is registered only while this process's depth is below that cap. At the\n\t// default cap this reproduces the original guard exactly: subagents (depth >= 1) get\n\t// no Task tool and cannot recursively dispatch.\n\tconst isSubagentChild = parsed.taskId !== undefined;\n\tif (process.env[SUBAGENT_MAX_DEPTH_ENV] === undefined) {\n\t\t// The root seeds the tree-wide cap; the --max-subagent-depth flag overrides the\n\t\t// setting. resolveMaxSubagentDepth clamps it to the supported range so the seeded\n\t\t// env (and everything that inherits it) carries a sane value. Descendants inherit\n\t\t// it via the environment (env already set => keep it).\n\t\tprocess.env[SUBAGENT_MAX_DEPTH_ENV] = String(\n\t\t\tresolveMaxSubagentDepth(parsed.maxSubagentDepth ?? settingsManager.getMaxSubagentDepth()),\n\t\t);\n\t}\n\tif (process.env[NESTED_CONCURRENCY_ENV] === undefined) {\n\t\t// Seed the nested-pool concurrency from settings so descendants agree on one value.\n\t\tprocess.env[NESTED_CONCURRENCY_ENV] = String(\n\t\t\tresolveNestedConcurrency(settingsManager.getNestedSubagentConcurrency()),\n\t\t);\n\t}\n\t// Scoped delegation: --delegate-allow is the authoritative restriction for this\n\t// process. Set it from the flag, or clear any inherited value so a restricted\n\t// parent's scope never leaks into a child that wasn't given its own.\n\tif (parsed.delegateAllow && parsed.delegateAllow.length > 0) {\n\t\tprocess.env[DELEGATE_ALLOW_ENV] = parsed.delegateAllow.join(\",\");\n\t} else {\n\t\tdelete process.env[DELEGATE_ALLOW_ENV];\n\t}\n\tif (canSpawnSubagent() && (parsed.subagent ?? settingsManager.getEnableSubagent())) {\n\t\toptions.customTools = [\n\t\t\t...(options.customTools ?? []),\n\t\t\tcreateTaskToolDefinition(),\n\t\t\tcreateTaskOutputToolDefinition(),\n\t\t];\n\t\t// Warm subagents (experimental): dispatch eligible foreground subagents on\n\t\t// reused RPC workers to skip the cold-boot. Root-only — the Task tool exists\n\t\t// only where canSpawnSubagent holds and never inside a spawned child — and\n\t\t// carried via env so the Task tool reads it without threading a setting.\n\t\tif (!isSubagentChild && (parsed.warmSubagents ?? settingsManager.getWarmSubagents())) {\n\t\t\tprocess.env[WARM_SUBAGENTS_ENV] = \"1\";\n\t\t}\n\t}\n\n\t// Optional TodoWrite tool: opt-in via --enable-todowrite flag or the\n\t// enableTodoWrite setting. Never registered inside a spawned subagent child —\n\t// its todos would otherwise leak into the parent's \"main\" task group in the pane.\n\tif (!isSubagentChild && (parsed.todoWrite ?? settingsManager.getEnableTodoWrite())) {\n\t\toptions.customTools = [...(options.customTools ?? []), createTodoWriteToolDefinition()];\n\t}\n\n\t// Deferred MCP tool schemas (default on; disable via deferMcpSchemas=false): set\n\t// for the top-level agent only. Subagent\n\t// children clear this env (see subagent-pool) so a child that needs MCP resolves\n\t// its allowlisted tools eagerly at dispatch.\n\tif (!isSubagentChild && settingsManager.getDeferMcpSchemas()) {\n\t\tprocess.env[DEFER_MCP_SCHEMAS_ENV] = \"1\";\n\t}\n\n\t// Plugin lifecycle tools (SearchPlugins, InstallPlugin, ...). Top-level agent\n\t// only: these are capability-acquisition tools and must never be available to\n\t// a spawned subagent child (privilege-amplification guardrail, spec §3).\n\t// `enablePluginTools` is the master switch for the whole autonomous plugin\n\t// system (default off) — it gates both these tools and the runtime reuse\n\t// nudge (see extensions/core/prompt-reactive), so both flip together.\n\tif (!isSubagentChild && settingsManager.getEnablePluginTools()) {\n\t\toptions.customTools = [\n\t\t\t...(options.customTools ?? []),\n\t\t\t...createPluginLifecycleToolDefinitions(),\n\t\t\t...createProposePluginToolDefinitions(),\n\t\t];\n\t}\n\n\treturn { options, cliThinkingFromModel, diagnostics };\n}\n\nfunction resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {\n\treturn paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));\n}\n\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n): Promise<string | undefined> {\n\tinitTheme(settingsManager.getTheme());\n\tsetKeybindings(KeybindingsManager.create());\n\n\treturn new Promise((resolve) => {\n\t\tconst ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());\n\t\tui.setClearOnShrink(settingsManager.getClearOnShrink());\n\n\t\tlet settled = false;\n\t\tconst finish = (result: string | undefined) => {\n\t\t\tif (settled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tui.stop();\n\t\t\tresolve(result);\n\t\t};\n\n\t\tconst selector = new ExtensionSelectorComponent(\n\t\t\tformatMissingSessionCwdPrompt(issue),\n\t\t\t[\"Continue\", \"Cancel\"],\n\t\t\t(option) => finish(option === \"Continue\" ? issue.fallbackCwd : undefined),\n\t\t\t() => finish(undefined),\n\t\t\t{ tui: ui },\n\t\t);\n\t\tui.addChild(selector);\n\t\tui.setFocus(selector);\n\t\tui.start();\n\t});\n}\n\nexport interface MainOptions {\n\textensionFactories?: ExtensionFactory[];\n}\n\n/**\n * Built-in extension factories loaded when the caller supplies none. This is\n * the single source of truth for the app's built-ins (hoo-core: /loop, /plugin,\n * /mode, /cost, scaffold commands, the MCP loader + remote-MCP/OAuth flow).\n * Both entry points — the node CLI (bin/hoocode.js) and the compiled binary\n * (src/cli.ts) — call main() without factories and inherit this default.\n * Downstream embedders that pass their own extensionFactories override it.\n */\nconst DEFAULT_EXTENSION_FACTORIES: ExtensionFactory[] = [hooCore];\n\nexport async function main(args: string[], options?: MainOptions) {\n\tresetTimings();\n\tconst offlineMode = args.includes(\"--offline\") || isTruthyEnvFlag(process.env.HOOCODE_OFFLINE);\n\tif (offlineMode) {\n\t\tprocess.env.HOOCODE_OFFLINE = \"1\";\n\t\tprocess.env.HOOCODE_SKIP_VERSION_CHECK = \"1\";\n\t}\n\n\tif (await handlePackageCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleConfigCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleResourcesCommand(args)) {\n\t\treturn;\n\t}\n\n\tconst parsed = parseArgs(args);\n\tif (parsed.diagnostics.length > 0) {\n\t\tfor (const d of parsed.diagnostics) {\n\t\t\tconst color = d.type === \"error\" ? chalk.red : chalk.yellow;\n\t\t\tconsole.error(color(`${d.type === \"error\" ? \"Error\" : \"Warning\"}: ${d.message}`));\n\t\t}\n\t\tif (parsed.diagnostics.some((d) => d.type === \"error\")) {\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"parseArgs\");\n\tlet appMode = resolveAppMode(parsed, process.stdin.isTTY);\n\tconst shouldTakeOverStdout = appMode !== \"interactive\";\n\tif (shouldTakeOverStdout) {\n\t\ttakeOverStdout();\n\t}\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.export) {\n\t\tlet result: string;\n\t\ttry {\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tresult = await exportFromFile(parsed.export, outputPath);\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconsole.log(`Exported to: ${result}`);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tvalidateForkFlags(parsed);\n\n\t// Run migrations (pass cwd for project-local migrations)\n\tconst { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());\n\ttime(\"runMigrations\");\n\n\tconst cwd = process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst startupSettingsManager = SettingsManager.create(cwd, agentDir);\n\treportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, \"startup session lookup\"));\n\n\t// Decide the final runtime cwd before creating cwd-bound runtime services.\n\t// --session and --resume may select a session from another project, so project-local\n\t// settings, resources, provider registrations, and models must be resolved only after\n\t// the target session cwd is known. The startup-cwd settings manager is used only for\n\t// sessionDir lookup during session selection.\n\tconst envSessionDir = process.env[ENV_SESSION_DIR];\n\tconst sessionDir =\n\t\tparsed.sessionDir ??\n\t\t(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??\n\t\tstartupSettingsManager.getSessionDir();\n\tlet sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);\n\t\t\tif (!selectedCwd) {\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tsessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);\n\t\t} else {\n\t\t\tconsole.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"createSessionManager\");\n\n\tconst resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);\n\tconst resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);\n\tconst resolvedAgentPaths = resolveCliPaths(cwd, parsed.agents);\n\tconst resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);\n\tconst resolvedSlashCommandPaths = resolveCliPaths(cwd, parsed.slashCommands);\n\tconst resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);\n\n\t// Populate the module-level CLI agent store before any session is created.\n\tsetAgentCliPaths(resolvedAgentPaths ?? []);\n\n\t// Synthetic factory: feed CLI --mode-path values into the extension runtime\n\t// so hoo-core (and any other extension that reads pi.getModeSearchPaths)\n\t// sees them alongside extension-registered dirs.\n\tconst cliModePaths = parsed.modePaths ?? [];\n\tconst cliResourcePathFactories: ExtensionFactory[] =\n\t\tcliModePaths.length === 0\n\t\t\t? []\n\t\t\t: [\n\t\t\t\t\tObject.assign(\n\t\t\t\t\t\t(pi: ExtensionAPI) => {\n\t\t\t\t\t\t\tfor (const p of cliModePaths) pi.addModeSearchPath(p);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ internal: true },\n\t\t\t\t\t),\n\t\t\t\t];\n\t// `??` (not `[...a, ...b]`) so an explicit list — from bin/hoocode.js or a\n\t// downstream embedder — fully replaces the default and hoo-core is never\n\t// registered twice.\n\tconst allExtensionFactories: ExtensionFactory[] = [\n\t\t...cliResourcePathFactories,\n\t\t...(options?.extensionFactories ?? DEFAULT_EXTENSION_FACTORIES),\n\t];\n\tconst authStorage = AuthStorage.create();\n\t// A spawned subagent (run with a task id) is a single-shot, non-interactive\n\t// process: it renders no TUI and its prompt is a plain task, never a slash\n\t// command. Themes, slash commands, and prompt templates are dead weight in that\n\t// path, so skip loading them to trim the child's cold-boot cost. Skills, context\n\t// files, and extensions (which carry the core tools) are kept — they affect the\n\t// subagent's actual work.\n\tconst isSubagentBoot = parsed.taskId !== undefined;\n\tconst createRuntime: CreateAgentSessionRuntimeFactory = async ({\n\t\tcwd,\n\t\tagentDir,\n\t\tsessionManager,\n\t\tsessionStartEvent,\n\t}) => {\n\t\tconst services = await createAgentSessionServices({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tauthStorage,\n\t\t\textensionFlagValues: parsed.unknownFlags,\n\t\t\tresourceLoaderOptions: {\n\t\t\t\tadditionalExtensionPaths: resolvedExtensionPaths,\n\t\t\t\tadditionalSkillPaths: resolvedSkillPaths,\n\t\t\t\tadditionalPromptTemplatePaths: resolvedPromptTemplatePaths,\n\t\t\t\tadditionalSlashCommandPaths: resolvedSlashCommandPaths,\n\t\t\t\tadditionalThemePaths: resolvedThemePaths,\n\t\t\t\tnoExtensions: parsed.noExtensions,\n\t\t\t\tnoSkills: parsed.noSkills,\n\t\t\t\tnoPromptTemplates: parsed.noPromptTemplates || isSubagentBoot,\n\t\t\t\tnoSlashCommands: parsed.noSlashCommands || isSubagentBoot,\n\t\t\t\tnoThemes: parsed.noThemes || isSubagentBoot,\n\t\t\t\tnoContextFiles: parsed.noContextFiles,\n\t\t\t\tsystemPrompt: parsed.systemPrompt,\n\t\t\t\textensionFactories: allExtensionFactories,\n\t\t\t},\n\t\t});\n\t\tconst { settingsManager, modelRegistry, resourceLoader } = services;\n\t\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [\n\t\t\t...services.diagnostics,\n\t\t\t...collectSettingsDiagnostics(settingsManager, \"runtime creation\"),\n\t\t\t...resourceLoader.getExtensions().errors.map(({ path, error }) => ({\n\t\t\t\ttype: \"error\" as const,\n\t\t\t\tmessage: `Failed to load extension \"${path}\": ${error}`,\n\t\t\t})),\n\t\t];\n\n\t\t// When subagent tooling is enabled, append the main session subagent instructions.\n\t\tif (parsed.subagent ?? settingsManager.getEnableSubagent()) {\n\t\t\tresourceLoader.addAppendSystemPrompt(buildTaskMainPrompt());\n\t\t}\n\n\t\tconst modelPatterns = parsed.models ?? settingsManager.getEnabledModels();\n\t\tconst scopedModels =\n\t\t\tmodelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRegistry) : [];\n\t\tconst {\n\t\t\toptions: sessionOptions,\n\t\t\tcliThinkingFromModel,\n\t\t\tdiagnostics: sessionOptionDiagnostics,\n\t\t} = buildSessionOptions(\n\t\t\tparsed,\n\t\t\tscopedModels,\n\t\t\tsessionManager.buildSessionContext().messages.length > 0,\n\t\t\tmodelRegistry,\n\t\t\tsettingsManager,\n\t\t);\n\t\tdiagnostics.push(...sessionOptionDiagnostics);\n\n\t\tif (parsed.apiKey) {\n\t\t\tif (!sessionOptions.model) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: \"--api-key requires a model to be specified via --model, --provider/--model, or --models\",\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tauthStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);\n\t\t\t}\n\t\t}\n\n\t\tconst created = await createAgentSessionFromServices({\n\t\t\tservices,\n\t\t\tsessionManager,\n\t\t\tsessionStartEvent,\n\t\t\tmodel: sessionOptions.model,\n\t\t\tthinkingLevel: sessionOptions.thinkingLevel,\n\t\t\tscopedModels: sessionOptions.scopedModels,\n\t\t\ttools: sessionOptions.tools,\n\t\t\tnoTools: sessionOptions.noTools,\n\t\t\tcustomTools: sessionOptions.customTools,\n\t\t\tenableWebTools: sessionOptions.enableWebTools,\n\t\t\tenableBrowserTools: sessionOptions.enableBrowserTools,\n\t\t\tenableFileTools: sessionOptions.enableFileTools,\n\t\t});\n\t\tconst cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;\n\t\tif (created.session.model && cliThinkingOverride) {\n\t\t\tcreated.session.setThinkingLevel(created.session.thinkingLevel);\n\t\t}\n\n\t\treturn {\n\t\t\t...created,\n\t\t\tservices,\n\t\t\tdiagnostics,\n\t\t};\n\t};\n\ttime(\"createRuntime\");\n\tconst runtime = await createAgentSessionRuntime(createRuntime, {\n\t\tcwd: sessionManager.getCwd(),\n\t\tagentDir,\n\t\tsessionManager,\n\t});\n\tconst { services, session, modelFallbackMessage } = runtime;\n\tconst { settingsManager, modelRegistry, resourceLoader } = services;\n\n\tif (parsed.help) {\n\t\tconst extensionFlags = resourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.flatMap((extension) => Array.from(extension.flags.values()));\n\t\tprintHelp(extensionFlags);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRegistry, searchPattern);\n\t\tprocess.exit(0);\n\t}\n\n\t// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC\n\tlet stdinContent: string | undefined;\n\tif (appMode !== \"rpc\") {\n\t\tstdinContent = await readPipedStdin();\n\t\tif (stdinContent !== undefined && appMode === \"interactive\") {\n\t\t\tappMode = \"print\";\n\t\t}\n\t}\n\ttime(\"readPipedStdin\");\n\n\tconst { initialMessage, initialImages } = await prepareInitialMessage(\n\t\tparsed,\n\t\tsettingsManager.getImageAutoResize(),\n\t\tstdinContent,\n\t);\n\ttime(\"prepareInitialMessage\");\n\tinitTheme(settingsManager.getTheme(), appMode === \"interactive\");\n\ttime(\"initTheme\");\n\n\t// Show deprecation warnings in interactive mode\n\tif (appMode === \"interactive\" && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\tconst scopedModels = [...session.scopedModels];\n\ttime(\"resolveModelScope\");\n\treportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tprocess.exit(1);\n\t}\n\ttime(\"createAgentSession\");\n\n\tif (appMode !== \"interactive\" && !session.model) {\n\t\tconsole.error(chalk.red(formatNoModelsAvailableMessage()));\n\t\tprocess.exit(1);\n\t}\n\n\tconst startupBenchmark = isTruthyEnvFlag(process.env.HOOCODE_STARTUP_BENCHMARK);\n\tif (startupBenchmark && appMode !== \"interactive\") {\n\t\tconsole.error(chalk.red(\"Error: HOOCODE_STARTUP_BENCHMARK only supports interactive mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tif (appMode === \"rpc\") {\n\t\tprintTimings();\n\t\tawait runRpcMode(runtime);\n\t} else if (appMode === \"interactive\") {\n\t\tif (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) {\n\t\t\tconst modelList = scopedModels\n\t\t\t\t.map((sm) => {\n\t\t\t\t\tconst thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : \"\";\n\t\t\t\t\treturn `${sm.model.id}${thinkingStr}`;\n\t\t\t\t})\n\t\t\t\t.join(\", \");\n\t\t\tconsole.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray(\"(Ctrl+P to cycle)\")}`));\n\t\t}\n\n\t\tconst interactiveMode = new InteractiveMode(runtime, {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages: parsed.messages,\n\t\t\tverbose: parsed.verbose,\n\t\t});\n\n\t\t// Optional hooteams bridge. `--team auto` discovers a config, spawns a\n\t\t// local hooteams child on a free port, and proceeds as if its URL had\n\t\t// been passed; the child is reaped on exit (clean or signal) via a\n\t\t// process \"exit\" hook plus the explicit stop below.\n\t\tlet autoTeam: { url: string; stop(): Promise<void> } | undefined;\n\t\tlet teamUrl = parsed.team;\n\t\tif (teamUrl === \"auto\") {\n\t\t\tconst { startAutoTeam } = await import(\"./core/team-auto.js\");\n\t\t\ttry {\n\t\t\t\tautoTeam = await startAutoTeam(process.cwd(), { log: (message) => console.log(chalk.dim(message)) });\n\t\t\t\tteamUrl = autoTeam.url;\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(chalk.red(error instanceof Error ? error.message : String(error)));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\n\t\t// Team mirror + client. Fire-and-forget: connect failures and drops warn\n\t\t// in the background and never block the main agent. Warnings go through\n\t\t// the chat, not console.error: a raw stderr write while the TUI owns the\n\t\t// screen scribbles over the render and can leave the editor looking\n\t\t// frozen. The same connection powers team focus / nudge / attach.\n\t\tlet teamView: { stop(): void } | undefined;\n\t\tif (teamUrl) {\n\t\t\tconst { connectTeamView } = await import(\"./core/team-view.js\");\n\t\t\tconst teamClient = connectTeamView(teamUrl, {\n\t\t\t\twarn: (message) => interactiveMode.showWarning(message),\n\t\t\t});\n\t\t\tteamView = teamClient;\n\t\t\tinteractiveMode.attachTeamClient(teamClient);\n\t\t}\n\t\tif (startupBenchmark) {\n\t\t\tawait interactiveMode.init();\n\t\t\ttime(\"interactiveMode.init\");\n\t\t\tprintTimings();\n\t\t\tteamView?.stop();\n\t\t\tawait autoTeam?.stop();\n\t\t\tinteractiveMode.stop();\n\t\t\tstopThemeWatcher();\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tprintTimings();\n\t\ttry {\n\t\t\tawait interactiveMode.run();\n\t\t} finally {\n\t\t\tteamView?.stop();\n\t\t\tawait autoTeam?.stop();\n\t\t}\n\t} else {\n\t\tprintTimings();\n\t\tconst exitCode = await runPrintMode(runtime, {\n\t\t\tmode: toPrintOutputMode(appMode),\n\t\t\tmessages: parsed.messages,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\ttaskId: parsed.taskId,\n\t\t\tmaxTurns: parsed.maxTurns,\n\t\t});\n\t\tstopThemeWatcher();\n\t\trestoreStdout();\n\t\tif (exitCode !== 0) {\n\t\t\tprocess.exitCode = exitCode;\n\t\t}\n\t\t// Spawned subagents (run with a task id) must exit promptly once their work is\n\t\t// done and result.json is written. The child's runtime can leave handles open\n\t\t// (e.g. MCP client connections) that keep the event loop alive, so a natural\n\t\t// exit may never happen. When that occurs the parent lifeguard SIGKILLs the idle\n\t\t// child at the 60s heartbeat threshold and misreports an already-completed task\n\t\t// as \"stalled\". Force a clean exit after draining output to avoid that false stall.\n\t\tconst ranAsSubagent = typeof parsed.taskId === \"string\" && parsed.taskId.length > 0;\n\t\tif (ranAsSubagent) {\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tprocess.exit(exitCode);\n\t\t}\n\t\treturn;\n\t}\n}\n"]}
package/dist/main.js CHANGED
@@ -400,6 +400,9 @@ function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRegi
400
400
  // Plugin lifecycle tools (SearchPlugins, InstallPlugin, ...). Top-level agent
401
401
  // only: these are capability-acquisition tools and must never be available to
402
402
  // a spawned subagent child (privilege-amplification guardrail, spec §3).
403
+ // `enablePluginTools` is the master switch for the whole autonomous plugin
404
+ // system (default off) — it gates both these tools and the runtime reuse
405
+ // nudge (see extensions/core/prompt-reactive), so both flip together.
403
406
  if (!isSubagentChild && settingsManager.getEnablePluginTools()) {
404
407
  options.customTools = [
405
408
  ...(options.customTools ?? []),