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
@@ -1,58 +0,0 @@
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.
@@ -1,104 +0,0 @@
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.
@@ -1,170 +0,0 @@
1
- # Memory & session state
2
-
3
- Part of [Decisions](../decisions.md).
4
-
5
- ## Memory: own file-based implementation
6
-
7
- The original plan scoped memory out as "Anthropic-hosted", but the memory
8
- system Claude Code actually runs on is machine-local plain files the model
9
- manages itself with ordinary file tools: one fact per markdown file under
10
- `~/.claude/projects/<slug>/memory/`, a `MEMORY.md` index, a `# Memory` system
11
- prompt section describing the format, and the index injected into the first
12
- turn. All of that is local, so we implement it (`extensions/lib/memory.ts` +
13
- `extensions/memory/`) rather than depend on `pi-memory`, whose model differs
14
- (its own tools and storage format) and would not read or write Claude Code's
15
- existing memory directories.
16
-
17
- Fidelity details confirmed against the official memory doc
18
- (<https://code.claude.com/docs/en/memory>), `payload.json`, and a live Claude
19
- Code context:
20
-
21
- - **Slug is keyed by git root, not cwd** — all worktrees and subdirectories of
22
- a repo share one memory directory; outside a repo the cwd is used. We resolve
23
- with `findGitRoot` (now in `lib/git.ts`).
24
- - **Only the first 200 lines or 25KB of MEMORY.md load**; `truncateIndex`
25
- mirrors that. Topic files are never startup-loaded — the model reads them on
26
- demand.
27
- - **Placement**: Claude Code nests the index inside the first-turn claudeMd
28
- `<system-reminder>` ("Contents of … (user's auto-memory, persists across
29
- conversations):"). We reuse that exact framing but emit a standalone
30
- reminder, because our CLAUDE.md equivalent travels through pi's
31
- `contextFiles` into the system prompt instead of a first-turn reminder.
32
- - **Write-time frontmatter stamping** (found by diffing a real session
33
- transcript's Write inputs against the files on disk): Claude Code enriches a
34
- memory file's `metadata:` at write time with `node_type: memory`,
35
- `originSessionId`, and a `modified` ISO timestamp — and never adds
36
- frontmatter to a file that has none, which keeps MEMORY.md unstamped. We
37
- stamp by mutating the `write` tool's input in `tool_call`, *before*
38
- execution, so the file lands stamped and file-tracker observes the same
39
- bytes that hit disk (stamping after its `tool_result` observation would make
40
- every memory write look externally modified).
41
- - **Write-time index limit guard**: after a write/edit of MEMORY.md, Claude
42
- Code measures the loadable content against the 200-line/25KB limits —
43
- near-limit it reminds the model to shorten the index; over-limit the write
44
- succeeds but the result is an error telling the model to rewrite. Replicated
45
- via the reminder queue and pi's rewritable `tool_result`.
46
-
47
- Not replicated: relevance-based mid-session recall of individual memory files.
48
- Its selection mechanism is undocumented client internals (the frontmatter
49
- `description` is "used to decide relevance during recall", and the UI shows
50
- "Recalled N memories"), and no recalled-memory block has been
51
- observed in practice — including in a long live session working on this very
52
- project with a relevant memory on file. There is nothing observable to copy;
53
- the injected index is the entry point and the model follows links from there.
54
-
55
- Split follows the house pattern: pure helpers (slug, paths, truncation,
56
- prompt/reminder text) in `lib/memory.ts`; the `memory` extension does mkdir +
57
- first-turn reminder over the queue; `system-prompt` re-derives the directory
58
- for its section (no shared module state — jiti). Using Claude Code's exact
59
- slug and layout means memories written by real Claude Code sessions are picked
60
- up unchanged, and vice versa.
61
-
62
- Verified live via tmux + `debug-capture.ts` (the sandboxed shell can't run pi
63
- directly): the `# Memory` section lands just before `# Environment`, the
64
- directory is created with the expected slug on session start (from a
65
- subdirectory of a git repo it resolves to the repo root), and a planted
66
- `MEMORY.md` arrives as a `<system-reminder>` on the first user message.
67
- Stamping was verified end-to-end with a real model run through the permission
68
- gate (allow rules, no bypass flag): the model wrote plain frontmatter and the
69
- file landed with `node_type`/`originSessionId`/`modified` in Claude Code's
70
- field order, while MEMORY.md stayed unstamped.
71
-
72
- ## The memory dir is harness-designated working space, like the plan file
73
-
74
- Auto mode blocked the auto-memory feature it ships next to: the system prompt
75
- instructs the model to write memories into `~/.claude/projects/<slug>/memory`,
76
- which is both outside the working directory and under the protected `.claude`
77
- dir, so every memory write went to the classifier and its out-of-project rule
78
- (correctly, per its own text) blocked it. The harness was flagging its own
79
- instruction.
80
-
81
- The fix follows the plan-file precedent exactly: `decide()` takes a
82
- `memoryDirPath` and deterministically allows *writing-tool* calls that land
83
- inside it, with cause `memory-dir`. Scope notes, since this punches through
84
- the protected-path check:
85
-
86
- - Deny rules and explicit ask rules still win — an ask rule is the user's
87
- stated intent to be prompted, and the check sits after both.
88
- - Only the exact per-project dir clears. Another project's memory dir, and
89
- everything else under `.claude`, still hits the protected-path check.
90
- - The check judges `resolvedSubject` when available — where the write actually
91
- *lands* — so a symlink planted inside the memory dir cannot convert the
92
- allow into a write elsewhere; `resolve()` normalises `..` traversals for the
93
- same reason. Bash writes into the dir are not cleared, only the write tools.
94
- - The permissions extension re-derives the dir (`memoryDir` + `findGitRoot`)
95
- rather than sharing state with the memory extension, per the jiti
96
- module-isolation rule. The workflow permission gate does the same, since
97
- memory writes inside `agent()` calls previously died on "needs interactive
98
- approval".
99
- - Plan mode still denies memory writes (unchanged); the auto-mode safety floor
100
- is unaffected — it deliberately targets exact settings files and already
101
- documents that `~/.claude` holds memory the agent writes routinely.
102
- - Trap, caught live: `resolveForContainment` returns a *case-folded* path, so
103
- any prefix/equality check against it must case-fold both sides (as
104
- `isProtectedPath` does). The first cut compared case-sensitively, matched
105
- nothing on macOS, and every memory write silently fell through to the
106
- protected-path check — visible only because the live run was checked before
107
- committing.
108
-
109
- ## Session scratchpad, Claude Code-style
110
-
111
- Claude Code gives every session a scratchpad
112
- (`<tmp>/claude-<uid>/<project-slug>/<session-id>/scratchpad`) via a system
113
- prompt section and lets it be used "generally without permission prompts";
114
- One Code had nothing, so temp files landed in `/tmp` or the project — and in
115
- auto mode, `/tmp` writes are exactly the out-of-project pattern the classifier
116
- flags. Same failure family as the memory-dir block, fixed with the same
117
- mechanism:
118
-
119
- - `lib/scratchpad.ts` derives the path (pure core + `sessionScratchpadDir`
120
- re-derived by every consumer, per the jiti module-isolation rule) and
121
- carries the prompt section verbatim from `payload.json`. `/tmp` is resolved
122
- through its symlink up front so the prompt, the permission comparison, and
123
- the case-folded resolved subject all name one real location.
124
- - The system-prompt extension mkdirs it at `session_start` and renders the
125
- section. The path embeds the session id, so it deliberately lives *outside*
126
- the (cwd, model)-cached environment block — constant within a session, which
127
- is all provider prompt caching needs. An unwritable tmp drops the section
128
- rather than promising a directory writes will error on.
129
- - `decide()` grew a `scratchpadDirPath` beside `memoryDirPath` — one shared
130
- `isInsideDir` check (resolved subject, case-folded, traversal-safe), deny
131
- and ask rules still winning, other sessions' scratchpads and bare `/tmp`
132
- not cleared. The workflow gate derives the *child's* scratchpad lazily from
133
- its first tool call, since the child session id does not exist when the
134
- gate is constructed.
135
- - The write tools get the deterministic allow; bash into the scratchpad still
136
- goes to the classifier, so the outside-cwd default rule now names the
137
- harness's designated directories (scratchpad, auto-memory) as clearing it.
138
-
139
- Verified live without `--dangerously-skip-permissions`: a plain `-p` session
140
- was asked for "a temporary file in the right place per your instructions",
141
- wrote `notes.txt` into its own session scratchpad path, and the file was on
142
- disk — no prompt, correct slug and session id. Typecheck and 711 unit tests
143
- pass.
144
-
145
- ## Own state, borrowed config: `.claude` is read-only, One Code writes to `~/.one-code`
146
-
147
- One Code sat in a three-way namespace muddle: pi owns `~/.pi` (sessions,
148
- models.json — harness plumbing, invisible, and untouchable without forking),
149
- `.claude` is Claude Code's directory, and One Code had started *generating*
150
- state into the latter (plan files at first; memory was already there at
151
- `~/.claude/projects/<slug>/memory`). Reading `.claude` deeply is the product —
152
- "your Claude Code setup works on any model" is the pitch, so settings, skills,
153
- commands, agents, plugins, and CLAUDE.md stay read-from-`.claude` forever. But
154
- *writing* into a namespace another product owns invites collisions: Claude
155
- Code also writes under `~/.claude/projects/<slug>/` and evolves that layout
156
- without notice, and a user auditing "what did Claude Code do" finds artifacts
157
- it didn't make.
158
-
159
- The policy, in one line: **`.claude` is a read-only compat surface; everything
160
- One Code generates goes to `~/.one-code`.** `extensions/lib/paths.ts` centralises
161
- both roots — `claudeConfigDir()` (honouring `CLAUDE_CONFIG_DIR`, which Claude
162
- Code itself supports and One Code previously ignored) and `oneCodeStateDir()`
163
- (honouring `ONE_CODE_STATE_DIR`). Plan files moved to `~/.one-code/plans`
164
- immediately, while the feature was a day old. `.one-code` joined the protected
165
- dirs (the gate's own namespace must be as guarded in its new home as in the
166
- old), with `.one-code/plans` excepted as working space. Deliberately *not* done:
167
- a One Code-native settings schema (no demand yet — compat reads suffice), and
168
- the memory-dir migration, which has a real tradeoff (sharing memory with
169
- Claude Code on the same repo is arguably a feature) and waits on its own
170
- decision.
@@ -1,126 +0,0 @@
1
- # Model policy
2
-
3
- Part of [Decisions](../decisions.md).
4
-
5
- ## Shared role profiles: smaller classifiers and delegated workers by default
6
-
7
- A real OpenAI Codex session exposed two different defaults with the same costly
8
- result: selecting `gpt-5.6-sol` made the banner read `classifier 5.6-sol
9
- (planned)` and `subagents 5.6-sol (session)`. The classifier's provider table
10
- predated Luna and 5.4 Mini, so its deliberate "session before anything chosen
11
- only by price" fallback did exactly what it should. Subagents had a separate,
12
- intentional session-model default. Workflow `agent()` had a third path: it
13
- always received `ctx.model`, so it did not actually honour `/subagent` despite
14
- the command and reminder saying it did.
15
-
16
- The fix is one pure policy layer (`extensions/lib/model-policy.ts`) shared by
17
- both selectors, but **not one shared capability rule**:
18
-
19
- - classifier profiles contain only reviewed small-but-capable families; price
20
- alone can never promote a model into the security boundary;
21
- - subagent profiles prefer economical coding/reasoning models, then may use a
22
- cheaper price-ranked model when the provider policy says containment is
23
- knowable;
24
- - explicit classifier, per-call, agent-frontmatter, and `/subagent` choices
25
- retain their old precedence and may cross providers because naming one is
26
- choosing it; `inherit` explicitly suppresses automatic selection;
27
- - both roles finish at the session model rather than breaking the feature.
28
-
29
- Automatic subagent selection is a *cost optimisation*, and review tightened it
30
- to demand cost evidence: it engages only when both the session model and the
31
- candidate carry real prices and the candidate is strictly cheaper. Without that
32
- rule an unpriced catalog let the profile's order silently *upgrade* a haiku
33
- session to sonnet-class. The price-ranked fallback (for when the profile has
34
- gone stale) additionally requires one of the known small-model name words and
35
- ranks those tiers ahead of raw price — otherwise the absolute cheapest
36
- contained model became the default coding worker, which on a mainstream catalog
37
- is nano/lite-tier or something wholly unknown. And once any *explicit* choice
38
- (agent frontmatter or configured default) fails to resolve, automatic selection
39
- stays out of it: substituting a cheaper vetted model for a model somebody named
40
- is a model nobody described, so the remaining chain and then the session model
41
- serve.
42
-
43
- The profile inventory was checked on 2026-08-06 against pi 0.84.0's generated
44
- catalog and official model pages: [Anthropic](https://platform.claude.com/docs/en/about-claude/models/overview),
45
- [OpenAI](https://developers.openai.com/api/docs/models),
46
- [Google](https://ai.google.dev/gemini-api/docs/models),
47
- [xAI](https://docs.x.ai/developers/models),
48
- [Mistral](https://docs.mistral.ai/getting-started/models/models_overview/),
49
- [DeepSeek](https://api-docs.deepseek.com/quick_start/pricing),
50
- [Z.AI](https://docs.z.ai/), [Qwen Coding Plan](https://help.aliyun.com/en/model-studio/coding-plan),
51
- [Kimi](https://platform.kimi.ai/docs/models),
52
- [MiniMax](https://platform.minimax.io/docs/guides/models-intro),
53
- [Xiaomi](https://mimo.mi.com/docs/quick-start/summary/model), and
54
- [Ant Ling](https://developer.ant-ling.com/en/docs/models/ling/). Hosted-profile
55
- IDs came from the official catalogs for [NVIDIA NIM](https://build.nvidia.com/models),
56
- [Groq](https://console.groq.com/docs/models),
57
- [Cerebras](https://inference-docs.cerebras.ai/models/overview),
58
- [Fireworks](https://fireworks.ai/models),
59
- [Together](https://docs.together.ai/docs/serverless/models),
60
- [Baseten](https://www.baseten.co/products/model-apis/), and
61
- [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/models/).
62
- The lists are intentionally short prefixes applied to the *live authenticated
63
- catalog*, not copied catalogs; unavailable entries simply fall through.
64
-
65
- All 39 built-in pi language-model provider IDs have an explicit policy. Direct
66
- vendors contain to the provider. Stable hosted inference services such as Groq
67
- and NVIDIA contain to that host even when IDs carry publisher-like prefixes.
68
- Gateways require more care:
69
-
70
- - OpenRouter's `creator/model` prefix identifies a model namespace, **not the
71
- serving inference provider**; automatic switching stays in that namespace and
72
- reuses its direct-family profile, but One Code cannot claim processor
73
- containment because pi exposes none of OpenRouter's provider-routing options
74
- ([official routing semantics](https://openrouter.ai/docs/guides/routing/provider-selection)).
75
- - Vercel likewise separates creator IDs from serving providers; profile reuse
76
- stays in the creator namespace ([provider options](https://vercel.com/docs/ai-gateway/models-and-providers/provider-options)).
77
- - Cloudflare's generated pi catalog strips some upstream prefixes, so route
78
- identity comes from API/base transport rather than slash parsing
79
- ([provider integrations](https://developers.cloudflare.com/ai-gateway/providers/)).
80
- - Bedrock containment includes geography plus family: an automatic choice may
81
- not move from `us.` to `eu.` or `global.`. Opaque application-profile ARNs
82
- stay on the session model ([model IDs](https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html)).
83
- - Hugging Face, Radius, OpenCode Zen, OpenCode Go, unknown aliases, and custom
84
- providers whose route cannot be established are session-only automatically.
85
- Hugging Face repository IDs in particular do not name the serving inference
86
- provider unless a provider suffix is pinned
87
- ([routing docs](https://huggingface.co/docs/inference-providers/en/index)).
88
-
89
- With no explicit default, an OpenAI Codex Sol session now plans both its
90
- classifier and delegated workers on Luna. The banner labels the latter `(auto)`
91
- rather than implying it came from settings. `workflow/index.ts` passes the same
92
- user/managed default into `AgentRunner`, and workflow calls no longer bypass
93
- the automatic resolver when `model` is omitted.
94
-
95
- Verified against fresh pi processes: the TUI banner showed `model 5.6-sol ·
96
- subagents 5.6-luna (auto)`; an omitted-model foreground subagent recorded
97
- `openai-codex/gpt-5.6-luna`; and an omitted-model synchronous workflow completed
98
- on the same low-cost path. Luna's classifier was exercised in both directions
99
- without `--dangerously-skip-permissions`: an exact user-named `/tmp` write was
100
- allowed by verified intent, while a delegated outside-project backup path was
101
- blocked under S5. Typecheck and all 682 unit tests passed.
102
-
103
- ## Upward cost pressure: an informational warning and a per-call gate
104
-
105
- Automatic selection only ever moves cost *down*, but two paths still let a
106
- subagent run on something pricier than the session model: a configured default
107
- (deliberate cheap-driver/strong-worker setups exist) and the per-call `model`
108
- field (chosen by the main model itself). Those get different treatment because
109
- their authors differ:
110
-
111
- - **Configured default pricier than the session model** → one informational
112
- line in the every-turn reminder, with both prices. It suggests a cheaper
113
- listed model for routine tasks but does not tell the model to override the
114
- user's knob — the user set it, and second-guessing a user setting from the
115
- reminder would invert authority.
116
- - **Per-call `model` pricier than the session model** → the subagent tool
117
- errors with both prices and the menu, and workflow `agent()` throws, unless
118
- the call sets `allow_expensive: true` (`allowExpensive` in agent() opts). The
119
- schema tells the model to set it only when the user explicitly asked for that
120
- model. Only `source: "call"` resolutions are gated: `subagentModel`, the env
121
- var, agent-file frontmatter, and `inherit` are user-installed choices, and
122
- the codebase rule is that naming a model is choosing it.
123
-
124
- The gate opens when either price is unknown — a cost gate that fails closed on
125
- an unpriced catalog blocks the feature outright, the same failure shape as
126
- gating a tool other tools sit behind.
@@ -1,178 +0,0 @@
1
- # Modes & keybindings
2
-
3
- Part of [Decisions](../decisions.md).
4
-
5
- ## `/effort`, and ultracode as a standing mode
6
-
7
- Claude Code exposes reasoning effort as a Faster→Smarter slider whose last stop,
8
- past `max` and behind a divider, is `ultracode` — subtitled "xhigh + workflows".
9
- The divider is the whole point: every other stop buys more thinking, while that
10
- one also changes *how the model works*, arming multi-agent orchestration on every
11
- turn instead of the single turn the keyword arms. One Code now has the same
12
- command, and the same two-tier opt-in: the keyword in a message for one turn
13
- (`extensions/workflow`), `/effort ultracode` for a standing mode that persists
14
- until switched off.
15
-
16
- The mode is implemented the way the permissions extension implements plan mode —
17
- an `every-turn` reminder on `one-code:system-reminder` under a key, withdrawn with
18
- `{remove: true}` when leaving — plus `ctx.ui.setStatus` for a `✦ ultracode`
19
- footer indicator. Its standing text is deliberately stronger than the keyword's
20
- one-turn nudge: orchestrate substantive work *by default* and verify
21
- adversarially, with trivial and conversational turns carved out so the mode does
22
- not spawn a fleet to rename a variable.
23
-
24
- The slider itself is a hand-rolled `Component` passed to `ctx.ui.custom()`,
25
- following branding's precedent of implementing pi-tui's interface inline. That is
26
- not stylistic: `@earendil-works/pi-tui` is a regular dependency of
27
- pi-coding-agent rather than a peer, so it is unhoisted and unresolvable from this
28
- package — pi's own thinking picker is a vertical `SelectList`, which we cannot
29
- import and which is the wrong shape anyway. The cost is decoding keys from raw
30
- bytes (`handleInput` hands over terminal data, not key names) and owning the
31
- layout, so the marker row, labels, and track are laid out against measured
32
- column positions and collapse to a single line rather than wrapping when the pane
33
- is too narrow.
34
-
35
- One live finding shaped the design. `setThinkingLevel` **clamps to model
36
- capability silently** — no throw, no warning, and `thinking_level_select` fires
37
- after the fact without a veto — so asking for xhigh on haiku-4-5 quietly yields
38
- `high`. Rather than report a level the model never accepted, the command sets,
39
- reads back, and says so: "Effort: ultracode — high reasoning, workflows armed
40
- every turn (this model caps reasoning at high)". That also fixed a subtler bug:
41
- the shift+tab hygiene (drop the mode when the user cycles thinking away, so the
42
- footer cannot lie) originally compared against xhigh, which on a clamping model
43
- would have made ultracode cancel itself the moment anything re-announced the
44
- clamped level. It compares against the level the mode actually settled on.
45
-
46
- Verified: 19 unit tests over the pure slider (choice/level mapping, both arrow
47
- encodings, clamped movement, marker alignment under the selected label, the
48
- ultracode subtitle appearing only when selected, narrow-pane degradation) and a
49
- live tmux TUI run — the slider opened preselected at the current level, arrow
50
- keys moved the marker, `ultracode` revealed its subtitle, Enter applied it with
51
- the clamp reported honestly, `✦ ultracode` appeared in the footer, and a
52
- `CC_E2E_LOG` capture confirmed the standing reminder in **both** subsequent
53
- turns' payloads rather than just the first.
54
-
55
- ## Aligning `/effort` with shift+tab
56
-
57
- The first cut left two dials disagreeing: shift+tab cycled pi's seven thinking
58
- levels and called it "thinking", while `/effort` showed Claude Code's five plus
59
- ultracode and called it "effort". Same underlying number, two names, two sets.
60
-
61
- Taking over the key is not available: `app.thinking.cycle` is on pi's
62
- `RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS` list, so `registerShortcut` on
63
- shift+tab is skipped with a warning rather than honoured, and no extension API
64
- unbinds or retargets a reserved key (only the user's own keybindings config can).
65
- Disabling it was therefore off the table too, which settles the direction:
66
- conform to the key rather than compete with it. The slider shows the full `off`→
67
- `max` ladder plus `ultracode` past the divider, but **enables only the stops the
68
- active model can reach**: pi filters shift+tab's own cycle through
69
- `getSupportedThinkingLevels(model)` (a per-model set derived from the model's
70
- `thinkingLevelMap`, see findings §3), so a fixed seven-stop ladder disagreed with
71
- shift+tab on every model that doesn't support all of them. Unsupported stops stay
72
- on the track dimmed and are skipped by navigation, with a `dimmed = unsupported by
73
- <model>` note; everything we write calls the dial "effort" (banner hint included;
74
- pi's own footer still says what it says), and the footer names shift+tab as the
75
- shortcut for the plain stops, so the two read as one control with two entry points
76
- rather than two competing settings.
77
-
78
- Building the slider from the fixed ladder rather than the model's set was a live
79
- bug: on `deepseek-v4-flash` (supported levels `{high, max}`) picking `low` made
80
- pi's `clampThinkingLevel` snap *upward* to `high`, which the old code mislabelled
81
- "caps reasoning at high". A downward clamp (the model genuinely can't reach the
82
- requested level) is now the only thing flagged — "tops out at" — and the typed
83
- path (`/effort low`) refuses an unreachable level, naming what the model does
84
- support, instead of silently rerouting it. Verified live against
85
- `deepseek-v4-flash`.
86
-
87
- Widening the track from six stops to eight is what exposed a latent bug: the
88
- layout only checked whether the *track* fit the pane, so the longer hint line
89
- overflowed at 80 columns. Width is now measured on the unstyled text of every row
90
- (escape codes would inflate the count), and anything that does not fit collapses
91
- to a clipped one-line stop list — verified by a test that walks every stop at
92
- widths from 200 down to 10.
93
-
94
- ## Permission-mode cycling on ctrl+q, not shift+tab, ctrl+m, or alt+m
95
-
96
- Claude Code cycles permission modes with shift+tab; typing
97
- `/permission-mode acceptEdits` for the same switch was friction worth removing.
98
- The obvious key is taken: pi reserves shift+tab for `app.thinking.cycle` (see
99
- "Aligning `/effort` with shift+tab" above), and freeing it would mean writing
100
- `app.thinking.cycle: []` into the user's `~/.pi/agent/keybindings.json` — a
101
- package silently editing user config to defeat a deliberate reservation. That
102
- was prototyped and rejected in favour of keeping pi's default: shift+tab stays
103
- the effort dial.
104
-
105
- ctrl+m was considered next and is impossible at the protocol level: ctrl+m *is*
106
- carriage return (0x0D) in terminal encoding, so outside kitty-protocol
107
- terminals `matchesKey("\r", "ctrl+m")` is true and every Enter press would
108
- cycle the mode. pi-tui's `rawCtrlChar` confirms there is no special-casing.
109
- alt+m — Claude Code's own documented fallback binding, used on Windows when VT
110
- input is unavailable — shipped briefly and was rejected for macOS ergonomics:
111
- option+m types "µ" unless the terminal is configured to send option as Meta,
112
- which is off by default everywhere and not a setting to ask users for.
113
-
114
- That leaves the ctrl+letters, and pi plus the terminal claim nearly all of
115
- them: a/b/e/f/k/u/w/y (editor), c/d/z (clear/exit/suspend), g/l/n/o/p/t/v/x
116
- (pi app keys), r/s (session picker), and h/i/j/m are Backspace/Tab/LF/CR at
117
- the byte level. **ctrl+q** is the one left standing — historically XON flow
118
- control, but pi's raw-mode TUI disables IXON so it arrives everywhere,
119
- macOS included, with no configuration.
120
-
121
- The modes themselves follow Claude Code v2.1 (verified against
122
- code.claude.com/docs/en/permission-modes.md): `default` is displayed as
123
- "manual" and `manual` is accepted as an alias everywhere a mode is named; the
124
- cycle is manual → acceptEdits → plan, with bypassPermissions joining only when
125
- the session started with it; `dontAsk` (deny instead of prompting; never in
126
- the cycle) is accepted via flag/settings; footer badges use Claude Code's
127
- exact strings (`⏸ manual mode on`, `⏵⏵ accept edits on`, …) via
128
- `ctx.ui.setStatus`. Claude Code's `auto` mode is deliberately not implemented:
129
- it is gated on a server-side approval classifier we have no equivalent of, and
130
- Claude Code itself drops it from the cycle when unavailable, so its absence is
131
- faithful rather than a gap. `/permission-mode` is gone; mode-change reminders
132
- are keyed so cycling through several modes announces only the one settled on.
133
- Verified in a live tmux TUI: badge at startup, three-stop cycle, bypass
134
- joining under `--dangerously-skip-permissions`, and `/permission` completing
135
- to only `/permissions`.
136
-
137
- ## Plan mode is file-based, like current Claude Code
138
-
139
- Claude Code moved plan mode from "pass the plan as an ExitPlanMode parameter"
140
- to a **plan file**: entering plan mode allocates `~/.one-code/plans/<slug>.md`,
141
- a per-turn system message names it as the one writable path and prescribes an
142
- explore→design→review→write→approve workflow, and ExitPlanMode takes no
143
- parameters — it reads the file. Observed live in Claude Code and mirrored here: the plan
144
- survives compaction, the user can open/edit it, and long plans stop bloating
145
- tool calls and `ui.select` titles.
146
-
147
- **Who owns what.** Mode stays with `permissions`; the file belongs to
148
- `plan-mode`. Allocation happens in `before_agent_start` — the one hook that
149
- covers all three entry points (tool, ctrl+q, `defaultMode: "plan"`) and runs
150
- after every extension's `session_start`, so restoring the branch's previous
151
- path (a `plan-mode-file` custom entry, read via `getBranch()` like tasks/todo)
152
- can never race a fresh allocation. The path crosses to the matcher over a new
153
- `one-code:plan-file-path` channel, mirroring `one-code:set-permission-mode` in the
154
- other direction; `plan-mode` learns the mode from the existing
155
- `one-code:permission-status` channel rather than a shared module (jiti).
156
-
157
- **The reminder key moved.** `setMode` no longer emits plan mode's reminder;
158
- `plan-mode` re-emits under the same `"permission-mode"` key every turn, with
159
- `existsSync` flipping "create it at …" to "continue building …" the turn after
160
- the file appears. Every-turn re-emits replace by key, so nothing accumulates.
161
-
162
- **The carve-out allows outright.** In plan mode a write whose subject (or
163
- symlink-resolved subject) is the plan file returns `allow`, and everything
164
- else keeps denying. Outside plan mode the file has no special status, matching
165
- Claude Code. Deny rules still beat the carve-out. (`.one-code` itself is a
166
- protected dir like `.claude`; `.one-code/plans` is excepted as working space —
167
- plan documents are rendered to the user, never executed.)
168
-
169
- **Slug fidelity note.** Claude Code's real slugs start with the user's opening
170
- words plus two random words (`we-are-going-to-async-turtle.md`); One Code uses
171
- three random words. Deriving from the prompt was skipped — the slug is
172
- allocated before any user text is guaranteed to exist (ctrl+q, defaultMode).
173
-
174
- Verified live: an RPC run (no `--dangerously-skip-permissions`) showed a
175
- blocked ordinary write and an unprompted plan-file write in plan mode; a tmux
176
- TUI run exercised the scrollable approval viewer (scroll, choice switch,
177
- reject→stay-planning, Enter→approve→manual mode) and ctrl+q entry reusing the
178
- session's existing file.