one-code-extension 0.4.1 → 0.6.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 (439) hide show
  1. package/README.md +16 -5
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +58 -31
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +124 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +56 -0
  63. package/extensions/background/wakeup.ts +178 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +66 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -11,37 +11,44 @@
11
11
  * silently selected `anthropic/claude-haiku-4-5` — shipping that user's prompts to
12
12
  * a vendor they had not chosen for this session, through a component with no UI.
13
13
  *
14
- * So: **never leave the session's provider unless the user asked for it.** An
15
- * explicit `autoMode.classifierModel` may name any provider, because naming it is
16
- * the asking. Everything else stays where the session already is. This is the same
17
- * reasoning pi's own subagent config gives for defaulting to the session model —
18
- * "this keeps new installs from depending on a provider you may not have
19
- * configured" — and this module follows that precedence shape: explicit override,
20
- * then a configured default, then something sensible in-provider, then the session
21
- * model itself.
14
+ * So: **never leave the session's provider or route.** Automatic selection uses
15
+ * only contained models. It also requires a catalog context window at least as
16
+ * large as the session's, because the classifier receives the full transcript;
17
+ * if that cannot be proved, it screens on the session model instead. And it
18
+ * never screens with a model in a lower tier than the session's, or one that is
19
+ * not strictly cheaper per classifier call (a tie keeps the session).
22
20
  *
23
- * Name matching is demoted to a tiebreak because it does not survive contact with
24
- * real catalogs: of Groq's 7 models and xAI's 3, *none* contain any of those
25
- * substrings, while OpenRouter has 303 models and 79 substring hits, so the "first
26
- * match" is arbitrary. Cost is the one signal every provider carries.
21
+ * "Cheaper" is judged on what a classifier call costs, not on list input
22
+ * price: a call is mostly cached input (`CLASSIFIER_CALL_TOKENS`), so the
23
+ * cache-read price dominates. By input price DeepSeek V3.2 screened its own
24
+ * sessions and timed out (findings §61); per call V4.1 Flash is cheaper and
25
+ * answers in seconds.
26
+ *
27
+ * An explicit `autoMode.classifierModel` may name any provider, because naming
28
+ * it is choosing it: it leads the chain, with notices for a cross-provider
29
+ * choice and warnings for what it costs (`lib/model-choice-warnings.ts`).
27
30
  */
28
31
 
29
32
  import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
33
+ import { type ChosenModelWarning, chosenModelWarnings } from "../lib/model-choice-warnings.ts";
30
34
  import {
31
35
  crossesProvider,
32
36
  findConfigured,
37
+ hasCatalogContextWindow,
38
+ isMovingAlias,
33
39
  isStaleContainmentStamp,
34
40
  modelIdentity,
35
41
  modelSpec as spec,
42
+ classifierPrice,
43
+ modelsContainedToSession,
36
44
  } from "../lib/model-policy.ts";
37
- import { capableContainedCandidates } from "../lib/model-tier.ts";
38
-
39
- export { findConfigured } from "../lib/model-policy.ts";
45
+ import { pairedModel } from "../lib/model-pairings.ts";
46
+ import { intrinsicTier, sameTierContainedCandidates } from "../lib/model-tier.ts";
40
47
 
41
48
  export interface Candidate {
42
49
  model: Model<Api>;
43
50
  /** How this candidate was arrived at, for the notice shown to the user. */
44
- source: "configured" | "economical" | "session";
51
+ source: "configured" | "paired" | "economical" | "session";
45
52
  }
46
53
 
47
54
  export interface SelectInput {
@@ -51,142 +58,158 @@ export interface SelectInput {
51
58
  /** `autoMode.classifierModel`, if set. May name any provider. */
52
59
  configured?: string;
53
60
  /**
54
- * The containment identity (`modelIdentity().containment`) the `classifierModel`
55
- * setting was stamped for when set via `/auto-mode model`. When it differs from
56
- * the current session's containment, a cross-provider setting is treated as
57
- * stale (set for a session since left) and overridden with a warning — the same
58
- * shape as the subagent `subagentModelSetFor` stamp. Undefined for a hand-edited
59
- * setting.
61
+ * The containment identity the `classifierModel` setting was stamped for by
62
+ * `/auto-mode model`. When it differs from this session's, a cross-provider
63
+ * setting is stale (set for a session since left) and is replaced, with a
64
+ * warning. Undefined for a hand-edited setting.
60
65
  */
61
66
  configuredSetForContainment?: string;
67
+ /** `suggestNewerModels` in ~/.onecode/settings.json; false drops the chosen model's newer-model warning. */
68
+ suggestNewer?: boolean;
69
+ }
70
+
71
+ /** Why automatic selection had to keep the session model. */
72
+ export type ClassifierFallbackReason = "unknown-session-context-window" | "paired-session" | "no-qualifying-model";
73
+
74
+ /** Structured selection fallback, retained with the chain for permission UI and logging. */
75
+ export interface ClassifierFallback {
76
+ reason: ClassifierFallbackReason;
77
+ text: string;
62
78
  }
63
79
 
64
80
  /** A user-facing selection notice, tagged so an informational one is not shown as a warning. */
65
81
  export interface ClassifierNotice {
66
82
  level: "info" | "warning";
67
83
  text: string;
84
+ /** Present for a session fallback, so renderers need not parse `text`. */
85
+ fallbackReason?: ClassifierFallbackReason;
86
+ /** Present for a warning about the user's chosen classifier (`lib/model-choice-warnings.ts`). */
87
+ choiceWarning?: ChosenModelWarning["kind"];
88
+ }
89
+
90
+ export interface ClassifierChain {
91
+ candidates: Candidate[];
92
+ notices: ClassifierNotice[];
93
+ fallback?: ClassifierFallback;
68
94
  }
69
95
 
70
96
  /**
71
- * The ordered candidate chain plus any user-facing notices. More than one
72
- * candidate is returned so one that turns out to be unusable at call time (not
73
- * entitled on this account, withdrawn by the provider) can be stepped over
74
- * instead of failing every tool call — the same shape as pi's subagent
75
- * `fallbackModels`.
76
- *
77
- * The chain always ends at the session model when there is one, so auto mode
78
- * degrades to "correct but not cheap" rather than to "broken".
79
- *
80
- * The automatic pick is the SAME floor-gated selector the subagent default uses
81
- * (`capableContainedCandidates`): the cheapest same-provider model (cheap →
82
- * workhorse → frontier, never `tiny`) at or above `automaticTierFloor` — a
83
- * session on a workhorse-or-better model is screened by a workhorse-or-better
84
- * model, Claude Code's `min(main, sonnet)`. `docs/decisions/auto-mode.md`
85
- * measured the same `rm -rf` grading stage-1 62 on Sonnet and ~22 on Haiku —
86
- * "a weak classifier is a weak boundary" — yet until 2026-09-05 the selector
87
- * picked Haiku even when the session itself was Sonnet, so the gate's threshold
88
- * behaviour depended on which model happened to be cheapest
89
- * (PERMISSIONS-REVIEW-2026-09-05 M6). A cheap session keeps a cheap screener
90
- * (nothing cheaper and capable exists), and `autoMode.classifierModel`
91
- * overrides either way. The `tiny` exclusion is the capability floor
92
- * `auto-mode.md` recorded as still-missing. Since 2026-09-11 subagents share
93
- * the floor, so a session screens and delegates on one model.
94
- *
95
- * The floor governs the AUTOMATIC pick only; the terminal fallback is never
96
- * refused, so the chain is empty only when there is no session model and nothing
97
- * available at all.
97
+ * The ordered fallback chain for the permission classifier. The user's
98
+ * `autoMode.classifierModel` leads when it resolves and is not stale; its
99
+ * warnings ride the notices. Then the automatic chain
100
+ * (`automaticClassifierCandidates`), which ends at the session model.
98
101
  */
99
- export function classifierCandidates({
100
- available,
101
- sessionModel,
102
- configured,
103
- configuredSetForContainment,
104
- }: SelectInput): { candidates: Candidate[]; notices: ClassifierNotice[] } {
105
- const candidates: Candidate[] = [];
102
+ export function classifierCandidates({ available, sessionModel, configured, configuredSetForContainment, suggestNewer = true }: SelectInput): ClassifierChain {
103
+ const automatic = automaticClassifierCandidates(available, sessionModel);
104
+ if (!configured) return automatic;
105
+ /** The setting does not apply: the automatic chain, led by a warning saying why. */
106
+ const automaticBecause = (text: string): ClassifierChain => ({ ...automatic, notices: [{ level: "warning", text }, ...automatic.notices] });
107
+ const resolved = findConfigured(available, configured);
108
+ if (!resolved) {
109
+ return automaticBecause(
110
+ `autoMode.classifierModel ${configured} is not an available model (unknown id, or no credentials for its provider); auto mode chooses automatically instead.`,
111
+ );
112
+ }
106
113
  const notices: ClassifierNotice[] = [];
107
- const push = (model: Model<Api> | undefined, source: Candidate["source"]) => {
108
- if (!model) return;
109
- if (candidates.some((entry) => entry.model.provider === model.provider && entry.model.id === model.id)) return;
110
- candidates.push({ model, source });
111
- };
112
-
113
- // 1. What the user asked for. Naming a provider is choosing it — but a setting
114
- // stamped for a provider this session has since left is stale and overridden
115
- // with a warning (parity with the subagent setting); a genuine cross-provider
116
- // choice made for THIS session is honored with an announcement, because the
117
- // classifier reads the user's prompts and CLAUDE.md.
118
- const resolved = configured ? findConfigured(available, configured) : undefined;
119
- if (resolved) {
120
- if (sessionModel && crossesProvider(resolved, sessionModel)) {
121
- if (isStaleContainmentStamp(configuredSetForContainment, sessionModel)) {
122
- notices.push({
123
- level: "warning",
124
- text:
125
- `autoMode.classifierModel ${spec(resolved)} was set for a different provider than this session (${spec(sessionModel)}); ` +
126
- "a same-provider model screens calls instead. Re-set it with /auto-mode model on this session to use it here.",
127
- });
128
- } else {
129
- // Informational, not a warning: the user deliberately set this for this
130
- // session, so it is doing exactly what they asked.
131
- notices.push({
132
- level: "info",
133
- text:
134
- `autoMode.classifierModel ${spec(resolved)} is a different provider than this session (${spec(sessionModel)}) — ` +
135
- "it reads your prompts and CLAUDE.md, so those go to that provider. It was set for this session, so it is honored.",
136
- });
137
- push(resolved, "configured");
138
- }
139
- } else {
140
- push(resolved, "configured");
114
+ if (sessionModel && crossesProvider(resolved, sessionModel)) {
115
+ // Naming a provider is choosing it, but a setting stamped for a provider
116
+ // this session has since left is stale (parity with the subagent setting).
117
+ if (isStaleContainmentStamp(configuredSetForContainment, sessionModel)) {
118
+ return automaticBecause(
119
+ `autoMode.classifierModel ${spec(resolved)} was set for a different provider than this session (${spec(sessionModel)}); ` +
120
+ "a same-provider model screens calls instead. Re-set it with /auto-mode model on this session to use it here.",
121
+ );
141
122
  }
142
- }
143
-
144
- // 2. The cheapest capable same-provider model, no dearer than the work being
145
- // screened — screening a call more expensively than making it is
146
- // indefensible. The shared gate excludes `tiny`, unpriced/opaque rows, and
147
- // the session model itself, so an unpriced provider yields nothing here and
148
- // the session model (step 3) screens the calls.
149
- // With an Artificial Analysis snapshot (an optional user key —
150
- // lib/capability-index.ts) the floor is MEASURED: a candidate whose coding
151
- // index reaches min(session, Sonnet 5), on the thinking-off variant the
152
- // classifier actually runs, screens — Claude Code's min(main, Sonnet) taken
153
- // literally, so a flash-class model that measurably matches its vendor's
154
- // flagship may screen it. A measured failure is never admitted; an
155
- // unscored candidate (no key, no confirmed match) is judged by the
156
- // name-class tier floor below, exactly as before the snapshot existed.
157
- if (sessionModel) {
158
- for (const model of capableContainedCandidates(available, sessionModel, "classifier")) push(model, "economical");
159
- }
160
-
161
- // 3. The session's own model: always correct, just not cheap. Terminal
162
- // fallback, so the gate degrades to screening on it rather than to broken.
163
- // No tier floor here: a floor that refused a `tiny` session model would
164
- // disarm auto mode on exactly the sessions least able to do without it,
165
- // on a price-and-name heuristic rather than measured competence. The
166
- // circumvention of a permission rule that motivated one is handled by
167
- // giving the classifier the user's own deny rules to judge by effect
168
- // (permissions/rule-prose.ts), not by picking a different screener.
169
- push(sessionModel, "session");
170
-
171
- // Nothing configured and no session model (a headless run with a bare
172
- // registry) — take anything available rather than refusing outright.
173
- if (candidates.length === 0) push(available[0], "session");
174
-
175
- // A configured spec that matched nothing available would otherwise fall through
176
- // in silence, leaving the user believing their setting is in force.
177
- if (configured && !resolved) {
178
- const instead = candidates[0];
123
+ // Informational: the user chose this for this session.
179
124
  notices.push({
180
- level: "warning",
125
+ level: "info",
181
126
  text:
182
- `autoMode.classifierModel is set to "${configured}", which is not an available model — check the name and that its provider is authenticated.` +
183
- (instead ? ` Auto mode is using ${describeCandidate(instead)} instead.` : ""),
127
+ `autoMode.classifierModel ${spec(resolved)} is a different provider than this session (${spec(sessionModel)}): ` +
128
+ "it reads your messages and CLAUDE.md, so those go to that provider. It was set for this session, so it is used.",
184
129
  });
185
130
  }
186
-
131
+ for (const warning of chosenModelWarnings({ available, sessionModel, chosen: resolved, role: "classifier", suggestNewer })) {
132
+ notices.push({ level: "warning", text: `${warning.text} ${warning.fix}`, choiceWarning: warning.kind });
133
+ }
134
+ const candidates: Candidate[] = [{ model: resolved, source: "configured" }];
135
+ for (const entry of automatic.candidates) {
136
+ if (entry.model.provider !== resolved.provider || entry.model.id !== resolved.id) candidates.push(entry);
137
+ }
138
+ // The automatic chain's "why the session screens" notice does not describe a
139
+ // chain the user's choice leads, so it is dropped with its fallback.
187
140
  return { candidates, notices };
188
141
  }
189
142
 
143
+ /**
144
+ * The automatic chain. For a session model the pairing table names
145
+ * (`lib/model-pairings.ts`), its pick leads (or the session alone screens);
146
+ * otherwise the cost algorithm below. Entries stay on the session provider/route, sit in the
147
+ * session's tier or above (`model-tier.ts`, from the public catalogs), cost
148
+ * strictly less than the session model, are not experimental builds or moving
149
+ * `-latest` aliases (what answers one changes without notice), and have
150
+ * a known catalog window at least as large as the session's: the same rule as
151
+ * the subagent default (`sameTierContainedCandidates`), priced per classifier
152
+ * call, plus the window. The
153
+ * cheapest qualifying model screens first, with the session retained as an
154
+ * availability fallback.
155
+ */
156
+ function automaticClassifierCandidates(available: Model<Api>[], sessionModel: Model<Api> | undefined): ClassifierChain {
157
+ // A classifier gets the whole user transcript. It must stay on the session's
158
+ // provider/route, sit in the session's tier, and be able to receive every
159
+ // token the session catalog says the main model can receive. Catalog
160
+ // omissions are unsafe to guess at: an unknown candidate window is ineligible.
161
+ if (!sessionModel) return { candidates: [], notices: [] };
162
+ const sessionOnly = (fallback: ClassifierFallback): ClassifierChain => ({
163
+ candidates: [{ model: sessionModel, source: "session" }],
164
+ notices: [{ level: "info", text: fallback.text, fallbackReason: fallback.reason }],
165
+ fallback,
166
+ });
167
+ if (!hasCatalogContextWindow(sessionModel)) {
168
+ return sessionOnly({
169
+ reason: "unknown-session-context-window",
170
+ text: `Auto mode is screening calls with ${spec(sessionModel)}, this session's model, because its catalog context window is unknown and no alternate classifier can be verified to contain it.`,
171
+ });
172
+ }
173
+ // The pairing table speaks first for the models it names (lib/model-pairings.ts),
174
+ // under the same window and moving-alias gates; it may also keep the session.
175
+ const fits = (model: Model<Api>) => !isMovingAlias(model.id) && hasCatalogContextWindow(model) && model.contextWindow >= sessionModel.contextWindow;
176
+ const contained = modelsContainedToSession(available, sessionModel);
177
+ const price = classifierPrice(sessionModel);
178
+ const paired = pairedModel("classifier", available, sessionModel, { contained, price, accept: fits });
179
+ if (paired?.kind === "session") {
180
+ return sessionOnly({
181
+ reason: "paired-session",
182
+ text: `Auto mode is screening calls with ${spec(sessionModel)}, this session's model: One Code's model pairing table finds no smaller model of its vendor close enough in capability to screen for it.`,
183
+ });
184
+ }
185
+ // Never weaker than the session, strictly cheaper per call (the user,
186
+ // 2026-10-05: Qwen 3.8 27B drew a 2.4T model at five times its input price;
187
+ // 2026-10-07: a same-price older model saves nothing; 2026-10-12: judged on
188
+ // a call's own cost shape), and an unpriced session cannot prove an
189
+ // alternate cheaper. The classifier adds a window that contains the
190
+ // session's and never a moving alias (experimental builds are never
191
+ // auto-selectable at all). A session with no catalog cache-read price is
192
+ // compared by list input price (classifierPrice).
193
+ const cheaper = sameTierContainedCandidates(available, sessionModel, { contained, price }).filter(fits);
194
+ // The cost algorithm's picks stay behind the table's, in case the provider
195
+ // lists the paired model but refuses it to this account at call time.
196
+ if (paired) {
197
+ const backups = cheaper.filter((model) => model !== paired.model).map((model): Candidate => ({ model, source: "economical" }));
198
+ return { candidates: [{ model: paired.model, source: "paired" }, ...backups, { model: sessionModel, source: "session" }], notices: [] };
199
+ }
200
+ if (cheaper.length === 0) {
201
+ return sessionOnly({
202
+ reason: "no-qualifying-model",
203
+ text: `Auto mode is screening calls with ${spec(sessionModel)}, this session's model, because no cheaper same-provider/route model in its ${intrinsicTier(sessionModel)} tier or above contains its ${sessionModel.contextWindow}-token catalog context window.`,
204
+ });
205
+ }
206
+ // The session stays last as the availability fallback.
207
+ return {
208
+ candidates: [...cheaper.map((model): Candidate => ({ model, source: "economical" })), { model: sessionModel, source: "session" }],
209
+ notices: [],
210
+ };
211
+ }
212
+
190
213
  /**
191
214
  * Whether an error means "this model is not usable on this account" — in which
192
215
  * case stepping to the next candidate is right — as opposed to a transient
@@ -222,11 +245,13 @@ export function describeCandidate(candidate: Candidate): string {
222
245
  const where = modelIdentity(candidate.model).profile ?? candidate.model.provider;
223
246
  switch (candidate.source) {
224
247
  case "configured":
225
- return `${name} (from autoMode.classifierModel)`;
248
+ return `${name} (autoMode.classifierModel)`;
249
+ case "paired":
250
+ return `${name} (paired with the session model in One Code's model pairing table)`;
226
251
  case "economical":
227
- return `${name} (cheapest model within ${where} that meets the capability floor — measured coding index when an Artificial Analysis snapshot is cached, else the session's tier)`;
252
+ return `${name} (cheapest model within ${where} in the session's tier or above that contains the session catalog context window)`;
228
253
  case "session":
229
- return `${name} (this session's model — nothing cheaper within ${where} meets the capability floor)`;
254
+ return `${name} (this session's model)`;
230
255
  }
231
256
  }
232
257
 
@@ -15,7 +15,7 @@
15
15
  * and every caller treats that as "not provably contained".
16
16
  */
17
17
 
18
- import { lstatSync, readlinkSync } from "node:fs";
18
+ import { type Dirent, lstatSync, readdirSync, readlinkSync } from "node:fs";
19
19
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
20
20
  import { comparablePath, expandTilde, gitBashPathToNative, isPathAtOrUnder, tryRealpath } from "../lib/paths.ts";
21
21
 
@@ -41,6 +41,19 @@ export function pathArgument(input: Record<string, unknown> | undefined): string
41
41
  return typeof path === "string" ? path : undefined;
42
42
  }
43
43
 
44
+ /**
45
+ * The fix to name when a call spells its target in two path fields with
46
+ * different values (`path` and `file_path`), undefined otherwise. The tool
47
+ * validator keeps both and the tool reads only one, so a gate that picked the
48
+ * other judged a file the call never touches
49
+ * (AUTO-MODE-SECURITY-REVIEW-2026-09-24 H1).
50
+ */
51
+ export function conflictingPathArguments(input: Record<string, unknown> | undefined): string | undefined {
52
+ const fields = ["path", "file_path", "notebook_path"].filter((key) => input?.[key] !== undefined);
53
+ if (new Set(fields.map((key) => input?.[key])).size < 2) return undefined;
54
+ return `This call names its target in ${fields.map((key) => `\`${key}\``).join(" and ")} with different values. Pass the target once, in the field the tool's schema defines.`;
55
+ }
56
+
44
57
  /** The shared comparison form (lib/paths.ts): `/` separators, case-folded on darwin/win32. */
45
58
  const normalize = comparablePath;
46
59
 
@@ -50,21 +63,32 @@ const normalize = comparablePath;
50
63
  * resolved — callers must not treat that as contained.
51
64
  */
52
65
  export function resolveForContainment(target: string): string | undefined {
66
+ const resolved = resolveThroughLinks(target);
67
+ return resolved === undefined ? undefined : normalize(resolved);
68
+ }
69
+
70
+ /**
71
+ * {@link resolveForContainment} in the filesystem's own spelling (not
72
+ * case-folded), for showing where a path lands.
73
+ */
74
+ export function resolveThroughLinks(target: string, hops = 0): string | undefined {
53
75
  const absolute = resolve(target);
54
76
 
55
77
  const direct = tryRealpath(absolute);
56
- if (direct) return normalize(direct);
78
+ if (direct) return direct;
57
79
 
58
80
  // The leaf may be a dangling symlink: realpath fails on it, but writes still
59
81
  // follow it. Read the link and resolve its target instead of assuming the
60
82
  // literal path (N9).
61
83
  try {
62
84
  const stats = lstatSync(absolute);
63
- if (stats.isSymbolicLink()) {
85
+ // A link cycle (`a -> a`) stops at the kernel's usual 40 hops, then
86
+ // resolves like any other leaf, below, instead of overflowing the stack.
87
+ if (stats.isSymbolicLink() && hops < 40) {
64
88
  const linkTarget = readlinkSync(absolute);
65
89
  const resolvedTarget = isAbsolute(linkTarget) ? linkTarget : resolve(dirname(absolute), linkTarget);
66
90
  // One hop is enough: the target's own realpath covers the rest of a chain.
67
- return resolveForContainment(resolvedTarget) ?? normalize(resolvedTarget);
91
+ return resolveThroughLinks(resolvedTarget, hops + 1) ?? resolvedTarget;
68
92
  }
69
93
  } catch {
70
94
  // Does not exist yet — fall through to ancestor resolution below.
@@ -79,7 +103,7 @@ export function resolveForContainment(target: string): string | undefined {
79
103
  if (parent === cursor) return undefined;
80
104
  tail.unshift(basename(cursor));
81
105
  const realParent = tryRealpath(parent);
82
- if (realParent) return normalize(join(realParent, ...tail));
106
+ if (realParent) return join(realParent, ...tail);
83
107
  cursor = parent;
84
108
  }
85
109
  }
@@ -117,3 +141,31 @@ export function toAbsolute(cwd: string, token: string, home: string): string {
117
141
  export function toAbsoluteBash(cwd: string, token: string, home: string): string {
118
142
  return toAbsolute(cwd, gitBashPathToNative(token), home);
119
143
  }
144
+
145
+ /**
146
+ * Whether moving, copying or removing `source` touches a path `guarded`
147
+ * flags beneath it: walks `source` without following symlinks, at most
148
+ * `budget` entries, and tests each entry's counterpart under `target` (the
149
+ * same path for a removal). A walk that runs out of budget counts as touching
150
+ * one, so the command is judged, not cleared. A file source walks nothing.
151
+ * `guarded` also gets the source's directory entry (a symbolic link is one).
152
+ */
153
+ export function touchesGuardedPath(source: string, target: string, guarded: (path: string, entry: Dirent) => boolean, budget = 5_000): boolean {
154
+ const stack = [""];
155
+ let seen = 0;
156
+ while (stack.length > 0) {
157
+ const relative = stack.pop() as string;
158
+ let entries;
159
+ try {
160
+ entries = readdirSync(join(source, relative), { withFileTypes: true });
161
+ } catch {
162
+ continue;
163
+ }
164
+ for (const entry of entries) {
165
+ const child = join(relative, entry.name);
166
+ if (++seen > budget || guarded(join(target, child), entry)) return true;
167
+ if (entry.isDirectory()) stack.push(child);
168
+ }
169
+ }
170
+ return false;
171
+ }
@@ -12,6 +12,27 @@
12
12
  export const CONSECUTIVE_BLOCK_LIMIT = 3;
13
13
  export const TOTAL_BLOCK_LIMIT = 20;
14
14
 
15
+ /**
16
+ * How long a timed resume prompt waits before denying the call, as Claude
17
+ * Code's denial-limit fallback does (2.1.282: 120 s, overridden by
18
+ * `CLAUDE_CODE_TICKLISH_WHISPER_TIMEOUT_MS`), so an unattended session is not
19
+ * held by a prompt nobody will answer.
20
+ */
21
+ export const UNATTENDED_PROMPT_TIMEOUT_MS = 120_000;
22
+
23
+ export function unattendedPromptTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
24
+ const value = Number(env.CLAUDE_CODE_TICKLISH_WHISPER_TIMEOUT_MS);
25
+ return Number.isFinite(value) && value > 0 ? value : UNATTENDED_PROMPT_TIMEOUT_MS;
26
+ }
27
+
28
+ /** Claude Code's warning line on a timed prompt, in its "about N minutes" form (the dialog adds a live countdown). */
29
+ export function unattendedPromptNotice(timeoutMs: number): string {
30
+ const minutes = Math.round(timeoutMs / 60_000);
31
+ const seconds = Math.max(1, Math.round(timeoutMs / 1000));
32
+ const left = timeoutMs >= 60_000 ? `about ${minutes} ${minutes === 1 ? "minute" : "minutes"}` : `about ${seconds} ${seconds === 1 ? "second" : "seconds"}`;
33
+ return `⚠ One Code will automatically deny this request in ${left}, to avoid blocking progress on an unattended session`;
34
+ }
35
+
15
36
  export interface Denial {
16
37
  toolName: string;
17
38
  /** The command or path the call targeted, for the /permissions listing. */
@@ -33,6 +54,10 @@ export class PauseTracker {
33
54
  private consecutive = 0;
34
55
  private total = 0;
35
56
  private paused = false;
57
+ /** Which limit tripped the current pause. */
58
+ private trippedBy: "consecutive" | "total" | undefined;
59
+ /** The timed resume prompt was shown in this block streak (Claude Code's `timedFallbackShown`). */
60
+ private timedShown = false;
36
61
  private readonly denials: Denial[] = [];
37
62
 
38
63
  /** Record a classifier block. Returns true if this one tripped the pause. */
@@ -44,10 +69,12 @@ export class PauseTracker {
44
69
 
45
70
  if (this.consecutive >= CONSECUTIVE_BLOCK_LIMIT) {
46
71
  this.paused = true;
72
+ this.trippedBy = "consecutive";
47
73
  return true;
48
74
  }
49
75
  if (this.total >= TOTAL_BLOCK_LIMIT) {
50
76
  this.paused = true;
77
+ this.trippedBy = "total";
51
78
  // The total counter resets when it is what triggered the fallback.
52
79
  // Without this, resuming leaves it at the limit and the very next block
53
80
  // re-pauses immediately, making the resume single-use.
@@ -57,9 +84,33 @@ export class PauseTracker {
57
84
  return false;
58
85
  }
59
86
 
60
- /** A classifier approval breaks a consecutive-block run. */
61
- recordAllow(): void {
87
+ /**
88
+ * Any allowed call in auto mode breaks a consecutive-block run, as in Claude
89
+ * Code, whose denial count resets on every allow in auto mode (2.1.282).
90
+ * Claude Code has no pause state: a block falls back to a prompt only while
91
+ * the count is at the limit, so an allow ends a pause the consecutive limit
92
+ * tripped. A 20-total pause still waits for the user. Returns true when this
93
+ * allow ended a pause.
94
+ */
95
+ recordAllow(): boolean {
62
96
  this.consecutive = 0;
97
+ this.timedShown = false;
98
+ if (!this.paused || this.trippedBy !== "consecutive") return false;
99
+ this.paused = false;
100
+ this.trippedBy = undefined;
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * Whether this resume prompt auto-denies after `unattendedPromptTimeoutMs`,
106
+ * and marks it shown. As in Claude Code, only a pause the consecutive limit
107
+ * tripped is timed, and only once until the streak breaks: the next prompt
108
+ * in the same streak waits for the user. A 20-total pause always waits.
109
+ */
110
+ takeTimedPrompt(): boolean {
111
+ if (!this.paused || this.trippedBy !== "consecutive" || this.timedShown) return false;
112
+ this.timedShown = true;
113
+ return true;
63
114
  }
64
115
 
65
116
  /**
@@ -70,6 +121,8 @@ export class PauseTracker {
70
121
  resume(): void {
71
122
  this.paused = false;
72
123
  this.consecutive = 0;
124
+ this.trippedBy = undefined;
125
+ this.timedShown = false;
73
126
  }
74
127
 
75
128
  isPaused(): boolean {
@@ -81,6 +134,8 @@ export class PauseTracker {
81
134
  this.consecutive = 0;
82
135
  this.total = 0;
83
136
  this.paused = false;
137
+ this.trippedBy = undefined;
138
+ this.timedShown = false;
84
139
  this.denials.length = 0;
85
140
  }
86
141