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,148 @@
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-unreviewed)** *(unreviewed)* — npm `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); Homebrew tap delivers the app.
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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * ask-user extension — Claude Code's AskUserQuestion.
3
+ *
4
+ * Asks the user up to four multiple-choice questions, each optionally
5
+ * multi-select, always with a free-text escape hatch. Questions are presented
6
+ * one dialog at a time (pi's UI has no native multi-question widget), and the
7
+ * collected answers come back as one tool result.
8
+ *
9
+ * Replaces the community `pi-ask-user`, which asked one question per call and
10
+ * pulled in a second, conflicting TypeBox.
11
+ */
12
+
13
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
+ import { Type } from "typebox";
15
+ import { ccToolRenderers } from "../lib/tui-render.ts";
16
+ import {
17
+ type Answer,
18
+ buildChoices,
19
+ DONE_LABEL,
20
+ formatAnswers,
21
+ OTHER_LABEL,
22
+ type Question,
23
+ resolveSelection,
24
+ stripMark,
25
+ } from "./questions.ts";
26
+
27
+ const NON_INTERACTIVE =
28
+ "This session has no interactive UI, so the user cannot be shown a dialog. Ask your question in your reply instead and stop, or proceed under a stated assumption.";
29
+
30
+ async function askOne(question: Question, ctx: ExtensionContext): Promise<Answer | undefined> {
31
+ const selected: string[] = [];
32
+ const title = `${question.header}\n\n${question.question}`;
33
+
34
+ // Multi-select loops until the user is done; single-select runs once.
35
+ for (let round = 0; round < question.options.length + 1; round++) {
36
+ const choice = await ctx.ui.select(title, buildChoices(question, selected));
37
+ if (choice === undefined) return undefined; // cancelled
38
+
39
+ if (choice === DONE_LABEL) break;
40
+
41
+ if (choice === OTHER_LABEL) {
42
+ const typed = await ctx.ui.input(question.question, "Type your answer");
43
+ if (typed === undefined) return undefined;
44
+ return { question: question.question, header: question.header, selected: [typed], freeform: true };
45
+ }
46
+
47
+ const label = resolveSelection(stripMark(choice), question.options);
48
+ if (!label) break;
49
+
50
+ if (!question.multiSelect) {
51
+ return { question: question.question, header: question.header, selected: [label], freeform: false };
52
+ }
53
+
54
+ const existing = selected.indexOf(label);
55
+ if (existing >= 0) selected.splice(existing, 1);
56
+ else selected.push(label);
57
+ }
58
+
59
+ return { question: question.question, header: question.header, selected, freeform: false };
60
+ }
61
+
62
+ export default function askUserExtension(pi: ExtensionAPI) {
63
+ pi.registerTool({
64
+ name: "ask_user_question",
65
+ label: "Ask User",
66
+ ...ccToolRenderers<{ questions?: Array<{ question?: string }> }>("Ask User", {
67
+ title: (a) => a?.questions?.[0]?.question,
68
+ }),
69
+ description:
70
+ "Ask the user to choose between options when you are blocked on a decision that is genuinely theirs — one you cannot resolve from the request, the code, or a sensible default. Ask up to four questions in one call; each gets a free-text 'Other' choice automatically. Do not use it for choices with an obvious default or for facts you can verify yourself.",
71
+ promptSnippet: "Ask the user to decide between options when genuinely blocked",
72
+ parameters: Type.Object({
73
+ questions: Type.Array(
74
+ Type.Object({
75
+ question: Type.String({ description: "The question, ending in a question mark" }),
76
+ header: Type.String({ description: "Very short label for the question (a few words)" }),
77
+ multiSelect: Type.Optional(
78
+ Type.Boolean({ description: "Allow choosing several options (default false)" }),
79
+ ),
80
+ options: Type.Array(
81
+ Type.Object({
82
+ label: Type.String({ description: "Short choice text" }),
83
+ description: Type.Optional(Type.String({ description: "What choosing this means" })),
84
+ }),
85
+ { minItems: 2, maxItems: 4 },
86
+ ),
87
+ }),
88
+ { minItems: 1, maxItems: 4, description: "Questions to ask (1-4)" },
89
+ ),
90
+ }),
91
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
92
+ if (!ctx.hasUI) {
93
+ return { content: [{ type: "text", text: NON_INTERACTIVE }], details: {}, isError: true };
94
+ }
95
+
96
+ const answers: Answer[] = [];
97
+ for (const question of params.questions as Question[]) {
98
+ const answer = await askOne(question, ctx);
99
+ if (!answer) {
100
+ // Cancelling one question cancels the batch: the remaining answers
101
+ // would be meaningless without it.
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text:
107
+ answers.length > 0
108
+ ? `The user cancelled before finishing. Answers so far:\n\n${formatAnswers(answers)}`
109
+ : "The user cancelled without answering.",
110
+ },
111
+ ],
112
+ details: { answers, cancelled: true },
113
+ };
114
+ }
115
+ answers.push(answer);
116
+ }
117
+
118
+ return {
119
+ content: [{ type: "text", text: formatAnswers(answers) }],
120
+ details: { answers },
121
+ };
122
+ },
123
+ });
124
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Question and answer shaping for ask_user_question (pure).
3
+ */
4
+
5
+ export interface QuestionOption {
6
+ label: string;
7
+ description?: string;
8
+ }
9
+
10
+ export interface Question {
11
+ question: string;
12
+ header: string;
13
+ multiSelect?: boolean;
14
+ options: QuestionOption[];
15
+ }
16
+
17
+ export interface Answer {
18
+ question: string;
19
+ header: string;
20
+ selected: string[];
21
+ /** True when the user typed their own answer instead of picking. */
22
+ freeform: boolean;
23
+ }
24
+
25
+ export const OTHER_LABEL = "Other (type your own answer)";
26
+ export const DONE_LABEL = "Done selecting";
27
+
28
+ /** `label — description`, so the description is visible in a plain select list. */
29
+ export function formatOptionLabel(option: QuestionOption): string {
30
+ return option.description ? `${option.label} — ${option.description}` : option.label;
31
+ }
32
+
33
+ /** Maps a chosen display string back to the option label it came from. */
34
+ export function resolveSelection(display: string, options: QuestionOption[]): string | undefined {
35
+ return options.find((option) => formatOptionLabel(option) === display)?.label;
36
+ }
37
+
38
+ export function buildChoices(question: Question, alreadySelected: string[] = []): string[] {
39
+ const marked = question.options.map((option) => {
40
+ const label = formatOptionLabel(option);
41
+ return question.multiSelect && alreadySelected.includes(option.label) ? `✓ ${label}` : label;
42
+ });
43
+ const extras = question.multiSelect ? [DONE_LABEL, OTHER_LABEL] : [OTHER_LABEL];
44
+ return [...marked, ...extras];
45
+ }
46
+
47
+ /** Strips the multi-select tick so the choice can be matched against options. */
48
+ export function stripMark(display: string): string {
49
+ return display.startsWith("✓ ") ? display.slice(2) : display;
50
+ }
51
+
52
+ export function formatAnswers(answers: Answer[]): string {
53
+ if (answers.length === 0) return "The user did not answer.";
54
+ return answers
55
+ .map((answer) => {
56
+ const value = answer.selected.length > 0 ? answer.selected.join(", ") : "(no answer)";
57
+ return `${answer.question}\n→ ${value}${answer.freeform ? " (typed by the user)" : ""}`;
58
+ })
59
+ .join("\n\n");
60
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The child-action log auto mode reviews when a subagent finishes (pure).
3
+ *
4
+ * Claude Code classifies subagent work at three points: the task description
5
+ * before the child starts, each of the child's own actions, and the child's
6
+ * full action history when it returns. The first two fall out of the permission
7
+ * gate — the spawn is classified like any other tool call, and the child
8
+ * inherits auto mode so its actions are classified in its own session.
9
+ *
10
+ * The return check is different in kind: it looks at the *aggregate*. Each step
11
+ * of "read the deploy config, read a token, open a PR" can pass individually
12
+ * while the sequence is what matters. That is only visible once the child is
13
+ * done, which is what this records.
14
+ *
15
+ * The log deliberately holds names and short subjects rather than tool output:
16
+ * output is untrusted content, and feeding it to the classifier is the injection
17
+ * path auto mode exists to close.
18
+ */
19
+
20
+ export const SUBAGENT_ACTIONS_CHANNEL = "one-code:subagent-actions";
21
+
22
+ export interface ChildAction {
23
+ toolName: string;
24
+ /** Command or path, clipped. Never tool output. */
25
+ subject: string;
26
+ }
27
+
28
+ export interface SubagentActionsPayload {
29
+ /** The parent's tool call id, so a foreground review attaches to the right result. */
30
+ toolCallId: string;
31
+ actions: ChildAction[];
32
+ /**
33
+ * True for a background/resident run: the spawning call has already returned, so
34
+ * there is no pending tool_result to attach the holistic review to. The gate
35
+ * reviews on receipt and surfaces any concern as its own notification instead.
36
+ */
37
+ background?: boolean;
38
+ /** Agent name, used to label a background review. */
39
+ agentName?: string;
40
+ }
41
+
42
+ /** Per-action and total caps: a long-running child must not blow up the prompt. */
43
+ const SUBJECT_LIMIT = 160;
44
+ const MAX_ACTIONS = 60;
45
+
46
+ export function recordAction(log: ChildAction[], toolName: string, input: unknown): void {
47
+ if (log.length >= MAX_ACTIONS) return;
48
+ const args = (input ?? {}) as Record<string, unknown>;
49
+ const raw = args.command ?? args.file_path ?? args.path ?? args.url ?? args.pattern ?? "";
50
+ const subject = typeof raw === "string" ? raw : JSON.stringify(raw);
51
+ log.push({ toolName, subject: subject.slice(0, SUBJECT_LIMIT) });
52
+ }
53
+
54
+ /** Render the log for the classifier, collapsing repeats so 40 reads read as one line. */
55
+ export function renderActions(actions: ChildAction[]): string {
56
+ const lines: string[] = [];
57
+ let previous: string | undefined;
58
+ let repeats = 0;
59
+
60
+ const flush = () => {
61
+ if (previous === undefined) return;
62
+ lines.push(repeats > 1 ? `${previous} (×${repeats})` : previous);
63
+ };
64
+
65
+ for (const action of actions) {
66
+ const line = action.subject ? `${action.toolName}: ${action.subject}` : action.toolName;
67
+ if (line === previous) {
68
+ repeats++;
69
+ continue;
70
+ }
71
+ flush();
72
+ previous = line;
73
+ repeats = 1;
74
+ }
75
+ flush();
76
+
77
+ return lines.join("\n");
78
+ }