one-code-extension 0.1.3 → 0.3.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 (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +200 -172
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +126 -0
  8. package/docs/guide/configuration.md +171 -0
  9. package/docs/guide/differences-from-claude-code.md +150 -0
  10. package/docs/guide/doctor.md +86 -0
  11. package/docs/guide/hooks.md +109 -0
  12. package/docs/guide/installation.md +124 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
  14. package/docs/guide/providers-and-models.md +194 -0
  15. package/docs/guide/reference.md +224 -0
  16. package/docs/guide/sessions-and-context.md +190 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +176 -0
  18. package/docs/guide/subagents-and-workflows.md +193 -0
  19. package/docs/guide/tasks-and-background-work.md +111 -0
  20. package/docs/guide/terminal-interface.md +155 -0
  21. package/docs/guide/tools.md +175 -0
  22. package/docs/guide/troubleshooting.md +175 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -202
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. package/extensions/workflow/permission-gate.ts +0 -71
package/docs/decisions.md DELETED
@@ -1,148 +0,0 @@
1
- # Decisions
2
-
3
- Why this package is shaped the way it is — the choices that aren't obvious from
4
- the code, and which community packages were adopted, rejected, or replaced and
5
- on what evidence. This file is the **index**: a one-liner per decision, grouped
6
- by area. Full rationale lives in `docs/decisions/`; follow the link on each
7
- entry. (Companion docs: [`findings.md`](findings.md) for how pi behaves,
8
- [`handoff.md`](handoff.md) for current state and what's left.)
9
-
10
- ## Distribution & dependencies
11
-
12
- Full context: [`decisions/distribution.md`](decisions/distribution.md).
13
-
14
- - **[Two artifacts from one codebase: the app and the package](decisions/distribution.md#two-artifacts-from-one-codebase-the-app-and-the-package-2026-08-10)** — npm `@one-ai/one-code` (bundled app: pinned pi, `~/.one-code` isolation via `PI_CODING_AGENT_DIR`, package-registration load shape, stdout rebranding, own update check) + npm `one-code-extension` (peer-dep pi package with a runtime tested-range warning); lockstep versions from 0.1.2; Homebrew tap `isurumaduranga/one-ai` delivers the app (brew's `--min-release-age=1` makes day-one brew installs fail by design).
15
- - **[Distribution: pi package, not a wrapper binary](decisions/distribution.md#distribution-pi-package-not-a-wrapper-binary)** — *(partially superseded by the two-artifact model)* ship as an installable pi package because pi's rebranding resolves from pi's own `package.json`; a dependent can't rebrand it, and a package keeps upstream upgrades a version bump away.
16
- - **[Community packages: adopted where they work](decisions/distribution.md#community-packages-adopted-where-they-work)** — prefer ecosystem packages, but trial first: only `pi-ask-user` survived; `pi-subagents` was rejected (children SIGKILLed ~29 ms after spawn here).
17
- - **[Publish readiness (Phase 7)](decisions/distribution.md#publish-readiness-phase-7)** — what's done and what remains before `npm publish` (metadata, tarball, install-path verification).
18
-
19
- ## Branding, themes & startup
20
-
21
- Full context: [`decisions/branding.md`](decisions/branding.md).
22
-
23
- - **[Branding without a fork](decisions/branding.md#branding-without-a-fork)** — the π banner, shortcut hints, and resource sections are extension output, not a patched pi; `CC_NO_BANNER=1` restores pi's header.
24
- - **[Themes: authored, not adopted](decisions/branding.md#themes-authored-not-adopted)** — `one-code`/`one-code-light` are hand-authored 51-token themes; no community theme fit.
25
- - **[Rebrand: pincer](decisions/branding.md#rebrand-pincer)** — (history) why the product was "pincer" / npm `pincer-agent`, and Claude Code stays descriptive ("compatible with…") never titular.
26
- - **[Rebrand: One Code](decisions/branding.md#rebrand-one-code)** — pincer → **One Code** / npm `one-code`: the stacked wordmark, the full rename surface (state dir, env, channels, repo), and what stays as history.
27
- - **[Startup listing: quietStartup + banner sections](decisions/branding.md#startup-listing-quietstartup-banner-sections)** — when pi's `quietStartup` is on, compact context/skills/workflows/themes sections replace pi's noisy resource listing.
28
-
29
- ## System prompt
30
-
31
- Full context: [`decisions/system-prompt.md`](decisions/system-prompt.md).
32
-
33
- - **[Per-turn `before_agent_start`, not a static override](decisions/system-prompt.md#system-prompt-per-turn-before_agent_start-not-a-static-override)** — rebuilt each turn so it reflects the live tool set (plan mode, deferred loading change it); the environment block is cached per (cwd, model) to stay byte-stable for prompt caching.
34
- - **[Tiered by model capability](decisions/system-prompt.md#system-prompt-tiered-by-model-capability-2026-08-07)** — three prompt tiers selected by model capability, tuning prompt *text* only (decoding params are a separate future dial).
35
-
36
- ## Tools
37
-
38
- Full context: [`decisions/tools.md`](decisions/tools.md).
39
-
40
- - **[Tool names stay pi-idiomatic (snake_case)](decisions/tools.md#tool-names-stay-pi-idiomatic-snake_case)** — register `todo_write`/`subagent`-style names; `matcher.ts` maps users' Claude Code PascalCase permission rules onto them.
41
- - **[Deferred tools (ToolSearch)](decisions/tools.md#deferred-tools-toolsearch)** — how tool schemas are kept out of the prompt until asked for, using the provider's native mechanism where available.
42
- - **[Web tools](decisions/tools.md#web-tools)** — `web_search` wraps the one community dep (`pi-web-search`); the rest is our own.
43
- - **[AskUserQuestion — our own](decisions/tools.md#askuserquestion-our-own)** — the multi-question schema, option lists, multi-select, and headless fallback.
44
- - **[web_fetch answers a `prompt` with a reader model](decisions/tools.md#web_fetch-answers-a-prompt-with-a-reader-model)** — fetch → readability → markdown, and an optional `prompt` answered by a small reader model with a loud raw-content fallback.
45
- - **[Tool errors fail loud, never soft-fallback](decisions/tools.md#tool-errors-fail-loud-never-soft-fallback)** — a malformed/ambiguous call returns `isError:true` with the fix named, never a plausible-but-wrong success (the `subagent`-catalog archetype); audit + the 13 fixes across 12 tools.
46
- - **[Harness discipline (divergences found by self-comparison)](decisions/tools.md#harness-discipline-divergences-found-by-self-comparison)** — Claude-Code-compatible behaviours found by self-comparison that aren't obvious from any single tool.
47
- - **[Tier-aware tool surface: search built-ins for mid/low only (unreviewed)](decisions/tools.md#tier-aware-tool-surface-search-built-ins-for-midlow-only-unreviewed)** — pi never activates `grep`/`find`/`ls`; mid/low tiers now get them (their prompts demand them), frontier stays lean like Claude Code's.
48
- - **[Subagent steering: injected catalog, fork hardening (unreviewed)](decisions/tools.md#subagent-steering-injected-catalog-fork-hardening-unreviewed)** — the agent catalog rides an every-turn reminder; a fork's task gets a do-only-this preamble; `model`/`thinking` on a fork fail loud.
49
- - **[Background bash: override the built-in, gate before detaching (unreviewed)](decisions/tools.md#background-bash-override-the-built-in-gate-before-detaching-unreviewed)** — `run_in_background` on a pi-executor-delegating `bash` override; task_output/task_stop work unchanged; never auto-allowed.
50
- - **[Deferral runs on all tiers; steering follows CC's channels](decisions/tools.md#deferral-runs-on-all-tiers-steering-follows-ccs-channels-2026-08-10)** — deferral applies on every tier (capture-confirmed CC defers on Haiku too; verified 35→15 tools on deepseek); notifications carry CC's anti-confabulation preamble; MCP server `instructions` are injected, not dropped.
51
- - **[CLAUDE.md, memory, and skills injected as the CC reminder stack](decisions/tools.md#claudemd-memory-and-skills-injected-as-the-cc-reminder-stack-2026-08-10)** — moved out of the system prompt into the byte-identical first-user-message `<system-reminder>` stack (`claude-context` + `lib/claude-context.ts`, placement/order/suffix on the reminder queue).
52
-
53
- ## Auto mode
54
-
55
- Full context: [`decisions/auto-mode.md`](decisions/auto-mode.md).
56
-
57
- - **[A deterministic pre-gate in front of an LLM classifier](decisions/auto-mode.md#auto-mode-a-deterministic-pre-gate-in-front-of-an-llm-classifier)** — a shell pre-gate that may only ever conclude "safe" (a latency optimisation, never a deny path), backed by an LLM approval classifier.
58
- - **[Parity pass against published Claude Code behaviour](decisions/auto-mode.md#auto-mode-parity-pass-against-the-published-claude-code-behaviour)** — where our auto mode was aligned to the shipped Claude Code auto-approve behaviour.
59
- - **[Grounding the verdict: cite a rule, don't narrate one](decisions/auto-mode.md#grounding-the-classifiers-verdict-cite-a-rule-dont-narrate-one)** — nothing the classifier says is trusted unless checked: a block must cite a real rule id, the tier comes from that id, an intent-based allow must quote the user verbatim.
60
- - **[Choosing the classifier model without leaking the session](decisions/auto-mode.md#choosing-the-classifier-model-without-leaking-the-session-to-another-provider)** — the classifier never silently changes provider; models are picked by cost + a per-provider default table, not id substrings.
61
- - **[Three defects the real OpenRouter catalog exposed](decisions/auto-mode.md#three-defects-the-real-openrouter-catalog-exposed)** — what a live gateway catalog broke in model selection, and the fixes.
62
- - **[Prompt caching, input size, and vendor containment on gateways](decisions/auto-mode.md#prompt-caching-input-size-and-vendor-containment-on-gateways)** — the stable prefix lives in the system prompt to cache; untrusted CLAUDE.md stays in the user message; sizing for Haiku's 4096-token prefix.
63
- - **[Hardening: the pi-automode review, and what came of it](decisions/auto-mode.md#auto-mode-hardening-the-pi-automode-review-and-what-came-of-it)** — the fail-closed posture: candidate chain, released pin on mid-session model death, deterministic floor for writes to the gate's own config.
64
-
65
- ## Code review
66
-
67
- Full context: [`decisions/code-review-remediation.md`](decisions/code-review-remediation.md).
68
-
69
- - **[Auto-mode gate hardening: four false "safe" verdicts closed](decisions/code-review-remediation.md#auto-mode-gate-hardening-four-false-safe-verdicts-closed-2026-08-10)** — bare redirects, read-only-command redirects, `git -C <outside>`, `sort -o`, and `Bash(sh -c *)` rules all reached "safe"/allow without their real effect being weighed; every fix converts a false "safe" into an escalation, keeping the pre-gate's never-deny contract.
70
- - **[Worktree vs. permission rules — preserve-original, not reorder](decisions/code-review-remediation.md#worktree-vs-permission-rules--preserve-original-not-reorder-2026-08-10)** — worktree's `cd`-wrapper defeated every Bash rule; the original command is preserved for rule-matching while the classifier keeps seeing the wrapped command. Reordering permissions ahead of worktree was rejected (it would move containment to the wrong dir).
71
- - **[Background subagents now get the holistic review](decisions/code-review-remediation.md#background-subagents-now-get-the-holistic-review-2026-08-10)** — the end-of-run "review the whole action sequence" checkpoint now fires for background/resident runs, delivered as a follow-up since there is no `tool_result` to attach to.
72
-
73
- ## Modes & keybindings
74
-
75
- Full context: [`decisions/modes.md`](decisions/modes.md).
76
-
77
- - **[`/effort`, and ultracode as a standing mode](decisions/modes.md#effort-and-ultracode-as-a-standing-mode)** — one "effort" dial; its last stop is `ultracode` = xhigh plus workflows armed every turn.
78
- - **[Aligning `/effort` with shift+tab](decisions/modes.md#aligning-effort-with-shifttab)** — why the slider mirrors shift+tab's model-filtered stops and dims the rest (pi reserves that key).
79
- - **[Permission-mode cycling on ctrl+q](decisions/modes.md#permission-mode-cycling-on-ctrlq-not-shifttab-ctrlm-or-altm)** — why the mode cycle is ctrl+q, not shift+tab / ctrl+m / alt+m.
80
- - **[Plan mode is file-based, like current Claude Code](decisions/modes.md#plan-mode-is-file-based-like-current-claude-code)** — plan mode writes a plan file under `~/.one-code/plans`; the matcher allows writes to that one file.
81
-
82
- ## Subagents & workflows
83
-
84
- Full context: [`decisions/subagents-workflows.md`](decisions/subagents-workflows.md).
85
-
86
- - **[Subagents: what matches Claude Code and what does not](decisions/subagents-workflows.md#subagents-what-matches-claude-code-and-what-does-not)** — the parity map for the subagent tool (fork, worktree isolation, background runs, two-way messaging).
87
- - **[Permission modes and subagents](decisions/subagents-workflows.md#permission-modes-and-subagents)** — how permission mode propagates into subagent sessions.
88
- - **[Subagent model selection, resolved in the parent](decisions/subagents-workflows.md#subagent-model-selection-resolved-in-the-parent-advertised-by-reminder)** — aliases stay in-provider, crossings are announced, defaults come from `CLAUDE_CODE_SUBAGENT_MODEL`/`subagentModel`, menu re-advertised every turn.
89
- - **[Workflow tool (ultracode orchestration)](decisions/subagents-workflows.md#workflow-tool-ultracode-orchestration)** — vm-sandboxed JS scripts fanning out to in-process subagents; background runs, journal resume, saved workflows.
90
-
91
- ## Model policy
92
-
93
- Full context: [`decisions/model-policy.md`](decisions/model-policy.md).
94
-
95
- - **[Shared role profiles](decisions/model-policy.md#shared-role-profiles-smaller-classifiers-and-delegated-workers-by-default)** — one same-provider/family table gives classifiers, delegated workers, and reader models a smaller default.
96
- - **[Upward cost pressure](decisions/model-policy.md#upward-cost-pressure-an-informational-warning-and-a-per-call-gate)** — an informational warning plus a per-call gate when a delegated model costs more than the session's.
97
-
98
- ## Memory & session state
99
-
100
- Full context: [`decisions/memory-state.md`](decisions/memory-state.md).
101
-
102
- - **[Memory: own file-based implementation](decisions/memory-state.md#memory-own-file-based-implementation)** — Claude Code's `~/.claude/projects/<slug>/memory/` layout keyed by git root; relevance-based mid-session recall is unreplicable client internals.
103
- - **[The memory dir is harness-designated working space](decisions/memory-state.md#the-memory-dir-is-harness-designated-working-space-like-the-plan-file)** — why writes there need no permission prompt, like the plan file.
104
- - **[Session scratchpad, Claude Code-style](decisions/memory-state.md#session-scratchpad-claude-code-style)** — a per-session temp dir: prompt section plus the allowed-writes rule.
105
- - **[Own state, borrowed config](decisions/memory-state.md#own-state-borrowed-config-claude-is-read-only-one-code-writes-to-one-code)** — `.claude` is read-only; One Code writes its own state to `~/.one-code` so the two never clobber each other.
106
-
107
- ## Compaction
108
-
109
- Full context: [`decisions/compaction.md`](decisions/compaction.md).
110
-
111
- - **[Compaction runs Claude Code's prompt, via session_before_compact](decisions/compaction.md#compaction-runs-claude-codes-prompt-via-session_before_compact)** — Claude Code's compaction prompt, `<summary>` extraction, an economical model, and the `CC_COMPACTION=0` pi fallback.
112
- - **[The cache miss, and the two-part fix](decisions/compaction.md#the-cache-miss-and-the-two-part-fix-2026-08-07)** — why the compaction call missed the prompt cache and the capture-based replay + reasoning-mirror + Anthropic beta-replay that fixed it, live-verified on three providers.
113
-
114
- ## MCP
115
-
116
- Full context: [`decisions/mcp.md`](decisions/mcp.md).
117
-
118
- - **[MCP — our own client on the official SDK](decisions/mcp.md#mcp-our-own-client-on-the-official-sdk)** — `.mcp.json` discovery, `mcp__server__tool` naming, resources.
119
- - **[MCP servers with unset credentials](decisions/mcp.md#mcp-servers-with-unset-credentials)** — how a server with missing credentials is handled rather than crashing discovery.
120
- - **[Background connect: session_start no longer blocks the prompt](decisions/mcp.md#background-connect-session_start-no-longer-blocks-the-prompt-2026-08-10)** — the awaited MCP handshake *was* the 4.9s startup (findings §15); now fire-and-forget in the interactive session (501ms measured), still awaited in one-shots and subagent children; SDK import lazified.
121
-
122
- ## LSP
123
-
124
- Full context: [`decisions/lsp.md`](decisions/lsp.md).
125
-
126
- - **[LSP: our own client, not a package](decisions/lsp.md#lsp-our-own-client-not-a-package)** — a zero-dep LSP client for post-edit diagnostics and `lsp_diagnostics`.
127
-
128
- ## Skills & plugins
129
-
130
- Full context: [`decisions/skills-plugins.md`](decisions/skills-plugins.md).
131
-
132
- - **[Skills and plugins](decisions/skills-plugins.md#skills-and-plugins)** — `~/.claude` + `.claude` skills/commands discovery, and Claude Code plugins contributing namespaced agents/skills/commands/MCP.
133
-
134
- ## TUI
135
-
136
- Full context: [`decisions/tui.md`](decisions/tui.md).
137
-
138
- - **[Compact tool rendering — the Claude Code transcript look](decisions/tui.md#compact-tool-rendering-the-claude-code-transcript-look-unreviewed)** *(unreviewed)* — `●`/`⎿` call+result lines with ctrl+o collapse for every One Code tool, compact `✳` notification headlines, boxless theme, dimmer thinking text.
139
- - **[Thinking collapsed by default, expandable with ctrl+t](decisions/tui.md#thinking-collapsed-by-default-expandable-with-ctrlt-unreviewed)** *(unreviewed)* — pi's own `hideThinkingBlock` defaulted on (only when the user never chose), `✻ Thinking…` label via `setHiddenThinkingLabel`; the write lands next session (settings cached at startup).
140
- - **[Built-in tools joined the ● language; banner cut to four lines](decisions/tui.md#built-in-tools-joined-the--language-banner-cut-to-four-lines-unreviewed)** *(unreviewed)* — `tool-style` wraps pi's read/write/edit/grep/find/ls renderers (`● Label(arg)` + base result under a `⎿` elbow, edit's diff preview kept); one curated hint line, sections as counts, warnings point at /lsp & /mcp.
141
- - **[Fullscreen (alt-screen) for Claude Code's clean exit — a setting, not extension code](decisions/tui.md#fullscreen-alt-screen-for-claude-codes-clean-exit-a-setting-not-extension-code-unreviewed)** *(unreviewed)* — CC's clean exit is the terminal alt-screen buffer; pi has it as `tuiMode: "fullscreen"`, but the renderer is a pi-core startup choice with no extension hook, so One Code recommends the setting rather than owning it.
142
- - **[Render memoization: components cache by width](decisions/tui.md#render-memoization-components-cache-by-width-2026-08-10)** — pi-tui renders every mounted component every frame (findings §15), so `linesComponent` memoizes by width (safe: state changes mint fresh components; `invalidate()` clears); `truncateLine` got a raw-length fast path. Rule: every custom renderer component must memoize `render(width)`.
143
-
144
- ## Hooks
145
-
146
- Full context: [`decisions/hooks.md`](decisions/hooks.md).
147
-
148
- - **[Hooks: own the mechanism, port the best of three references](decisions/hooks.md#hooks-own-the-mechanism-port-the-best-of-three-references)** — 8 events, the Claude Code stdin/envelope protocol, ask→block fail-closed, once-per-config consent, and running before worktree/permissions.
@@ -1,414 +0,0 @@
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
- }