one-code-extension 0.1.0

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 (153) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +202 -0
  3. package/agents/explore.md +24 -0
  4. package/agents/general-purpose.md +21 -0
  5. package/agents/plan.md +22 -0
  6. package/docs/decisions/auto-mode.md +526 -0
  7. package/docs/decisions/branding.md +127 -0
  8. package/docs/decisions/code-review-remediation.md +106 -0
  9. package/docs/decisions/compaction.md +140 -0
  10. package/docs/decisions/distribution.md +133 -0
  11. package/docs/decisions/hooks.md +64 -0
  12. package/docs/decisions/lsp.md +58 -0
  13. package/docs/decisions/mcp.md +104 -0
  14. package/docs/decisions/memory-state.md +170 -0
  15. package/docs/decisions/model-policy.md +126 -0
  16. package/docs/decisions/modes.md +178 -0
  17. package/docs/decisions/skills-plugins.md +56 -0
  18. package/docs/decisions/subagents-workflows.md +225 -0
  19. package/docs/decisions/system-prompt.md +98 -0
  20. package/docs/decisions/tools.md +407 -0
  21. package/docs/decisions/tui.md +202 -0
  22. package/docs/decisions.md +148 -0
  23. package/extensions/ask-user/index.ts +124 -0
  24. package/extensions/ask-user/questions.ts +60 -0
  25. package/extensions/auto-mode/actions.ts +78 -0
  26. package/extensions/auto-mode/classifier.ts +388 -0
  27. package/extensions/auto-mode/config.ts +237 -0
  28. package/extensions/auto-mode/decision-log.ts +60 -0
  29. package/extensions/auto-mode/defaults.ts +65 -0
  30. package/extensions/auto-mode/instructions.ts +74 -0
  31. package/extensions/auto-mode/model-picker.ts +225 -0
  32. package/extensions/auto-mode/model-select.ts +178 -0
  33. package/extensions/auto-mode/paths.ts +109 -0
  34. package/extensions/auto-mode/pause.ts +96 -0
  35. package/extensions/auto-mode/prompt.ts +307 -0
  36. package/extensions/auto-mode/rules.ts +96 -0
  37. package/extensions/auto-mode/safety-floor.ts +100 -0
  38. package/extensions/auto-mode/sensitive.ts +137 -0
  39. package/extensions/auto-mode/shell-analysis.ts +702 -0
  40. package/extensions/background/index.ts +405 -0
  41. package/extensions/background/registry.ts +81 -0
  42. package/extensions/background/wakeup.ts +41 -0
  43. package/extensions/bash/background.ts +136 -0
  44. package/extensions/bash/index.ts +149 -0
  45. package/extensions/branding/assistant-marker.ts +53 -0
  46. package/extensions/branding/index.ts +366 -0
  47. package/extensions/branding/prompt-editor.ts +42 -0
  48. package/extensions/branding/prompt-marker.ts +29 -0
  49. package/extensions/branding/startup.ts +152 -0
  50. package/extensions/claude-compat/index.ts +50 -0
  51. package/extensions/claude-context/index.ts +79 -0
  52. package/extensions/compaction/index.ts +231 -0
  53. package/extensions/compaction/prompt.ts +191 -0
  54. package/extensions/context-management/index.ts +147 -0
  55. package/extensions/effort/index.ts +216 -0
  56. package/extensions/effort/slider.ts +253 -0
  57. package/extensions/exit/index.ts +28 -0
  58. package/extensions/file-tracker/index.ts +103 -0
  59. package/extensions/file-tracker/tracker.ts +145 -0
  60. package/extensions/hooks/debug.ts +53 -0
  61. package/extensions/hooks/executor.ts +132 -0
  62. package/extensions/hooks/index.ts +310 -0
  63. package/extensions/hooks/matcher.ts +72 -0
  64. package/extensions/hooks/plugin-hooks.ts +79 -0
  65. package/extensions/hooks/protocol.ts +151 -0
  66. package/extensions/hooks/settings.ts +163 -0
  67. package/extensions/hooks/trust.ts +151 -0
  68. package/extensions/lib/branch-restore.ts +35 -0
  69. package/extensions/lib/claude-context.ts +148 -0
  70. package/extensions/lib/deferred.ts +119 -0
  71. package/extensions/lib/git.ts +14 -0
  72. package/extensions/lib/memory.ts +219 -0
  73. package/extensions/lib/model-policy.ts +436 -0
  74. package/extensions/lib/model-tier.ts +99 -0
  75. package/extensions/lib/notifications.ts +18 -0
  76. package/extensions/lib/paths.ts +26 -0
  77. package/extensions/lib/per-cwd.ts +21 -0
  78. package/extensions/lib/persisted-output.ts +70 -0
  79. package/extensions/lib/pi-version.ts +54 -0
  80. package/extensions/lib/plugins.ts +227 -0
  81. package/extensions/lib/reminders.ts +212 -0
  82. package/extensions/lib/scratchpad.ts +68 -0
  83. package/extensions/lib/tui-render.ts +522 -0
  84. package/extensions/lsp/client.ts +278 -0
  85. package/extensions/lsp/format.ts +46 -0
  86. package/extensions/lsp/index.ts +193 -0
  87. package/extensions/lsp/protocol.ts +64 -0
  88. package/extensions/lsp/servers.ts +79 -0
  89. package/extensions/mcp/client.ts +205 -0
  90. package/extensions/mcp/config.ts +199 -0
  91. package/extensions/mcp/index.ts +373 -0
  92. package/extensions/mcp/schema.ts +161 -0
  93. package/extensions/memory/index.ts +90 -0
  94. package/extensions/notebook/index.ts +77 -0
  95. package/extensions/notebook/notebook.ts +108 -0
  96. package/extensions/permissions/index.ts +876 -0
  97. package/extensions/permissions/matcher.ts +455 -0
  98. package/extensions/permissions/modes.ts +107 -0
  99. package/extensions/permissions/protected-paths.ts +125 -0
  100. package/extensions/permissions/settings.ts +98 -0
  101. package/extensions/plan-mode/index.ts +209 -0
  102. package/extensions/plan-mode/reminder.ts +33 -0
  103. package/extensions/plan-mode/slug.ts +28 -0
  104. package/extensions/plan-mode/viewer.ts +124 -0
  105. package/extensions/plugins/index.ts +115 -0
  106. package/extensions/plugins/template.ts +42 -0
  107. package/extensions/search-tools/index.ts +0 -0
  108. package/extensions/search-tools/tools.ts +24 -0
  109. package/extensions/skill/index.ts +170 -0
  110. package/extensions/subagents/agents.ts +126 -0
  111. package/extensions/subagents/child.ts +414 -0
  112. package/extensions/subagents/default-model.ts +122 -0
  113. package/extensions/subagents/index.ts +1119 -0
  114. package/extensions/subagents/model-select.ts +346 -0
  115. package/extensions/subagents/rpc-turns.ts +123 -0
  116. package/extensions/subagents/runs.ts +93 -0
  117. package/extensions/subagents/usage.ts +53 -0
  118. package/extensions/subagents/worktree.ts +69 -0
  119. package/extensions/system-prompt/environment.ts +35 -0
  120. package/extensions/system-prompt/index.ts +54 -0
  121. package/extensions/system-prompt/template.ts +103 -0
  122. package/extensions/system-prompt/tiers/common.ts +63 -0
  123. package/extensions/system-prompt/tiers/frontier.ts +24 -0
  124. package/extensions/system-prompt/tiers/low.ts +71 -0
  125. package/extensions/system-prompt/tiers/mid.ts +56 -0
  126. package/extensions/system-reminder/index.ts +37 -0
  127. package/extensions/tasks/index.ts +140 -0
  128. package/extensions/tasks/store.ts +174 -0
  129. package/extensions/todo/index.ts +133 -0
  130. package/extensions/tool-search/index.ts +168 -0
  131. package/extensions/tool-style/index.ts +84 -0
  132. package/extensions/web/index.ts +43 -0
  133. package/extensions/web-fetch/extract.ts +139 -0
  134. package/extensions/web-fetch/index.ts +291 -0
  135. package/extensions/web-fetch/summarize.ts +78 -0
  136. package/extensions/workflow/agent-session.ts +368 -0
  137. package/extensions/workflow/globals.ts +251 -0
  138. package/extensions/workflow/index.ts +364 -0
  139. package/extensions/workflow/journal.ts +75 -0
  140. package/extensions/workflow/permission-gate.ts +71 -0
  141. package/extensions/workflow/run-manager.ts +306 -0
  142. package/extensions/workflow/saved-workflows.ts +45 -0
  143. package/extensions/workflow/script-source.ts +201 -0
  144. package/extensions/workflow/types.ts +99 -0
  145. package/extensions/workflow/vm-runtime.ts +31 -0
  146. package/extensions/workflow/widget.ts +56 -0
  147. package/extensions/worktree/index.ts +266 -0
  148. package/extensions/worktree/rewrite.ts +59 -0
  149. package/package.json +101 -0
  150. package/themes/one-code-light.json +76 -0
  151. package/themes/one-code.json +76 -0
  152. package/types/app.d.ts +5 -0
  153. package/types/community-packages.d.ts +12 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Child-process subagent runner: spawns `pi --mode json -p`, parses its JSONL
3
+ * event stream, and exposes a handle usable both foreground (await result)
4
+ * and background (kill/snapshot, registered as a background task).
5
+ *
6
+ * Children persist their sessions (`--session-dir` per run) so a finished
7
+ * agent can be resumed later via `--session <file>` — that is what
8
+ * send_message builds on. `--no-session` is only the fallback when the parent
9
+ * itself has no session directory.
10
+ */
11
+
12
+ import { spawn } from "node:child_process";
13
+ import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
14
+ import os from "node:os";
15
+ import { basename, join } from "node:path";
16
+ import { type ChildAction, recordAction } from "../auto-mode/actions.ts";
17
+ import type { AgentDefinition } from "./agents.ts";
18
+ import { RpcTurnTracker, toMainMessage } from "./rpc-turns.ts";
19
+ import { addUsage, emptyUsage, type UsageTotals } from "./usage.ts";
20
+
21
+ export const OUTPUT_CAP = 50_000;
22
+
23
+ export function piInvocation(args: string[]): { command: string; args: string[] } {
24
+ const currentScript = process.argv[1];
25
+ if (currentScript && !currentScript.startsWith("/$bunfs/root/") && existsSync(currentScript)) {
26
+ return { command: process.execPath, args: [currentScript, ...args] };
27
+ }
28
+ const execName = basename(process.execPath).toLowerCase();
29
+ if (!/^(node|bun)(\.exe)?$/.test(execName)) {
30
+ return { command: process.execPath, args };
31
+ }
32
+ return { command: "pi", args };
33
+ }
34
+
35
+ /**
36
+ * Framing wrapped around a fork child's task. A fork inherits the parent's
37
+ * entire transcript; without this, a weaker model tends to abandon its
38
+ * assigned task and continue (or confabulate about) the inherited topic —
39
+ * then its output returns to the parent looking like independent
40
+ * confirmation. See docs/handoff-tool-ambiguity.md (fork confabulation).
41
+ */
42
+ export function forkTaskMessage(task: string): string {
43
+ return [
44
+ "You are a forked subagent. The conversation above is inherited context, for reference only — do NOT continue its open threads, verify its claims, or act on its plans. Do ONLY the task below; your final message is returned to the parent conversation verbatim, as data. You cannot see the parent's background tasks: its task ids are not addressable from here.",
45
+ "",
46
+ "Task:",
47
+ task,
48
+ ].join("\n");
49
+ }
50
+
51
+ export interface ChildOptions {
52
+ agent?: AgentDefinition;
53
+ task: string;
54
+ cwd: string;
55
+ /** Parent session file, for fork runs. */
56
+ forkFrom?: string;
57
+ /** Existing child session to continue (send_message). */
58
+ sessionFile?: string;
59
+ /** Where a new run's session file lands; falls back to --no-session. */
60
+ sessionDir?: string;
61
+ model?: string;
62
+ thinking?: string;
63
+ signal?: AbortSignal;
64
+ onProgress: (toolCalls: number, lastText: string, usage: UsageTotals) => void;
65
+ /** The child called send_message {to: "main"} — relay it to the main conversation. */
66
+ onMessageToMain?: (message: string, summary?: string) => void;
67
+ }
68
+
69
+ /** Session/agent/model flags shared by print and rpc children. */
70
+ function buildChildFlags(options: Omit<ChildOptions, "task" | "signal" | "onProgress">): string[] {
71
+ const { agent, forkFrom, sessionFile, sessionDir } = options;
72
+ const args: string[] = [];
73
+
74
+ if (sessionFile) {
75
+ args.push("--session", sessionFile);
76
+ } else if (forkFrom) {
77
+ // pi forks the session file into a fresh session, so the child starts with
78
+ // the parent's transcript and our own system prompt still applies.
79
+ args.push("--fork", forkFrom);
80
+ if (sessionDir) args.push("--session-dir", sessionDir);
81
+ } else {
82
+ if (sessionDir) args.push("--session-dir", sessionDir);
83
+ else args.push("--no-session");
84
+ if (agent) {
85
+ const tmpDir = mkdtempSync(join(os.tmpdir(), "cc-subagent-"));
86
+ const promptPath = join(tmpDir, "system.md");
87
+ writeFileSync(promptPath, agent.systemPrompt);
88
+ args.push("--system-prompt", promptPath);
89
+ }
90
+ }
91
+
92
+ const model = options.model ?? agent?.model;
93
+ if (model) args.push("--model", model);
94
+ if (options.thinking) args.push("--thinking", options.thinking);
95
+ if (agent?.tools && !forkFrom && !sessionFile) args.push("--tools", agent.tools.join(","));
96
+ return args;
97
+ }
98
+
99
+ export interface ChildOutcome {
100
+ output: string;
101
+ toolCalls: number;
102
+ usage: UsageTotals;
103
+ failed?: boolean;
104
+ /**
105
+ * What the child actually did, names and short subjects only. Auto mode
106
+ * reviews this when the child returns, to catch a sequence whose individual
107
+ * steps each passed. Never carries tool output — see auto-mode/actions.ts.
108
+ */
109
+ actions: ChildAction[];
110
+ }
111
+
112
+ /**
113
+ * A provider failure in a child (auth, billing, rate limit) surfaces ONLY as
114
+ * an assistant message with stopReason "error" in the event stream — stderr
115
+ * stays empty and the process exits normally. Without capturing it, the
116
+ * parent gets "Subagent produced no output (exit code 0)", the exact
117
+ * ambiguous result a weak model confabulates a cause for. The error is
118
+ * cleared by any later successful assistant message, so pi's internal
119
+ * retries don't leave a stale failure.
120
+ */
121
+ export function providerErrorFrom(message: { stopReason?: string; errorMessage?: string } | undefined): string | undefined {
122
+ if (!message) return undefined;
123
+ if (message.stopReason !== "error") return undefined;
124
+ return message.errorMessage || "unknown provider error";
125
+ }
126
+
127
+ /** The outcome a finished child resolves to, given everything its stream said. */
128
+ export function closeOutcome(args: {
129
+ finalText: string;
130
+ providerError?: string;
131
+ code: number | null;
132
+ closeSignal: NodeJS.Signals | null;
133
+ stderr: string;
134
+ toolCalls: number;
135
+ usage: UsageTotals;
136
+ actions: ChildAction[];
137
+ }): ChildOutcome {
138
+ const { finalText, providerError, code, closeSignal, stderr, toolCalls, usage, actions } = args;
139
+ const output = finalText.slice(0, OUTPUT_CAP);
140
+ if (providerError) {
141
+ return {
142
+ output: output.trim()
143
+ ? `${output}\n\n[The subagent's last request ended with a provider error: ${providerError}]`
144
+ : `Subagent failed with a provider error (its model could not be called — an auth/billing/rate-limit problem, not a task failure): ${providerError}`,
145
+ toolCalls,
146
+ usage,
147
+ actions,
148
+ failed: true,
149
+ };
150
+ }
151
+ if (output.trim()) {
152
+ return { output, toolCalls, usage, actions };
153
+ }
154
+ const why = closeSignal ? `terminated by ${closeSignal}` : `exit code ${code}`;
155
+ const detail = stderr.trim().split("\n").slice(-5).join("\n");
156
+ return {
157
+ output: `Subagent produced no output (${why}).${detail ? `\n${detail}` : ""}`,
158
+ toolCalls,
159
+ usage,
160
+ actions,
161
+ failed: true,
162
+ };
163
+ }
164
+
165
+ export interface ChildHandle {
166
+ result: Promise<ChildOutcome>;
167
+ kill(): void;
168
+ snapshot(): { toolCalls: number; text: string; usage: UsageTotals };
169
+ }
170
+
171
+ export function startChild(options: ChildOptions): ChildHandle {
172
+ const { cwd, signal, onProgress } = options;
173
+ const task = options.forkFrom ? forkTaskMessage(options.task) : options.task;
174
+ const args = ["--mode", "json", "-p", ...buildChildFlags(options), task];
175
+
176
+ const invocation = piInvocation(args);
177
+ const child = spawn(invocation.command, invocation.args, {
178
+ cwd,
179
+ shell: false,
180
+ stdio: ["ignore", "pipe", "pipe"],
181
+ env: { ...process.env, PI_SUBAGENT_CHILD: "1" },
182
+ });
183
+
184
+ let buffer = "";
185
+ let finalText = "";
186
+ let toolCalls = 0;
187
+ const actions: ChildAction[] = [];
188
+ let stderr = "";
189
+ let providerError: string | undefined;
190
+ const usage = emptyUsage();
191
+
192
+ const onAbort = () => child.kill("SIGTERM");
193
+ signal?.addEventListener("abort", onAbort, { once: true });
194
+
195
+ const processLine = (line: string) => {
196
+ if (!line.trim()) return;
197
+ let event: {
198
+ type?: string;
199
+ message?: { role?: string; content?: unknown; usage?: unknown; stopReason?: string; errorMessage?: string };
200
+ };
201
+ try {
202
+ event = JSON.parse(line);
203
+ } catch {
204
+ return;
205
+ }
206
+ if (event.type === "tool_execution_end") {
207
+ const toMain = toMainMessage(event);
208
+ if (toMain) options.onMessageToMain?.(toMain.message, toMain.summary);
209
+ } else if (event.type === "tool_execution_start") {
210
+ toolCalls++;
211
+ const start = event as { toolName?: string; args?: unknown };
212
+ if (start.toolName) recordAction(actions, start.toolName, start.args);
213
+ onProgress(toolCalls, finalText, usage);
214
+ } else if (event.type === "message_end" && event.message?.role === "assistant") {
215
+ addUsage(usage, event.message.usage);
216
+ providerError = providerErrorFrom(event.message);
217
+ const blocks = Array.isArray(event.message.content) ? event.message.content : [];
218
+ const text = blocks
219
+ .filter((b): b is { type: string; text: string } => (b as { type?: string }).type === "text")
220
+ .map((b) => b.text)
221
+ .join("");
222
+ if (text.trim()) finalText = text;
223
+ onProgress(toolCalls, finalText, usage);
224
+ }
225
+ };
226
+
227
+ const result = new Promise<ChildOutcome>((resolve) => {
228
+ child.stdout.on("data", (chunk: Buffer) => {
229
+ buffer += chunk.toString();
230
+ let idx: number;
231
+ while ((idx = buffer.indexOf("\n")) !== -1) {
232
+ processLine(buffer.slice(0, idx));
233
+ buffer = buffer.slice(idx + 1);
234
+ }
235
+ });
236
+ child.stderr.on("data", (chunk: Buffer) => {
237
+ stderr += chunk.toString();
238
+ });
239
+
240
+ child.on("error", (error) => {
241
+ signal?.removeEventListener("abort", onAbort);
242
+ resolve({ output: `Failed to start subagent: ${error.message}`, toolCalls, usage, actions, failed: true });
243
+ });
244
+
245
+ child.on("close", (code, closeSignal) => {
246
+ signal?.removeEventListener("abort", onAbort);
247
+ if (buffer.trim()) processLine(buffer);
248
+ resolve(closeOutcome({ finalText, providerError, code, closeSignal, stderr, toolCalls, usage, actions }));
249
+ });
250
+ });
251
+
252
+ return {
253
+ result,
254
+ kill: () => child.kill("SIGTERM"),
255
+ snapshot: () => ({ toolCalls, text: finalText, usage }),
256
+ };
257
+ }
258
+
259
+ export interface RpcChildOptions extends Omit<ChildOptions, "task" | "signal" | "onProgress"> {
260
+ onProgress: (toolCalls: number, lastText: string, usage: UsageTotals) => void;
261
+ /** Fires at the end of EVERY turn (initial task and later messages alike). */
262
+ onTurnEnd: (outcome: ChildOutcome) => void;
263
+ onExit?: () => void;
264
+ }
265
+
266
+ export interface RpcChildHandle {
267
+ /**
268
+ * Deliver a message. "started" = the child was idle and this began a new
269
+ * turn (onTurnEnd will fire for it); "steered" = the child was mid-turn and
270
+ * the message joined it (covered by that turn's onTurnEnd).
271
+ */
272
+ send(message: string): "started" | "steered";
273
+ busy(): boolean;
274
+ exited(): boolean;
275
+ kill(): void;
276
+ snapshot(): { toolCalls: number; text: string; usage: UsageTotals };
277
+ }
278
+
279
+ /**
280
+ * Resident child over `pi --mode rpc`: same event stream as json mode, plus a
281
+ * stdin command channel — which is what makes messaging a *running* agent
282
+ * possible (`streamingBehavior: "steer"` queues into the current turn; when
283
+ * idle the same command starts a new turn, so there is no idle/busy race).
284
+ * RPC children have hasUI=true, so extension_ui_request dialogs must be
285
+ * answered: every one is cancelled, which reproduces print-mode's
286
+ * non-interactive fallback (gated tools deny instead of deadlocking).
287
+ */
288
+ export function startRpcChild(options: RpcChildOptions): RpcChildHandle {
289
+ const args = ["--mode", "rpc", ...buildChildFlags(options)];
290
+ const invocation = piInvocation(args);
291
+ const child = spawn(invocation.command, invocation.args, {
292
+ cwd: options.cwd,
293
+ shell: false,
294
+ stdio: ["pipe", "pipe", "pipe"],
295
+ env: { ...process.env, PI_SUBAGENT_CHILD: "1" },
296
+ });
297
+
298
+ const tracker = new RpcTurnTracker();
299
+ let buffer = "";
300
+ let stderr = "";
301
+ let exited = false;
302
+ /**
303
+ * Messages accepted while a prompt was sent but the child's agent loop had
304
+ * not started yet (it boots for a few seconds) — a steer written in that
305
+ * window would run as its own turn instead of joining the pending one.
306
+ * Flushed as steers the moment the child reports agent_start.
307
+ */
308
+ let earlySteers: string[] = [];
309
+
310
+ const send = (command: Record<string, unknown>) => {
311
+ try {
312
+ child.stdin.write(`${JSON.stringify(command)}\n`);
313
+ } catch {
314
+ // A dead pipe surfaces via the close handler.
315
+ }
316
+ };
317
+
318
+ child.stdout.on("data", (chunk: Buffer) => {
319
+ buffer += chunk.toString();
320
+ let idx: number;
321
+ while ((idx = buffer.indexOf("\n")) !== -1) {
322
+ const line = buffer.slice(0, idx);
323
+ buffer = buffer.slice(idx + 1);
324
+ const action = tracker.process(line);
325
+ if (!action) continue;
326
+ if (action.kind === "ui_request") {
327
+ send({ type: "extension_ui_response", id: action.id, cancelled: true });
328
+ } else if (action.kind === "stream_start") {
329
+ for (const message of earlySteers) {
330
+ send({ type: "prompt", message, streamingBehavior: "steer" });
331
+ }
332
+ earlySteers = [];
333
+ } else if (action.kind === "progress") {
334
+ options.onProgress(tracker.toolCalls, tracker.turnText, tracker.usage);
335
+ } else if (action.kind === "message_to_main") {
336
+ options.onMessageToMain?.(action.message, action.summary);
337
+ } else if (action.kind === "turn_end") {
338
+ options.onTurnEnd(turnOutcome());
339
+ }
340
+ }
341
+ });
342
+ child.stderr.on("data", (chunk: Buffer) => {
343
+ stderr += chunk.toString();
344
+ });
345
+
346
+ const turnOutcome = (): ChildOutcome => {
347
+ const output = tracker.turnText.slice(0, OUTPUT_CAP);
348
+ if (tracker.providerError) {
349
+ return {
350
+ output: output.trim()
351
+ ? `${output}\n\n[The subagent's last request ended with a provider error: ${tracker.providerError}]`
352
+ : `Subagent failed with a provider error (its model could not be called — an auth/billing/rate-limit problem, not a task failure): ${tracker.providerError}`,
353
+ toolCalls: tracker.toolCalls,
354
+ usage: tracker.usage,
355
+ actions: tracker.actions,
356
+ failed: true,
357
+ };
358
+ }
359
+ if (output.trim()) return { output, toolCalls: tracker.toolCalls, usage: tracker.usage, actions: tracker.actions };
360
+ const detail = stderr.trim().split("\n").slice(-5).join("\n");
361
+ return {
362
+ output: `Subagent produced no output.${detail ? `\n${detail}` : ""}`,
363
+ toolCalls: tracker.toolCalls,
364
+ usage: tracker.usage,
365
+ actions: tracker.actions,
366
+ failed: true,
367
+ };
368
+ };
369
+
370
+ const onClose = () => {
371
+ if (exited) return;
372
+ exited = true;
373
+ if (tracker.busy) {
374
+ tracker.busy = false;
375
+ const detail = stderr.trim().split("\n").slice(-5).join("\n");
376
+ const provider = tracker.providerError ? `\nLast provider error: ${tracker.providerError}` : "";
377
+ options.onTurnEnd({
378
+ output: `Subagent exited mid-turn.${provider}${detail ? `\n${detail}` : ""}`,
379
+ toolCalls: tracker.toolCalls,
380
+ usage: tracker.usage,
381
+ actions: tracker.actions,
382
+ failed: true,
383
+ });
384
+ }
385
+ options.onExit?.();
386
+ };
387
+ child.on("close", onClose);
388
+ child.on("error", onClose);
389
+
390
+ return {
391
+ send(message: string): "started" | "steered" {
392
+ if (exited) throw new Error("subagent process has exited");
393
+ if (!tracker.busy) {
394
+ tracker.beginTurn();
395
+ send({ type: "prompt", message, streamingBehavior: "steer" });
396
+ return "started";
397
+ }
398
+ if (tracker.streaming) {
399
+ send({ type: "prompt", message, streamingBehavior: "steer" });
400
+ } else {
401
+ earlySteers.push(message);
402
+ }
403
+ return "steered";
404
+ },
405
+ busy: () => tracker.busy,
406
+ exited: () => exited,
407
+ kill: () => child.kill("SIGTERM"),
408
+ snapshot: () => ({
409
+ toolCalls: tracker.toolCalls,
410
+ text: tracker.busy && tracker.turnText ? `${tracker.transcript ? `${tracker.transcript}\n\n---\n\n` : ""}${tracker.turnText}` : tracker.transcript,
411
+ usage: tracker.usage,
412
+ }),
413
+ };
414
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The configured default subagent model (pure fs).
3
+ *
4
+ * Two knobs, Claude Code's first:
5
+ *
6
+ * - `CLAUDE_CODE_SUBAGENT_MODEL` — Claude Code's env var. Honored from the real
7
+ * environment, and from the `env` block of *user and managed* settings files
8
+ * (Claude Code applies settings `env` to the process; One Code does not, so this
9
+ * reads the one variable it needs). Project-scope settings are deliberately
10
+ * not read: both project files live in the repository, and a checked-in `env`
11
+ * block must not get to pick which provider receives subagent tasks — the
12
+ * same containment rule as `autoMode` config.
13
+ * - `subagentModel` — One Code's own top-level setting, same shape as
14
+ * `autoMode.classifierModel`. Wins over the env var when both are set,
15
+ * because it is the more specific statement of intent.
16
+ *
17
+ * The env var additionally applies only to Claude-family sessions (see
18
+ * `applicableSubagentDefault`): it is Claude Code's knob, and its typical
19
+ * values ("sonnet") were written for Anthropic models. On any other provider
20
+ * it is ignored; One Code's automatic same-provider role profile then chooses the
21
+ * default unless the user overrides it with `subagentModel`.
22
+ *
23
+ * The value is a spec, not a model: "sonnet", "inherit", or "provider/id" all
24
+ * pass through to resolveSubagentModel, which owns the semantics.
25
+ */
26
+
27
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
28
+ import { dirname, join } from "node:path";
29
+ import { autoModeSettingsPaths } from "../auto-mode/config.ts";
30
+
31
+ export interface SubagentDefault {
32
+ spec: string;
33
+ /** Which knob set it, for notices. */
34
+ source: "subagentModel setting" | "CLAUDE_CODE_SUBAGENT_MODEL";
35
+ }
36
+
37
+ interface SettingsFile {
38
+ subagentModel?: unknown;
39
+ env?: Record<string, unknown>;
40
+ }
41
+
42
+ function readSettings(path: string): SettingsFile | undefined {
43
+ if (!existsSync(path)) return undefined;
44
+ try {
45
+ return JSON.parse(readFileSync(path, "utf-8")) as SettingsFile;
46
+ } catch {
47
+ return undefined;
48
+ }
49
+ }
50
+
51
+ export function loadSubagentDefault(home: string, env: NodeJS.ProcessEnv = process.env): SubagentDefault | undefined {
52
+ let setting: string | undefined;
53
+ let settingsEnv: string | undefined;
54
+
55
+ // Lowest precedence first, so managed settings override user settings.
56
+ for (const path of autoModeSettingsPaths(home)) {
57
+ const file = readSettings(path);
58
+ if (!file) continue;
59
+ if (typeof file.subagentModel === "string" && file.subagentModel.trim()) {
60
+ setting = file.subagentModel.trim();
61
+ }
62
+ const fromEnvBlock = file.env?.CLAUDE_CODE_SUBAGENT_MODEL;
63
+ if (typeof fromEnvBlock === "string" && fromEnvBlock.trim()) {
64
+ settingsEnv = fromEnvBlock.trim();
65
+ }
66
+ }
67
+
68
+ if (setting) return { spec: setting, source: "subagentModel setting" };
69
+
70
+ const fromEnv = env.CLAUDE_CODE_SUBAGENT_MODEL?.trim();
71
+ if (fromEnv) return { spec: fromEnv, source: "CLAUDE_CODE_SUBAGENT_MODEL" };
72
+ if (settingsEnv) return { spec: settingsEnv, source: "CLAUDE_CODE_SUBAGENT_MODEL" };
73
+
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * The configured default that actually applies to this session.
79
+ *
80
+ * `subagentModel` is One Code's own setting — the user told *this* harness what
81
+ * subagents run on, so it applies on any provider. `CLAUDE_CODE_SUBAGENT_MODEL`
82
+ * was written for Claude Code, almost always as an Anthropic alias ("sonnet"),
83
+ * so it applies only when the session is already on a Claude model — borrowing
84
+ * it elsewhere silently moved subagent work off the provider the user chose
85
+ * for this session (observed live: an opencode deepseek session's subagents
86
+ * landing on claude-sonnet-5 because a settings `env` block said "sonnet").
87
+ */
88
+ export function applicableSubagentDefault(
89
+ configured: SubagentDefault | undefined,
90
+ sessionModel: { provider: string; id: string } | undefined,
91
+ ): SubagentDefault | undefined {
92
+ if (!configured || configured.source === "subagentModel setting") return configured;
93
+ if (!sessionModel) return configured;
94
+ const claudeFamily = sessionModel.provider === "anthropic" || /claude/i.test(sessionModel.id);
95
+ return claudeFamily ? configured : undefined;
96
+ }
97
+
98
+ /**
99
+ * Persist `subagentModel` in the *user* settings file, preserving every other
100
+ * key. `undefined` removes the setting. Always user scope, like the loader:
101
+ * the default may move subagent work to another provider, so it lives where
102
+ * only the user writes — never in a project file (see the module comment).
103
+ *
104
+ * Unlike loading, this throws on a malformed file: a lenient read merely
105
+ * skips a setting, but a lenient write would replace the user's whole
106
+ * settings file with only ours.
107
+ */
108
+ export function persistSubagentModel(spec: string | undefined, home: string): void {
109
+ const path = join(home, ".claude", "settings.json");
110
+ let file: Record<string, unknown> = {};
111
+ if (existsSync(path)) {
112
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
113
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
114
+ throw new Error(`${path}: root must be a JSON object`);
115
+ }
116
+ file = parsed as Record<string, unknown>;
117
+ }
118
+ if (spec === undefined) delete file.subagentModel;
119
+ else file.subagentModel = spec;
120
+ mkdirSync(dirname(path), { recursive: true });
121
+ writeFileSync(path, `${JSON.stringify(file, null, 2)}\n`);
122
+ }