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,106 @@
1
+ # Code review remediation
2
+
3
+ The first whole-codebase pass of `/simplify` + `/code-review` (2026-08-10), and
4
+ what came of it. Run report-first over the entire source tree (the code had
5
+ never been reviewed). Seven correctness findings were confirmed by an
6
+ independent verify pass; ~22 quality cleanups were applied and the more invasive
7
+ refactors deferred.
8
+
9
+ ## Auto-mode gate hardening: four false "safe" verdicts closed (2026-08-10)
10
+
11
+ The shell pre-gate's contract is that it may **only ever conclude "safe", never
12
+ deny** — a gap costs one classifier call, never a bypass (see
13
+ [`auto-mode.md`](auto-mode.md)). The review found four commands reaching "safe"
14
+ without their real effect being weighed, plus one allow-rule class skipping the
15
+ classifier entirely:
16
+
17
+ - **A bare redirect with no command word** (`> /etc/hosts`) — the analyze loop
18
+ `continue`d on an empty command name before it ever looked at the segment's
19
+ redirects.
20
+ - **A read-only command's redirect** (`git log > outside.txt`) — the git/cd
21
+ branches `continue`d before the write-target check ran.
22
+ - **`git -C <dir>`** whose directory escapes the working tree — the global-flag
23
+ loop skipped `-C` and its value without validating the target.
24
+ - **`sort -o <file>`** — `sort` is fast-pathed as read-only, but `-o` writes an
25
+ arbitrary path.
26
+ - **`Bash(sh -c *)` / `python -c *` / `node -e *`** allow rules — `isBroadExecutionRule`
27
+ only recognised `run|exec|x` as a runner's escape hatch, so interpreter
28
+ inline-code flags matched and returned `allow` without ever reaching the
29
+ classifier.
30
+
31
+ **Fix keeps the contract.** Every change converts a false "safe" into an
32
+ escalation — none adds a deny path. The structural fix for the two redirect
33
+ gaps was to lift redirect checks to run **first for every segment** (a shared
34
+ `checkWriteTarget` closure), so no command-specific `continue` can skip a write
35
+ target — deeper than patching the bare-redirect case alone. `git -C`/`--git-dir`/
36
+ `--work-tree` now escalate when their directory leaves the working tree; `sort -o`
37
+ escalates as a write; interpreter `-c`/`-e` flags are treated as broad (which, if
38
+ it over-flags a runner's unrelated `-c`, only costs a classifier call — the safe
39
+ direction). All five are covered by regression tests
40
+ (`test/unit/auto-mode-shell.test.ts`, `test/unit/matcher.test.ts`).
41
+
42
+ **Live-verified both directions** (2026-08-10, `openai-codex/gpt-5.6-luna` in
43
+ `--permission-mode auto`, `logDecisions` on): plainly-safe commands (`cat`,
44
+ `git status`) resolved on the deterministic pre-gate (`source: pre-gate`), while
45
+ the two newly-covered commands (`sort -o …`, `git -C /etc status`) routed to the
46
+ classifier (`source: classifier`) — i.e. escalated rather than fast-pathed to
47
+ "safe", the fix's whole point. The classifier then allowed both (an in-cwd sort
48
+ and a read-only status are not dangerous); the fix owns the escalation, the
49
+ classifier owns the verdict.
50
+
51
+ ## Worktree vs. permission rules — preserve-original, not reorder (2026-08-10)
52
+
53
+ A worktree session rewrites bash commands into `cd '<wt>' && (…)` on the
54
+ `tool_call` hook, and worktree loads before permissions, so the gate saw the
55
+ wrapped command — defeating every configured `Bash(...)` allow/ask/deny rule for
56
+ the whole session.
57
+
58
+ **Chosen: preserve-original.** worktree stashes the model's pre-wrap command on
59
+ `event.input` (`ORIGINAL_COMMAND_KEY`); only rule-matching reads it, while the
60
+ classifier and safety floor keep seeing the wrapped command (whose `cd` makes
61
+ containment resolve inside the worktree).
62
+
63
+ **Rejected: reorder permissions ahead of worktree** — the mechanism the hooks
64
+ decision used for "matchers see the model's original call"
65
+ ([`hooks.md`](hooks.md)). It would have shifted worktree-session protected-path
66
+ containment from the actual write location (the worktree) back to the original
67
+ checkout, a semantic change worse than the bug (which fails toward *more*
68
+ prompting/classifier review — the safe direction).
69
+
70
+ ## Background subagents now get the holistic review (2026-08-10)
71
+
72
+ Auto mode's third subagent checkpoint — reviewing a finished child's action
73
+ *sequence* as a whole ("read the deploy config, read a token, open a PR") — only
74
+ fired for foreground runs, because it attaches to the spawning call's
75
+ `tool_result`, which a background/resident run has already returned.
76
+
77
+ **Chosen: review-on-receipt.** Background turns now emit their action sequence;
78
+ the gate reviews it on receipt (shared `reviewCompletedRun`, using the last live
79
+ context) and surfaces any concern as its own follow-up notification, alongside
80
+ the completion report. The foreground path is unchanged; both share one helper.
81
+
82
+ ## Next-turn reminders no longer lost without a user message (2026-08-10)
83
+
84
+ `reminderQueue.drain()` cleared the next-turn queue unconditionally, but
85
+ `injectReminders` is a no-op when there is no user message to attach to — so a
86
+ `context` event on a user-message-less array silently dropped every pending
87
+ reminder. The gate now only drains once a user message exists, honoring the
88
+ module's own documented re-enqueue contract.
89
+
90
+ ## Quality cleanups, and what was deferred
91
+
92
+ ~22 reuse/dedup/dead-code cleanups were applied; their rationale is the drift
93
+ they remove. Highlights: shared `lib/` helpers (`isWritingTool` hoisted to the
94
+ lower layer, `projectMemoryDir`, `restoreLatestDetails`, `perCwd`,
95
+ `safeThemePaint`, `tryReadFile`, notification-format constants); dead code
96
+ removed (workflow `AgentCallMeta`/`tokenUsage`/`ReplayCursor.totals()`, the
97
+ misleading `readCapped` size branch); two efficiency wins (MCP connect via
98
+ `Promise.allSettled`, `findRoleProfileModel` no longer double-computes
99
+ containment on the classifier hot path). A behavior fix surfaced by the review:
100
+ **`CLAUDE_CONFIG_DIR` is now honored** for CLAUDE.md/skills/commands discovery
101
+ (`claude-context`, `claude-compat`), matching real Claude Code.
102
+
103
+ **Deferred** as focused follow-ups — internal structure or low value, zero
104
+ correctness impact, high edit-risk to batch here: the `subagents/index.ts`
105
+ `execute()` split and its sibling helpers, background-worktree parallelization,
106
+ the notify-envelope consolidation across five dirs, and a few minor dedups.
@@ -0,0 +1,140 @@
1
+ # Compaction
2
+
3
+ Part of [Decisions](../decisions.md).
4
+
5
+ ## Compaction runs Claude Code's prompt, via session_before_compact
6
+
7
+ pi hardcodes its own summarization prompt (settings only expose
8
+ enabled/reserveTokens/keepRecentTokens), but `session_before_compact` lets an
9
+ extension supply the whole `CompactionResult` — so One Code now compacts the
10
+ way Claude Code does. Three design points:
11
+
12
+ - **The prompt**: appended as user text after the conversation; the model
13
+ answers `<analysis>` + `<summary>`, and only the `<summary>` content
14
+ survives. `extractSummary` matches tags at *line starts* and spans to the
15
+ last closing tag; an untagged reply is used whole minus any analysis block —
16
+ losing a compaction to a formatting slip is worse than untidy text. Empty
17
+ means failure. Trap, caught live: the first run's analysis said "wrapped in
18
+ `<analysis>` and `<summary>` tags", a loose regex anchored on that inline
19
+ mention, and the stored summary began with the tail of the analysis — prose
20
+ *about* the format is indistinguishable from the format unless tags are
21
+ line-anchored.
22
+ - **The model — the session model, for the cache**: Claude Code keeps the
23
+ session's system prompt, tool definitions, and message prefix intact,
24
+ appends the instruction, and runs the call on the session's own model. That
25
+ is not thrift on the model choice — it is thrift on the *tokens*: the
26
+ summarization call replays the
27
+ already-cached prefix, so it is mostly cache reads. The extension does the
28
+ same: session model, `ctx.getSystemPrompt()`, the active tool definitions in
29
+ active order (a mismatch there costs cache hits, never correctness), and — the
30
+ key to actually hitting the cache — the session's *last real outgoing
31
+ messages*, captured from the `context` event (see the cache section below)
32
+ rather than reconstructed from entries, with the instruction as a
33
+ final user message: one `<system-reminder>` holding the trigger notice
34
+ ("the user has triggered a /compact command" vs "the conversation context
35
+ window is running out") directly above the verbatim instruction, preceded by
36
+ any `/compact <instructions>` as a "## Compact Instructions" reminder. The
37
+ stored summary gets a continuation preamble baked in ("This session is being
38
+ continued from a previous conversation that ran out of context…") plus a
39
+ pointer to the session JSONL with a NEVER-read-it-whole warning — details
40
+ the summary lost stay grepable instead of gone, and `read`/`grep` are
41
+ safe-tier tools, so following the pointer never prompts, auto mode included.
42
+ All inside pi's own hardcoded `<summary>` frame. A previous compaction's summary lives
43
+ in the *live* context but is excluded from `messagesToSummarize`
44
+ (prepareCompaction starts after that entry, handing the text over
45
+ separately) — it is reattached as the leading `compactionSummary` message,
46
+ the exact shape the live context carries, so convertToLlm renders the same
47
+ bytes the cached prefix holds and re-compactions stay cache reads too.
48
+ - **Reasoning mirrors the session's thinking level.** The call sends
49
+ `reasoning: ctx.thinkingLevel` (with `off`→none). This began as a deliberate
50
+ omission — `reasoning` fails closed on providers pi's compat data mispredicts,
51
+ the classifier's documented trade — but that reasoning does not apply here and
52
+ omitting it turned out to be the last thing keeping the cache cold (see below).
53
+ Unlike the classifier, compaction runs the *session's own* model, already
54
+ proven to accept the level, and `streamSimple` clamps an unsupported level down
55
+ rather than erroring — so mirroring never fails closed. It also restores the
56
+ extended-thinking behavior for free.
57
+
58
+ All three reasons (manual, threshold, overflow) take this path; any failure —
59
+ no auth, timeout, empty summary — returns nothing and pi's own compaction
60
+ serves: a different summary style, never a broken compaction. `CC_COMPACTION=0`
61
+ opts out entirely.
62
+
63
+ Verified live on a Luna session with the final shape: two /compact runs in one
64
+ session (48,356 then 30,838 tokens) both stored fromHook entries — preamble
65
+ first, transcript pointer with the warning, all nine sections, no tag bleed
66
+ (inline backtick *mentions* of the tags in the second summary are content, and
67
+ line-anchored extraction correctly ignored them).
68
+
69
+ ## The cache miss, and the two-part fix (2026-08-07)
70
+
71
+ For a while the summarization call read `cacheRead: 0` on openai-codex even
72
+ though the surrounding session sat at ~78–96% cache hits. Passing
73
+ `options.sessionId` (pi's prompt_cache_key / session-affinity carrier, which
74
+ completeSimple otherwise omits) was necessary but not sufficient. Two things
75
+ were wrong, both now fixed:
76
+
77
+ 1. **Reconstruction diverged from the cached prefix at message one.** The
78
+ request was rebuilt from session *entries*, but per-request `<system-reminder>`
79
+ injections (the memory index, the every-turn reminders) are added at request
80
+ time and never become entries — so no reconstruction can reproduce them. The
81
+ fix is *capture, not reconstruct*: a `context`-event listener stashes the
82
+ exact `AgentMessage[]` the session last handed the provider (a held reference —
83
+ pi `structuredClone`s the array per turn, and compaction is the last `context`
84
+ handler, so it is the array actually sent). pi's agent loop builds a request as
85
+ `transformContext` then `convertToLlm`; running the *same* `convertToLlm` on
86
+ the captured array makes the message prefix byte-identical, and appending only
87
+ the instruction leaves that whole prefix a cache read. The entry path survives
88
+ as a fallback for when no turn has run yet (e.g. `/compact` first thing in a
89
+ resumed session).
90
+
91
+ 2. **The reasoning config is part of the cache identity.** Even with a
92
+ byte-identical prefix, `prompt_cache_key`, tools, and system prompt, the
93
+ request *still* missed — because it omitted `reasoning` while the session's
94
+ normal requests sent it. On the Responses API that is a different request
95
+ configuration and the cached prefix is not reused. Found by dumping the last
96
+ normal payload and the compaction payload and diffing: everything matched
97
+ except `reasoning`. Fixed by mirroring the session thinking level (above).
98
+
99
+ Measured after the fix (thinking high, ~48–56k-token context):
100
+
101
+ | provider / model | before | after |
102
+ |---|---|---|
103
+ | openai-codex `gpt-5.6-luna` | `input 49776, cacheRead 0` | `input 2094, cacheRead 47616` |
104
+ | anthropic `claude-haiku-4-5` | — | `input 10, cacheRead 13042, cacheWrite 44287` |
105
+ | openrouter `deepseek/deepseek-v4-flash` | — | `input 1402, cacheRead 51200` |
106
+
107
+ openai-codex and deepseek get near-total prefix reuse immediately (automatic
108
+ prefix caching keyed by `prompt_cache_key`, no explicit breakpoints). Anthropic
109
+ needed a third fix. At first it reused only ~13k (≈ system + tools) and re-cached
110
+ the ~44k of history. Two dead-end explanations were ruled out by experiment
111
+ before the real one: it is **not** `cache_control` breakpoint placement (adding
112
+ an explicit anchor on the history's last block changed nothing — still 13k) and
113
+ **not** the reasoning param (identical in both). The confirmed cause, found by
114
+ diffing the compaction wire payload against the session's last normal payload and
115
+ by A/B-ing `CC_CLEAR_THINKING`:
116
+
117
+ - The `context-management` extension (default-on for first-party Anthropic) puts
118
+ `context_management: { edits: [clear_thinking_20251015] }` on every agent-loop
119
+ request plus the `context-management-2025-06-27` beta. So the session's cached
120
+ **message** prefix is stored in the thinking-cleared form.
121
+ - `completeSimple` bypasses `before_provider_headers`/`before_provider_request`,
122
+ so the compaction request omitted both — sending full thinking blocks with no
123
+ `clear_thinking`. On Anthropic that mismatch invalidates the *message* cache
124
+ (system+tools are unaffected, hence they still read), so the whole history
125
+ re-caches: cacheRead ~13k, cacheWrite ~44k.
126
+ - Fix: when `clearThinkingEnabled(...)` is true, the compaction call replays the
127
+ same beta header and `clear_thinking` body edit (reusing context-management's
128
+ exported helpers), so its message prefix matches the session's cache. Measured
129
+ after: `input 10, cacheRead 56032, cacheWrite 1460` on `claude-haiku-4-5:high`
130
+ — near-total reuse, matching the other providers.
131
+
132
+ So the compaction request must be a faithful replay of the session's request in
133
+ *three* dimensions the naive shape got wrong: the request-time reminder
134
+ injections (capture, not reconstruct), the reasoning config (Responses-API cache
135
+ identity), and — on Anthropic — the context-management beta + `clear_thinking`
136
+ edit. All three providers now produce valid fromHook summaries with near-total
137
+ cache reuse, and none fail-closed. (Any provider-specific request mutation an
138
+ extension adds via `before_provider_*` is invisible to `completeSimple`; a future
139
+ compaction refactor that routed through the same request builder would get these
140
+ for free — a point for the upstream pi PR.)
@@ -0,0 +1,133 @@
1
+ # Distribution & dependencies
2
+
3
+ Part of [Decisions](../decisions.md).
4
+
5
+ ## Two artifacts from one codebase: the app and the package (2026-08-10, unreviewed)
6
+
7
+ One Code ships two npm packages built from this single repo:
8
+
9
+ | npm name | What it is | pi relationship |
10
+ |---|---|---|
11
+ | **`one-code`** (`app/`) | the bundled app — a `one-code` bin | **exact-pins** pi (`0.84.1`) as a dependency |
12
+ | **`one-code-extension`** (repo root) | the pi package | **peerDependency** range; runs on the user's own pi |
13
+
14
+ The split exists because the two installs need opposite dependency stances
15
+ (reproducible pin vs. ride-the-user's-pi), and nothing else: the app is a thin
16
+ launcher over the same extension set. `one-code-extension` was chosen over
17
+ `one-code-pi`, which reads as "one-code *with* pi bundled" — the opposite of
18
+ what it is. A Homebrew tap (`brew install IsuruMaduranga/one-code/one-code`)
19
+ delivers the same app and solves the Node ≥22.19 requirement via
20
+ `depends_on "node"`; a single compiled binary is not viable (pi-tui ships
21
+ per-platform native modules).
22
+
23
+ **How the app loads the extensions — package registration, not
24
+ `extensionFactories`.** The bin sets `PI_CODING_AGENT_DIR=~/.one-code/agent`
25
+ (pi's env override for its config dir) and idempotently registers the
26
+ `one-code-extension` directory from its own `node_modules` as a local-path
27
+ package in the isolated `settings.json`, then calls pi's public `main(argv)`.
28
+ pi resolves local-path package sources **in place** (no copying), so the
29
+ manifest drives everything exactly as a `pi install` would: extensions in
30
+ their load-bearing order, `pi.themes`, and the bundled agents (which resolve
31
+ relative to the extension files). The alternative — passing our factories via
32
+ `main(argv, { extensionFactories })` — was prototyped and rejected: it loads
33
+ no themes without extra plumbing, and our extensions are TypeScript that only
34
+ pi's jiti loader handles; a plain Node bin cannot import them. Verified live
35
+ from packed tarballs: themes, agents, permission prompts, session resume, and
36
+ full state isolation (`~/.one-code` created, `~/.pi` untouched, and a stale
37
+ `packages` entry in a user's pi settings cannot double-load us because the
38
+ isolated dir never reads them).
39
+
40
+ **Version drift is guarded at runtime, not by the peer range.** pi renames
41
+ settings/flags between minors (0.83→0.84: `uiMode`→`tuiMode`, and 0.84 rejects
42
+ the old flag), so the package variant warns at startup when the running pi is
43
+ outside the tested range (`extensions/lib/pi-version.ts`, wired in branding's
44
+ `session_start`; fail-silent on unparseable versions). The app never warns —
45
+ its pin is inside the range by construction. The peer range stays loose
46
+ (`>=0.83.0`); a hard range would make `pi install` fail instead of warn.
47
+
48
+ **Branding: a surgical stdout rewrite in the bin.** pi prints its own command
49
+ name from `APP_NAME` (fork-gated, resolved from pi's *own* package.json) in
50
+ two plain-stdout places: the post-TUI resume hint and `--help`. Under
51
+ isolation the resume hint `pi --session <id>` is not just mis-branded but
52
+ broken (stock pi cannot see `~/.one-code` sessions), so the bin rewrites
53
+ ` pi --session ` → ` one-code --session ` on that line (matched on the label,
54
+ which carries ANSI dim codes) and standalone-word `pi` in help output only.
55
+ Nothing else is touched — TUI escape streams pass through byte-identical.
56
+ Verified: the rewritten resume command actually resumes the session. The real
57
+ fix is an upstream `PI_APP_NAME` env override PR; the rewrite shrinks then.
58
+
59
+ **Updates: pi's check suppressed, ours added.** The bin sets
60
+ `PI_SKIP_VERSION_CHECK=1` (pi's endpoint is hardcoded to its own registry) and
61
+ injects one inline JS extension (`app/update-check.mjs`) via
62
+ `extensionFactories`: a non-blocking, fail-silent fetch of
63
+ `registry.npmjs.org/one-code/latest` on session start, notifying with an
64
+ install-aware hint (`brew upgrade one-code` when the bin resolves under a
65
+ Homebrew prefix, else `npm i -g one-code`). pi's semver helpers are not
66
+ exported from the package root, so the dotted-numeric compare lives in the
67
+ app. `one-code --version` reports the app version with the pinned pi in
68
+ parentheses. Both paths live-verified (drift warning and update notice each
69
+ fired against a real TUI).
70
+
71
+ Registering the package also seeds first-run settings (`theme: "one-code"`,
72
+ `quietStartup: true`), which doubles as skipping pi's stock first-time theme
73
+ picker — the branded first run. Three hardcoded `~/.pi` paths found during
74
+ this work (quiet-startup detection, the skills listing, the Anthropic-OAuth
75
+ check) now resolve through pi's `getAgentDir()`, which honours the isolation
76
+ env var.
77
+
78
+ ## Distribution: pi package, not a wrapper binary
79
+
80
+ *(Partially superseded 2026-08-10 by the two-artifact model above: a wrapper
81
+ **app** now exists alongside the package. What still stands: never fork, and
82
+ the package variant remains first-class.)*
83
+
84
+ pi's `piConfig` rebranding (app name, config dir) resolves from pi's *own*
85
+ installed `package.json`, so a dependent package cannot rebrand it. Shipping as
86
+ a pi package (`pi install npm:pi-claude-code`) costs nothing extra and keeps
87
+ upstream pi upgrades a version bump away. All Claude-Code-shaped paths
88
+ (`.claude/settings.json`, `.claude/commands`, `.claude/agents`) are discovered
89
+ by our own code rather than by changing pi's `.pi` namespace.
90
+
91
+ ## Community packages: adopted where they work
92
+
93
+ Per project directive, prefer ecosystem packages over new code.
94
+
95
+ - **Adopted:** `pi-ask-user` for the AskUserQuestion role (option lists,
96
+ multi-select, freeform, headless fallback). Bundled as a dependency and
97
+ re-exported from `extensions/ask-user`, so users get it automatically.
98
+ - **Rejected: `pi-subagents` (0.40.0).** Its child processes were SIGKILLed by
99
+ the parent ~29 ms after spawn in this environment (macOS, Node 26,
100
+ pi 0.83.0), in both print and RPC mode, with our extensions absent and the
101
+ same failure when spawning through an explicit `PI_SUBAGENT_PI_BINARY`
102
+ wrapper. The identical child command line runs fine standalone, so the fault
103
+ is in the package's parent-side lifecycle management, not our integration.
104
+ `extensions/subagents` is therefore our own implementation, modeled on pi's
105
+ official `examples/extensions/subagent`: spawn `pi --mode json -p`, parse the
106
+ event stream, return the child's final text. Worth re-evaluating the package
107
+ on a future release.
108
+
109
+ ## Publish readiness (Phase 7)
110
+
111
+ `npm pack` produces a 69 kB / 47-file tarball: `extensions/`, `agents/`, `types/`,
112
+ README, LICENSE and `docs/decisions.md`. Every path in the `pi.extensions`
113
+ manifest is checked to be present in the tarball.
114
+
115
+ **A path install does not fetch dependencies.** Verified by installing the packed
116
+ copy from a directory: pi registered it, then every extension importing a
117
+ dependency failed to load (`Cannot find module '@modelcontextprotocol/sdk/...'`).
118
+ `pi install npm:<name>` is fine, because npm resolves the dependency tree — so
119
+ the README tells anyone installing from a checkout to run `npm install` first.
120
+
121
+ The published path was verified by simulating its layout: `npm install <tarball>`
122
+ into a scratch project (348 packages resolved), then `pi install
123
+ <scratch>/node_modules/pi-claude-code`. A fresh run listed all bundled and plugin
124
+ agents correctly.
125
+
126
+ **Portability**: `web_fetch` through `tool_search` was exercised on
127
+ `gpt-5.4`, `gpt-5.5` and `gpt-5.6-terra` — a pre-native-deferred-loading model,
128
+ a native one, and a newer family — all correct. Anthropic's `defer_loading` path
129
+ and the non-native fallback remain unexercised for lack of a credential.
130
+
131
+ **Declined:** the CC-style UI packages (`pi-cc-header`, `pi-cc-extensions`). They
132
+ are cosmetic and would add a dependency for appearance only, which fails our own
133
+ adopt-vs-build checklist.
@@ -0,0 +1,64 @@
1
+ # Hooks
2
+
3
+ Part of [Decisions](../decisions.md).
4
+
5
+ ## Hooks: own the mechanism, port the best of three references
6
+
7
+ Claude Code command hooks were the biggest remaining compat gap — real setups
8
+ carry `PreToolUse`/`PostToolUse`/`UserPromptSubmit` hooks in settings.json and
9
+ plugins ship `hooks/hooks.json`. Three MIT community implementations were
10
+ studied before building (`extensions/hooks/`); none survived as a dependency,
11
+ all three contributed:
12
+
13
+ - **`pi-code`** (ilovepixelart): the executor hardening — absolute `/bin/sh`,
14
+ `detached: true` + SIGKILL to the negative pid so grandchildren holding the
15
+ stdio pipes die with the timeout, utf8 stream decoding, 1MB output caps,
16
+ `CLAUDE_PROJECT_DIR`, the Node timer-overflow clamp — plus reading real
17
+ `.claude/settings.json` and the deliberate **ask→block** fail-closed mapping
18
+ (pi's tool_call is allow-or-block; ask-as-allow is the unsafe reading).
19
+ - **`@hsingjui/pi-hooks`**: the CC JSON envelope parsing, hidden-custom-message
20
+ context injection, and the Stop-hook loop re-trigger
21
+ (`deliverAs: "followUp", triggerTurn: true` + a `stop_hook_active`
22
+ re-entrancy flag). Not adopted as a dependency because it reads
23
+ `.pi/settings.json` rather than `.claude`, and its bugs were ported *around*:
24
+ snake_case event aliases, `if` conditions silently disabling non-tool hooks,
25
+ compact trigger hardcoded to "manual".
26
+ - **`pi-fairy-tales`**: the mtime-keyed config reload (both others only reload
27
+ at session_start).
28
+
29
+ Decisions that shaped the wiring:
30
+
31
+ - **Position in `pi.extensions` is the mechanism** for CC's "hook updatedInput
32
+ applies before permission evaluation": hooks sits right after
33
+ system-reminder, ahead of worktree (which rewrites `input.command` into a
34
+ `cd … && (…)` block — matchers must see the model's original call),
35
+ file-tracker, and permissions. pi passes the same `event.input` object to
36
+ every handler, so an in-place mutation is exactly what the gate, safety
37
+ floor, and classifier evaluate.
38
+ - **A hook can never pre-approve**: `permissionDecision: "allow"` is parsed and
39
+ ignored. Deny direction is honoured everywhere; timeouts fail closed for
40
+ PreToolUse/UserPromptSubmit only.
41
+ - **Trust**: project/local hooks are arbitrary code execution, so they run
42
+ after a once-per-config consent (sha256 of the canonicalised project hook
43
+ config, persisted in `~/.one-code/hooks/project-approvals.json`; a decline
44
+ sticks for the session only). pi's own project-trust store was deliberately
45
+ not reused — it never triggers for repos that have only `.claude/*` files.
46
+ Plugin hooks are user scope: installing the plugin was the consent.
47
+ - **No loader exemption** (user decision): a hook may block `tool_search`/
48
+ `skill`/`structured_output`, full CC fidelity — see the foot-gun note in
49
+ findings.
50
+ - **additionalContext** is a hidden `pi.sendMessage` custom message (persisted,
51
+ survives resume), not a reminder-queue entry (transient) and not deferred to
52
+ `before_agent_start` (fires once per outer turn — mid-loop context would
53
+ arrive a turn late). Mid-loop events use `deliverAs: "steer"`; prompt-time
54
+ context is stashed and injected at `before_agent_start` so it lands on the
55
+ same turn as the prompt.
56
+ - **Notification is unsupported**: pi has no event carrying that concept;
57
+ documented rather than stubbed. `http`/`prompt`/`agent` hook types are
58
+ skipped with a diagnostic.
59
+
60
+ Verified live (rpc e2e, no --dangerously-skip-permissions): consent prompt
61
+ shown once; approved run executes the hook, blocks the bash call with the
62
+ hook's stderr as reason, and no permission prompt fires for it (the hook
63
+ short-circuits ahead of the gate); declined run never executes the hook and
64
+ the same call flows to the ordinary permission prompt.
@@ -0,0 +1,58 @@
1
+ # LSP
2
+
3
+ Part of [Decisions](../decisions.md).
4
+
5
+ ## LSP: our own client, not a package
6
+
7
+ Originally adopted **`pi-lsp-extension`** (MIT). It worked, but proved fragile in
8
+ four ways, all of which needed workarounds on our side:
9
+
10
+ 1. A stale transitive import (`vscode-languageserver-protocol/node.js`, an export
11
+ removed in 3.18) had to be pinned with an npm `override`.
12
+ 2. Its language servers kept the process alive after the final turn, so `pi -p`
13
+ **never exited**. We had to gate the whole extension off in one-shot runs,
14
+ which meant no diagnostics there at all.
15
+ 3. Its auto-append hook only fired when a server was *already* running, and
16
+ servers started lazily on the first file **read** — so an edit as the first
17
+ action of a session silently produced nothing.
18
+ 4. Against TypeScript 7 (whose native compiler dropped `lib/tsserver.js`) the
19
+ server failed and diagnostics **silently** degraded to tree-sitter syntax
20
+ checks, with no signal that type checking was gone.
21
+
22
+ It also carried far more than we need: ~7000 lines and twelve tools, including
23
+ tree-sitter search, a workspace index, completions, and code actions. Claude Code
24
+ has **no** LSP tools — it only receives diagnostics after edits — so eleven of
25
+ those were beyond parity, and their schemas would crowd the prompt.
26
+
27
+ `extensions/lsp/` is now our own, ~600 lines with **zero dependencies**:
28
+ `protocol.ts` (Content-Length framing), `servers.ts` (language/server/root
29
+ detection, table taken from pi-lsp-extension), `client.ts` (spawn, initialize,
30
+ document sync, diagnostics), `format.ts` (rendering), `index.ts` (wiring).
31
+ Two behaviors only: error diagnostics appended to `edit`/`write` results, and a
32
+ deferred `lsp_diagnostics` tool. Adding hover or definition later is one
33
+ `client.request(...)` each.
34
+
35
+ Each fragility above is addressed by design:
36
+
37
+ - **No dependencies**, so no transitive protocol conflicts to pin.
38
+ - Child process and stdio handles are `unref`'d and shut down on
39
+ `session_shutdown`, so one-shot runs exit — **the argv gate is gone and
40
+ diagnostics now work in `-p` too**.
41
+ - Content is pushed with `didOpen`/`didChange` before every read, so an edit as
42
+ the session's first action is covered, and answers reflect current content
43
+ rather than whatever the server happened to cache.
44
+ - `getDiagnostics` awaits the server's next publish for that document (timeout
45
+ plus last-known fallback) instead of sleeping a fixed interval.
46
+ - A TypeScript preflight detects the missing `tsserver.js` and returns a
47
+ sentence explaining it, rather than degrading silently.
48
+
49
+ Diagnostics are still delivered in the tool result rather than through our
50
+ system-reminder queue: that is where the model needs them, and it costs one
51
+ delivery instead of two.
52
+
53
+ Verified end-to-end in one-shot mode: an edit introducing a type error appended
54
+ `src/index.ts:5:30 error: Argument of type 'number' is not assignable to
55
+ parameter of type 'string'. (2345) [typescript]` — matching `tsc` — the run
56
+ exited in 16s, no language-server processes leaked, the deferred tool loaded and
57
+ answered on demand, and a TypeScript 7 project returned the preflight
58
+ explanation.
@@ -0,0 +1,104 @@
1
+ # MCP
2
+
3
+ Part of [Decisions](../decisions.md).
4
+
5
+ ## MCP — our own client on the official SDK
6
+
7
+ pi ships no MCP support by design, so this is `extensions/mcp/` (~500 lines) on
8
+ top of `@modelcontextprotocol/sdk`. That split is the point: the SDK carries the
9
+ protocol (a genuine standard, worth depending on), and we own the thin part —
10
+ config discovery, namespacing, lifecycle, deferral.
11
+
12
+ Both community options were rejected on the checklist:
13
+
14
+ - **`pi-mcp-extension`** still peers on the pre-rename `@mariozechner/pi-*`
15
+ packages, i.e. it has not been touched since pi moved to Earendil.
16
+ - **`pi-mcp-adapter`** is 19,284 lines with twelve dependencies including a
17
+ native keyring binding, a browser launcher, and a TOML parser — the same
18
+ surface-far-past-the-need profile as the packages already removed.
19
+
20
+ Design notes:
21
+
22
+ - **Config is Claude Code's format**, merged lowest-to-highest from
23
+ `~/.claude.json`, then `.mcp.json` walked from cwd up to the repo root
24
+ (so a monorepo root config applies in subdirectories), then
25
+ `.claude/settings.local.json`. `$VAR`/`${VAR}` are expanded in commands, args,
26
+ env, urls and headers. Both stdio (`command`) and remote (`url`) servers work;
27
+ a `disabled: true` entry *removes* a server inherited from a broader scope.
28
+ - **Tools are namespaced `mcp__<server>__<tool>`**, sanitised to legal tool-name
29
+ characters, so users' existing Claude Code permission rules match. Verified:
30
+ a `deny: ["mcp__fixture__add_numbers"]` rule blocked the call.
31
+ - **Every MCP tool is deferred.** A few servers can contribute dozens of tools;
32
+ putting those schemas in the prompt is what `tool_search` exists to prevent.
33
+ - **JSON Schema is converted to TypeBox explicitly** (`schema.ts`) rather than
34
+ passing raw schemas through and hoping TypeBox's runtime checks accept them.
35
+ Unrecognised constructs degrade to permissive types, so an unusual server
36
+ schema means arguments pass through rather than every call being rejected.
37
+ - Servers connect once at `session_start` (their tool lists are needed before
38
+ anything can be registered), in parallel, each with a timeout, and close on
39
+ `session_shutdown`. A failing server is reported by name and does not affect
40
+ the others.
41
+
42
+ One bug worth remembering, caught only by the end-to-end test: `resources/read`
43
+ returns resource *contents* (`{uri, mimeType, text|blob}`) — **not** the typed
44
+ content blocks that `tools/call` returns. Running them through the content-block
45
+ formatter yielded `[undefined content]`. They now have their own formatter, which
46
+ also summarises binary blobs instead of dumping base64 into the context.
47
+
48
+ Verified against a fixture stdio server: `.mcp.json` discovered, tool loaded via
49
+ `tool_search` and called (`mcp__fixture__add_numbers` → `SUM_IS_42`), resources
50
+ listed and read, the deny rule enforced, and the run exited in 16s with no
51
+ lingering processes.
52
+
53
+ ## MCP servers with unset credentials
54
+
55
+ Found from a real run: the `github` plugin's config is
56
+ `Authorization: "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"`, and with the variable
57
+ unset our expansion produced the literal `"Bearer "`. The server then failed with
58
+ "Authorization header is badly formatted" — a confusing protocol error for what is
59
+ really a missing token.
60
+
61
+ `missingEnvVars()` now reports variables a config references but that are not set
62
+ (across command, args, env, url and headers), and such servers are **skipped with
63
+ an explanatory message** rather than connected with an empty credential:
64
+ `MCP server "github" failed: not started — GITHUB_PERSONAL_ACCESS_TOKEN not set in
65
+ the environment`.
66
+
67
+ ## Background connect: session_start no longer blocks the prompt (2026-08-10)
68
+
69
+ The connect ran as an awaited `session_start` handler — and pi awaits every
70
+ extension's `session_start` serially before the interactive input loop opens
71
+ (findings §15). With two remote HTTP servers configured (user-level deepwiki
72
+ plus the context7 plugin), that made the MCP handshake **the entire perceived
73
+ startup**: 4.9s to a usable prompt measured with `PI_STARTUP_BENCHMARK`, of
74
+ which everything-but-mcp was 0.24s.
75
+
76
+ **Decision:** in the interactive main session, `connectAll` runs in the
77
+ background — `session_start` kicks it off and returns. Tools register as each
78
+ server answers (late registration is what `tool-search` was already built to
79
+ handle for exactly this producer), `/mcp` reports `still connecting` until the
80
+ connect settles, and per-server failures surface through the existing
81
+ notify-on-failure path. Measured after: 501ms to a usable prompt; both remote
82
+ servers connected and registered their tools a few seconds later while the
83
+ session was already live.
84
+
85
+ Two paths deliberately still **await** the connect:
86
+
87
+ - **Print-mode one-shots** (`!ctx.hasUI`): the single turn starts immediately
88
+ and would race past tools that are not registered yet.
89
+ - **Subagent RPC children** (`PI_SUBAGENT_CHILD=1` — they have `hasUI: true`,
90
+ so the env var is the discriminator): same race, and a child is often
91
+ spawned specifically to use an MCP tool.
92
+
93
+ Hardening that came with the change: the background promise is caught (an
94
+ unhandled rejection would crash Node; per-server failures were already
95
+ collected, so the catch only guards setup bugs and surfaces them loud via
96
+ notify), and a connection that lands after `session_shutdown` is closed
97
+ immediately instead of leaking its transport. The MCP SDK import also moved
98
+ into the first `connect()` call — ~70-80ms off every startup, and a session
99
+ with zero configured servers never loads it.
100
+
101
+ Accepted trade-off: the first turn(s) of an interactive session may run before
102
+ MCP `instructions` reminders and tool registrations exist. The tools are
103
+ deferred behind `tool_search` anyway, so the model discovers them on first
104
+ search after registration.