one-code-extension 0.4.0 → 0.5.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 (419) hide show
  1. package/README.md +219 -219
  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 +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -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 +66 -34
  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 +123 -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 +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -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 +52 -0
  63. package/extensions/background/wakeup.ts +159 -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 +88 -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 +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +19 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -22,7 +22,7 @@
22
22
  * `~/.claude`: One Code used to write them there, and a stale value (a model the
23
23
  * session cannot reach) must not keep biting. Every write goes to
24
24
  * `~/.onecode/settings.json`; One Code never mutates Claude Code's files. See
25
- * "Own state, borrowed config" in docs/decisions/memory-state.md.
25
+ * "Own state, borrowed config" in working-docs/decisions/memory-state.md.
26
26
  *
27
27
  * ## The customization surface (CC 2.1.233's own)
28
28
  *
@@ -42,11 +42,12 @@
42
42
  *
43
43
  * (The rule lists were briefly retired between the ruleset adoption and CC
44
44
  * 2.1.233 shipping `/auto-mode-setup`, which writes this exact schema — see
45
- * docs/decisions/auto-mode.md.)
45
+ * working-docs/decisions/auto-mode.md.)
46
46
  */
47
47
 
48
48
  import { existsSync, readFileSync } from "node:fs";
49
- import { claudeUserSettingsPath } from "../lib/claude-settings.ts";
49
+ import { claudeUserSettingsPath, managedSettingsPaths } from "../lib/claude-settings.ts";
50
+ import { claudeSourcesOn } from "../lib/config-mode.ts";
50
51
  import { oneCodeSettingsPath, readSettingsForWrite, writeSettings } from "../lib/one-code-settings.ts";
51
52
  import { DEFAULT_ENVIRONMENT, slotName } from "./defaults.ts";
52
53
 
@@ -61,12 +62,12 @@ export interface AutoModeConfig {
61
62
  allow: string[];
62
63
  /** Suspend narrow Bash allow rules so every shell command reaches the classifier. */
63
64
  classifyAllShell: boolean;
64
- /** `provider/model-id` override for the classifier model. */
65
+ /** `provider/model-id` the user chose for the classifier (`/auto-mode model`). */
65
66
  classifierModel?: string;
66
67
  /**
67
68
  * Containment identity (`modelIdentity().containment`) the `classifierModel`
68
69
  * was stamped for when set via `/auto-mode model`. A cross-provider setting
69
- * whose stamp no longer matches the session is treated as stale and overridden
70
+ * whose stamp no longer matches the session is treated as stale and replaced
70
71
  * — see `classifierCandidates`. Undefined for a hand-edited setting.
71
72
  */
72
73
  classifierModelSetFor?: string;
@@ -91,21 +92,16 @@ interface AutoModeSettingsFile {
91
92
 
92
93
  const DEFAULTS_TOKEN = "$defaults";
93
94
 
94
- /** Managed-settings locations, highest authority, matching Claude Code's paths. */
95
- export function managedSettingsPaths(): string[] {
96
- if (process.platform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-settings.json"];
97
- if (process.platform === "win32") return ["C:\\ProgramData\\ClaudeCode\\managed-settings.json"];
98
- return ["/etc/claude-code/managed-settings.json"];
99
- }
100
-
101
95
  /**
102
96
  * The files `autoMode` is read from, lowest precedence first. One Code's own
103
97
  * auto-mode keys (`classifierModel`, `classifierModelSetFor`) are deliberately
104
- * NOT collected from the borrowed `claudeUserSettingsPath` — Claude Code does not
105
- * define them, and One Code once wrote them there, so a stale value must not
106
- * survive the move to `~/.onecode` (see the read guard in the loader).
98
+ * NOT collected from the borrowed `claudeUserSettingsPath` (see the read guard
99
+ * in the loader).
107
100
  */
108
101
  export function autoModeSettingsPaths(home: string): string[] {
102
+ // Independent mode (lib/config-mode.ts) reads One Code's file alone; the
103
+ // built-in rules apply regardless, since settings only append to them.
104
+ if (!claudeSourcesOn()) return [oneCodeSettingsPath(home)];
109
105
  return [claudeUserSettingsPath(home), oneCodeSettingsPath(home), ...managedSettingsPaths()];
110
106
  }
111
107
 
@@ -285,7 +281,7 @@ export function loadAutoModeConfigWithDiagnostics(home: string): AutoModeConfigL
285
281
  // `classifierModel` is One Code's own key, not Claude Code's. It is read from
286
282
  // `~/.onecode` and managed settings only — never from `~/.claude`, where an
287
283
  // old One Code build may have left a stale value. A leftover in `~/.claude` is
288
- // surfaced as a diagnostic so the user knows why it stopped applying.
284
+ // surfaced as a diagnostic so the user knows why it does not apply.
289
285
  if (typeof block.classifierModel === "string" && block.classifierModel.trim()) {
290
286
  if (path === claudeUser) {
291
287
  diagnostics.push(
@@ -331,9 +327,8 @@ export function loadAutoModeConfig(home: string): AutoModeConfig {
331
327
  /**
332
328
  * Persist `autoMode.classifierModel` in One Code's own settings file
333
329
  * (`~/.onecode/settings.json`), preserving every other key. `undefined` removes
334
- * the setting. Never touches Claude Code's files — this is One Code's key, and
335
- * the knob that may move the classifier to another provider, so it lives in One
336
- * Code's own state (see the module comment).
330
+ * the setting. Never touches Claude Code's files: this is One Code's key, and
331
+ * the knob that may move the classifier to another provider.
337
332
  *
338
333
  * Unlike loading, this throws on a malformed file: a lenient read merely skips
339
334
  * rules, but a lenient write would replace the whole settings file with only ours.
@@ -369,7 +364,7 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
369
364
  * Persist a `/auto-mode setup` result to One Code's own settings file: replace
370
365
  * the wizard-owned keys (environment + the three rule lists — an undefined list
371
366
  * removes stale values, last-writer like CC's own wizard) while preserving every
372
- * other settings key and the non-wizard autoMode keys (classifierModel, …).
367
+ * other settings key and non-wizard autoMode keys.
373
368
  * Never touches Claude Code's files; throws on a malformed file rather than
374
369
  * replacing the settings with only ours.
375
370
  */
@@ -387,6 +382,63 @@ export function persistAutoModeSetup(patch: Record<string, string[] | undefined>
387
382
  writeSettings(path, file);
388
383
  }
389
384
 
385
+ /** The auto-mode keys the /permissions Auto mode tab lists and edits. */
386
+ export type AutoModeListKey = "allow" | "soft_deny" | "hard_deny" | "environment";
387
+
388
+ export interface SourcedAutoModeEntry {
389
+ key: AutoModeListKey;
390
+ text: string;
391
+ source: "claude-user" | "onecode-user" | "managed";
392
+ path: string;
393
+ }
394
+
395
+ /**
396
+ * Every auto-mode rule and environment entry with the file it came from, in
397
+ * load order. `"$defaults"` is left out: in the rule lists it means nothing
398
+ * here (the built-ins always apply), and in the environment it is the
399
+ * splice point, shown as the built-in default rather than as an entry.
400
+ */
401
+ export function listAutoModeEntries(home: string): SourcedAutoModeEntry[] {
402
+ const claudeUser = claudeUserSettingsPath(home);
403
+ const oneCodeUser = oneCodeSettingsPath(home);
404
+ const entries: SourcedAutoModeEntry[] = [];
405
+ for (const path of autoModeSettingsPaths(home)) {
406
+ const block = asRecord(readFile(path, [])?.autoMode);
407
+ if (!block) continue;
408
+ const source = path === claudeUser ? "claude-user" : path === oneCodeUser ? "onecode-user" : "managed";
409
+ for (const key of ["allow", "soft_deny", "hard_deny", "environment"] as const) {
410
+ for (const text of stringArray(block[key]) ?? []) {
411
+ if (text.trim() !== DEFAULTS_TOKEN) entries.push({ key, text, source, path });
412
+ }
413
+ }
414
+ }
415
+ return entries;
416
+ }
417
+
418
+ /** Whether One Code's own `autoMode.environment` keeps the built-in default through `"$defaults"`. */
419
+ export function oneCodeEnvironmentExtendsDefault(home: string): boolean {
420
+ const block = asRecord(readFile(oneCodeSettingsPath(home), [])?.autoMode);
421
+ return (stringArray(block?.environment) ?? []).some((text) => text.trim() === DEFAULTS_TOKEN);
422
+ }
423
+
424
+ /**
425
+ * Rewrite one auto-mode list in One Code's own settings file. An emptied list
426
+ * is removed (for the environment, that restores the built-in default), and so
427
+ * is an emptied `autoMode` block. Throws on a malformed file, like the other
428
+ * writers.
429
+ */
430
+ export function updateOneCodeAutoModeList(key: AutoModeListKey, update: (entries: string[]) => string[], home: string): void {
431
+ const path = oneCodeSettingsPath(home);
432
+ const file = readSettingsForWrite(path);
433
+ const block = asRecord(file.autoMode) ?? {};
434
+ const next = update(stringArray(block[key]) ?? []);
435
+ if (next.length === 0) delete block[key];
436
+ else block[key] = next;
437
+ if (Object.keys(block).length === 0) delete file.autoMode;
438
+ else file.autoMode = block;
439
+ writeSettings(path, file);
440
+ }
441
+
390
442
  /**
391
443
  * The `permissions.allow` entries in One Code's own settings file — what the
392
444
  * setup audit can actually remove. Broad rules that live in Claude Code's files
@@ -38,6 +38,8 @@ export interface DecisionEntry {
38
38
  raw?: string;
39
39
  /** `provider/id` of the model that produced a classifier verdict. */
40
40
  model?: string;
41
+ /** The provider rejected the full classifier transcript as too long, not unsafe. */
42
+ transcriptTooLong?: true;
41
43
  }
42
44
 
43
45
  const SUBJECT_LIMIT = 300;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The ruleset itself is no longer configurable prose lists — the classifier
5
5
  * ruleset is a fixed monolith matching Claude Code's classifier (see
6
- * classifier-prompt.ts and docs/decisions/auto-mode.md). The one customization
6
+ * classifier-prompt.ts and working-docs/decisions/auto-mode.md). The one customization
7
7
  * surface Claude Code exposes, and the only one we expose, is the `## Environment` section:
8
8
  * `DEFAULT_ENVIRONMENT` is CC's default slot text, and a user's `autoMode.environment`
9
9
  * entries replace it. Re-exported here so config.ts has a single stable import
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Calls the auto-mode classifier denied this session, and the one-shot grants
3
+ * the user mints by approving them in `/permissions` (pure).
4
+ *
5
+ * Claude Code records every classifier denial (newest first, at most 20) for
6
+ * its "Recently denied" tab, keyed by tool and exact input. Approving one there
7
+ * writes no rule and bypasses nothing: the model is told it may retry, and the
8
+ * retry goes through the classifier again (findings §33). One Code's classifier
9
+ * takes user intent only from the user's own typed input, so a retry would
10
+ * most likely be blocked again. Here an approval instead mints a grant for that
11
+ * exact tool and input: the next call that matches it skips the classifier
12
+ * once, then the grant is spent. It carries the authority of answering "Yes"
13
+ * once on a permission prompt, and no more: a changed command, another
14
+ * directory or a second run is judged as usual, and deny rules and the safety
15
+ * floor are checked before any grant (working-docs/decisions/auto-mode.md,
16
+ * "Approving a denied call").
17
+ *
18
+ * The store lives in the permissions extension's memory for one session. It is
19
+ * never persisted, and a new session starts empty.
20
+ */
21
+
22
+ /** Claude Code's `MAX_DENIALS`. */
23
+ export const MAX_DENIALS = 20;
24
+
25
+ export interface AutoModeDenial {
26
+ /** Unique within the store, so a panel row outlives list changes. */
27
+ id: number;
28
+ /** The normalized tool name (`bash`, `write`, `mcp__server__tool`). */
29
+ toolName: string;
30
+ /** What the user sees for the call: `bash(rm -rf dist)`, its subject clipped at 200 characters for the panel. */
31
+ display: string;
32
+ /** The same call unclipped, for the text the model reads (the grant message, the breadcrumb). */
33
+ fullDisplay: string;
34
+ /** The exact call (`denialInputKey`). */
35
+ inputKey: string;
36
+ /** The classifier's reason (a grounded rule name, or the failure it explained). */
37
+ reason: string;
38
+ /** The grounded rule name, when the verdict cited one. */
39
+ rule?: string;
40
+ timestamp: number;
41
+ }
42
+
43
+ /** JSON with object keys sorted at every level, so two spellings of one input agree. */
44
+ function stableJson(value: unknown): string {
45
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
46
+ if (value && typeof value === "object") {
47
+ const entries = Object.entries(value as Record<string, unknown>)
48
+ .filter(([, v]) => v !== undefined)
49
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
50
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`).join(",")}}`;
51
+ }
52
+ return JSON.stringify(value) ?? "null";
53
+ }
54
+
55
+ /**
56
+ * The key a denial and its grant are matched by: the tool, the directory the
57
+ * call runs in, and its whole input. For a shell tool only the command counts,
58
+ * as in Claude Code (its `description` and `timeout` do not change what runs);
59
+ * for every other tool the whole input does. The directory is part of the key
60
+ * because the same command in another directory is another action.
61
+ */
62
+ export function denialInputKey(toolName: string, input: Record<string, unknown>, cwd: string, shell: boolean): string {
63
+ const counted = shell ? { command: input.command } : input;
64
+ return `${toolName}\0${cwd}\0${stableJson(counted)}`;
65
+ }
66
+
67
+ export class DenialStore {
68
+ private denials: AutoModeDenial[] = [];
69
+ /** Unspent grants per exact call: two approved denials of one call allow two retries. */
70
+ private readonly grants = new Map<string, number>();
71
+ private nextId = 1;
72
+
73
+ /** Record a classifier denial, newest first, keeping the last `MAX_DENIALS`. */
74
+ record(denial: Omit<AutoModeDenial, "id">): AutoModeDenial {
75
+ const entry = { ...denial, id: this.nextId++ };
76
+ this.denials = [entry, ...this.denials].slice(0, MAX_DENIALS);
77
+ return entry;
78
+ }
79
+
80
+ /** Newest first. */
81
+ list(): readonly AutoModeDenial[] {
82
+ return this.denials;
83
+ }
84
+
85
+ /**
86
+ * Approve denials by id: each leaves the list and mints a grant for its exact
87
+ * call. Returns the approved entries, in list order; unknown ids are skipped.
88
+ */
89
+ approve(ids: ReadonlySet<number>): AutoModeDenial[] {
90
+ const approved = this.denials.filter((denial) => ids.has(denial.id));
91
+ for (const denial of approved) this.grants.set(denial.inputKey, (this.grants.get(denial.inputKey) ?? 0) + 1);
92
+ this.denials = this.denials.filter((denial) => !ids.has(denial.id));
93
+ return approved;
94
+ }
95
+
96
+ /** Spend one grant for this exact call. True once per approval. */
97
+ takeGrant(inputKey: string): boolean {
98
+ const left = this.grants.get(inputKey);
99
+ if (!left) return false;
100
+ if (left === 1) this.grants.delete(inputKey);
101
+ else this.grants.set(inputKey, left - 1);
102
+ return true;
103
+ }
104
+
105
+ /** A call that went through another way drops its earlier denials (Claude Code does the same). */
106
+ settle(inputKey: string): void {
107
+ this.denials = this.denials.filter((denial) => denial.inputKey !== inputKey);
108
+ }
109
+
110
+ /** A new session starts with no denials and no grants. */
111
+ reset(): void {
112
+ this.denials = [];
113
+ this.grants.clear();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * The message the model gets when the user approves denials in `/permissions`:
119
+ * Claude Code's wording, verbatim (findings §33).
120
+ */
121
+ export function permissionGrantedMessage(displays: readonly string[]): string {
122
+ return `Permission granted for: ${displays.join(", ")}. You may now retry ${displays.length === 1 ? "this command" : "these commands"} if you would like.`;
123
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Whether a checkout's own git configuration makes a read-only git command run
3
+ * a program (pure apart from fs reads).
4
+ *
5
+ * The pre-gate fast-paths `git status`, `diff`, `log` and the other read-only
6
+ * subcommands by their options (read-only-options.ts). git honours the
7
+ * repository's `.git/config`, and some keys there name a program that those
8
+ * reads run: `core.fsmonitor` on every index read, a `filter.<driver>.clean`
9
+ * when `status` re-hashes a file, `diff.external` and the diff and textconv
10
+ * drivers on `diff`/`log -p`/`show`, `gpg.program` on `log --show-signature`
11
+ * or a `%G?` format, a pager. `status` also rewrites the index opportunistically,
12
+ * which runs a `post-index-change` hook. A directory whose `.git` came from an
13
+ * archive, a synced folder or a copied tree can carry any of these, so a git
14
+ * read there is not provably read-only and goes to the classifier (PR #8
15
+ * review; SECURITY-REVIEW-2026-09-23's folder-trust residual).
16
+ *
17
+ * Only repository-scoped files are read: the checkout's `config`, its
18
+ * `config.worktree`, every submodule's config, and the hooks
19
+ * directories (`core.hooksPath` included). The user's own global and system
20
+ * configuration is theirs, and git-lfs's standard filter commands pass. The
21
+ * scan is textual and wide on purpose: an unneeded escalation costs one
22
+ * classifier call, a missed key would be code execution behind a "safe", so
23
+ * anything it cannot read counts as a program too.
24
+ */
25
+
26
+ import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
27
+ import { isAbsolute, join, resolve } from "node:path";
28
+ import { findGitRoot, gitdirFileTarget } from "../lib/git.ts";
29
+ import { expandTilde } from "../lib/paths.ts";
30
+
31
+ const BOOLEAN = /^(true|false|yes|no|on|off|1|0)?$/i;
32
+
33
+ /** git-lfs's own filter commands (`git lfs install`), resolved on the user's PATH. */
34
+ const LFS_FILTERS = new Set(["git-lfs clean -- %f", "git-lfs smudge -- %f", "git-lfs filter-process"]);
35
+
36
+ /** Keys whose value git runs during a read, by section (subsections included). */
37
+ const PROGRAM_KEYS: Record<string, (key: string, value: string | undefined) => boolean> = {
38
+ // `core.fsmonitor=true` is git's own daemon; any other value is a hook command.
39
+ core: (key, value) => (key === "fsmonitor" && !BOOLEAN.test(value ?? "")) || key === "pager",
40
+ diff: (key) => key === "external" || key === "command" || key === "textconv",
41
+ filter: (key, value) => (key === "clean" || key === "smudge" || key === "process") && !LFS_FILTERS.has(value ?? ""),
42
+ pager: (_key, value) => !BOOLEAN.test(value ?? ""),
43
+ gpg: (key) => key === "program",
44
+ // An included file is not followed; it could hold any of the above.
45
+ include: (key) => key === "path",
46
+ includeif: (key) => key === "path",
47
+ };
48
+
49
+ /** One git config file's entries, section names lowercased without their subsection. */
50
+ function configEntries(text: string): { section: string; key: string; value: string | undefined }[] {
51
+ const entries: { section: string; key: string; value: string | undefined }[] = [];
52
+ let section = "";
53
+ for (const raw of text.split(/\r?\n/)) {
54
+ let line = raw.trim();
55
+ const header = /^\[\s*([A-Za-z0-9.-]+)(?:\s+"(?:[^"\\]|\\.)*")?\s*\]/.exec(line);
56
+ if (header) {
57
+ // `[diff "x"]` and the old `[diff.x]` spelling both name section `diff`.
58
+ section = header[1].split(".")[0].toLowerCase();
59
+ line = line.slice(header[0].length).trim();
60
+ }
61
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
62
+ const entry = /^([A-Za-z][A-Za-z0-9-]*)\s*(?:=\s*(.*))?$/.exec(line);
63
+ if (entry) entries.push({ section, key: entry[1].toLowerCase(), value: entry[2]?.trim().replace(/^"(.*)"$/, "$1") });
64
+ }
65
+ return entries;
66
+ }
67
+
68
+ /**
69
+ * What one git config file's text names: the first program-valued key, as
70
+ * `section.key`, and every `core.hooksPath`. A hooks path is not a program by
71
+ * itself (husky sets one); the caller looks there for the hook a read runs.
72
+ */
73
+ export function configRunsProgram(text: string): { program?: string; hooksPaths: string[] } {
74
+ const hooksPaths: string[] = [];
75
+ for (const { section, key, value } of configEntries(text)) {
76
+ if (section === "core" && key === "hookspath" && value) hooksPaths.push(value);
77
+ else if (PROGRAM_KEYS[section]?.(key, value)) return { program: `${section}.${key}`, hooksPaths };
78
+ }
79
+ return { hooksPaths };
80
+ }
81
+
82
+ /** The checkout holding `dir`: its worktree root, git dir and common dir, or undefined outside one. */
83
+ function checkoutOf(dir: string): { root: string; gitDir: string; commonDir: string } | undefined {
84
+ const root = findGitRoot(dir);
85
+ if (!root) return undefined;
86
+ const gitDir = gitdirFileTarget(root) ?? join(root, ".git");
87
+ let commonDir = gitDir;
88
+ try {
89
+ const common = readFileSync(join(gitDir, "commondir"), "utf-8").trim();
90
+ if (common) commonDir = isAbsolute(common) ? common : resolve(gitDir, common);
91
+ } catch {
92
+ // Not a linked worktree: the git dir is the common dir.
93
+ }
94
+ return { root, gitDir, commonDir };
95
+ }
96
+
97
+ /** A missing file or directory, as opposed to one this process cannot read. */
98
+ function isMissing(error: unknown): boolean {
99
+ const code = (error as NodeJS.ErrnoException).code;
100
+ return code === "ENOENT" || code === "ENOTDIR";
101
+ }
102
+
103
+ /**
104
+ * Every submodule git dir `status` can recurse into, or undefined when the
105
+ * search cannot finish (an unreadable directory, more than 200 repositories,
106
+ * nesting past 32 levels), which the caller treats as a program. Found two
107
+ * ways: the standard layout under `<commonDir>/modules` (a nested submodule's
108
+ * under its parent's own `modules/`; a slashed submodule name is a directory
109
+ * chain), and each `.gitmodules` path whose `.git` points somewhere else.
110
+ */
111
+ function submoduleGitDirs(root: string, commonDir: string): string[] | undefined {
112
+ const found = new Set<string>();
113
+ const walk = (dir: string, depth: number): boolean => {
114
+ if (depth > 32) return false;
115
+ let entries: Dirent[];
116
+ try {
117
+ entries = readdirSync(dir, { withFileTypes: true });
118
+ } catch (error) {
119
+ return isMissing(error);
120
+ }
121
+ if (entries.some((entry) => entry.name === "HEAD" || entry.name === "config")) {
122
+ found.add(dir);
123
+ return found.size <= 200 && walk(join(dir, "modules"), depth + 1);
124
+ }
125
+ return entries.every((entry) => !entry.isDirectory() || walk(join(dir, entry.name), depth + 1));
126
+ };
127
+ if (!walk(join(commonDir, "modules"), 0)) return undefined;
128
+ let gitmodules = "";
129
+ try {
130
+ gitmodules = readFileSync(join(root, ".gitmodules"), "utf-8");
131
+ } catch (error) {
132
+ if (!isMissing(error)) return undefined;
133
+ }
134
+ for (const { section, key, value } of configEntries(gitmodules)) {
135
+ if (section !== "submodule" || key !== "path" || !value) continue;
136
+ const worktree = resolve(root, value);
137
+ const dotGit = join(worktree, ".git");
138
+ const gitDir = gitdirFileTarget(worktree) ?? (existsSync(dotGit) ? dotGit : undefined);
139
+ if (gitDir) found.add(gitDir);
140
+ if (found.size > 200) return undefined;
141
+ }
142
+ return [...found];
143
+ }
144
+
145
+ /** The hooks a git read can run: `status` rewrites the index. */
146
+ export const READ_HOOKS = ["post-index-change"] as const;
147
+
148
+ /**
149
+ * Why a git command in `dir` could run a program the checkout names, or
150
+ * undefined when its repository-scoped configuration names none (or `dir` is
151
+ * in no checkout). `hooks` are the hook names the command can run; `home`
152
+ * expands a `~` hooks path.
153
+ */
154
+ export function checkoutGitRunsProgram(dir: string, home: string, hooks: readonly string[] = READ_HOOKS): string | undefined {
155
+ const checkout = checkoutOf(dir);
156
+ if (!checkout) return undefined;
157
+ const { root, gitDir, commonDir } = checkout;
158
+ const modules = submoduleGitDirs(root, commonDir);
159
+ if (!modules) return "the checkout's submodules cannot all be read";
160
+ // `status` rewrites the index, which runs `post-index-change`; it recurses
161
+ // into submodules, each with its own hooks directory.
162
+ const hooksDirs = [join(commonDir, "hooks"), ...modules.map((module) => join(module, "hooks"))];
163
+ for (const file of new Set([join(commonDir, "config"), join(commonDir, "config.worktree"), join(gitDir, "config.worktree")])) {
164
+ const found = readConfig(file);
165
+ if (!found) return "the checkout's git config cannot be read";
166
+ if (found.program) return `the checkout's git config sets ${found.program}, which names a program git runs`;
167
+ for (const path of found.hooksPaths) hooksDirs.push(resolve(root, expandTilde(path, home)));
168
+ }
169
+ for (const module of modules) {
170
+ const found = readConfig(join(module, "config"));
171
+ // A submodule's relative hooks path is resolved against a worktree this check does not map.
172
+ if (!found || found.program || found.hooksPaths.length > 0) {
173
+ return `a submodule's git config ${found ? `sets ${found.program ?? "core.hookspath"}, which names a program git runs` : "cannot be read"}`;
174
+ }
175
+ }
176
+ const hook = hooks.find((name) => hooksDirs.some((dir) => existsSync(join(dir, name))));
177
+ return hook && `the checkout has a ${hook} hook, which git runs`;
178
+ }
179
+
180
+ /** What a config file names; empty when it does not exist, undefined when it cannot be read. */
181
+ function readConfig(file: string): { program?: string; hooksPaths: string[] } | undefined {
182
+ try {
183
+ return configRunsProgram(readFileSync(file, "utf-8"));
184
+ } catch (error) {
185
+ return isMissing(error) ? { hooksPaths: [] } : undefined;
186
+ }
187
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The auto-mode transcript's `{"meta":{"gitStatus":…}}` line (pure), in the
3
+ * Claude Code-compatible shape the ruleset describes.
4
+ *
5
+ * Before a shell command that can destroy uncommitted work, the harness runs
6
+ * `git status` itself and puts the result directly above that command in the
7
+ * classifier's `<transcript>`, so the classifier judges the tree's real state
8
+ * instead of presuming it dirty. The ruleset says how to read it ("A
9
+ * `gitStatus` of `{"clean":true}` clears the Irreversible Local Destruction
10
+ * presume-dirty for that command; staged/modified/untracked counts … confirm
11
+ * it"). A command qualifies when any pattern of a qualifying category
12
+ * (`GIT_STATUS_CATEGORIES`) matches it, `monitor` commands included.
13
+ */
14
+
15
+ type Pattern = { pattern: RegExp; category: string };
16
+
17
+ /** Bash command categories; `destructiveCategory` names a command by the first match. */
18
+ const BASH_PATTERNS: Pattern[] = [
19
+ { pattern: /\bgit\s+reset\s+--hard\b/, category: "git_reset_hard" },
20
+ { pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/, category: "git_force_push" },
21
+ { pattern: /\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/, category: "git_clean_force" },
22
+ { pattern: /\bgit\s+checkout\s+(--\s+)?\.[ \t]*($|[;&|\n])/, category: "git_checkout_dot" },
23
+ { pattern: /\bgit\s+restore\s+(--\s+)?\.[ \t]*($|[;&|\n])/, category: "git_restore_dot" },
24
+ { pattern: /\bgit\s+stash[ \t]+(drop|clear)\b/, category: "git_stash_drop" },
25
+ { pattern: /\bgit\s+branch\s+(-D[ \t]|--delete\s+--force|--force\s+--delete)\b/, category: "git_branch_force_delete" },
26
+ { pattern: /\bgit\s+(commit|push|merge)\b[^;&|\n]*--no-verify\b/, category: "git_no_verify" },
27
+ { pattern: /\bgit\s+commit\b[^;&|\n]*--amend\b/, category: "git_commit_amend" },
28
+ {
29
+ pattern: /(^|[;&|\n][ \t]*)rm\s+-[a-zA-Z]*[rR][a-zA-Z]*f|(^|[;&|\n][ \t]*)rm\s+-[a-zA-Z]*f[a-zA-Z]*[rR]/,
30
+ category: "rm_recursive_force",
31
+ },
32
+ { pattern: /(^|[;&|\n][ \t]*)rm\s+-[a-zA-Z]*[rR]/, category: "rm_recursive" },
33
+ { pattern: /(^|[;&|\n][ \t]*)rm\s+-[a-zA-Z]*f/, category: "rm_force" },
34
+ { pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i, category: "sql_drop_truncate" },
35
+ { pattern: /\bDELETE\s+FROM\s+\w+[ \t]*(;|"|'|\n|$)/i, category: "sql_delete_from" },
36
+ { pattern: /\bkubectl\s+delete\b/, category: "kubectl_delete" },
37
+ { pattern: /\bterraform\s+destroy\b/, category: "terraform_destroy" },
38
+ ];
39
+
40
+ const REMOVE = "(?:^|[|;&\\n({])[ \\t]*(Remove-Item|rm|del|rd|rmdir|ri)\\b[^|;&\\n}]*";
41
+
42
+ /** PowerShell command categories, in the same first-match order. */
43
+ const POWERSHELL_PATTERNS: Pattern[] = [
44
+ { pattern: new RegExp(`${REMOVE}-Recurse\\b[^|;&\\n}]*-Force\\b`, "i"), category: "remove_item_recursive_force" },
45
+ { pattern: new RegExp(`${REMOVE}-Force\\b[^|;&\\n}]*-Recurse\\b`, "i"), category: "remove_item_recursive_force" },
46
+ { pattern: new RegExp(`${REMOVE}-Recurse\\b`, "i"), category: "remove_item_recursive" },
47
+ { pattern: new RegExp(`${REMOVE}-Force\\b`, "i"), category: "remove_item_force" },
48
+ { pattern: /\bClear-Content\b[^|;&\n]*\*/i, category: "clear_content_glob" },
49
+ { pattern: /\bFormat-Volume\b/i, category: "format_volume" },
50
+ { pattern: /\bClear-Disk\b/i, category: "clear_disk" },
51
+ { pattern: /\bgit\s+reset\s+--hard\b/i, category: "git_reset_hard" },
52
+ { pattern: /\bgit\s+push\b[^|;&\n]*\s+(--force|--force-with-lease|-f)\b/i, category: "git_force_push" },
53
+ { pattern: /\bgit\s+clean\b(?![^|;&\n]*(?:-[a-zA-Z]*n|--dry-run))[^|;&\n]*-[a-zA-Z]*f/i, category: "git_clean_force" },
54
+ { pattern: /\bgit\s+stash\s+(drop|clear)\b/i, category: "git_stash_drop" },
55
+ { pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i, category: "sql_drop_truncate" },
56
+ { pattern: /\bStop-Computer\b/i, category: "stop_computer" },
57
+ { pattern: /\bRestart-Computer\b/i, category: "restart_computer" },
58
+ { pattern: /\bClear-RecycleBin\b/i, category: "clear_recycle_bin" },
59
+ ];
60
+
61
+ /** The categories that get a gitStatus line: the ones that can destroy uncommitted work. */
62
+ export const GIT_STATUS_CATEGORIES: ReadonlySet<string> = new Set([
63
+ "git_reset_hard",
64
+ "git_checkout_dot",
65
+ "git_restore_dot",
66
+ "git_clean_force",
67
+ "rm_recursive_force",
68
+ "rm_recursive",
69
+ "rm_force",
70
+ "remove_item_recursive_force",
71
+ "remove_item_recursive",
72
+ "remove_item_force",
73
+ "clear_content_glob",
74
+ ]);
75
+
76
+ /** The destructive category of a shell command (first match, first 10,000 characters), or undefined. */
77
+ export function destructiveCategory(tool: "bash" | "powershell", command: string): string | undefined {
78
+ const text = command.length > 10_000 ? command.slice(0, 10_000) : command;
79
+ const patterns = tool === "powershell" ? POWERSHELL_PATTERNS : BASH_PATTERNS;
80
+ return patterns.find(({ pattern }) => pattern.test(text))?.category;
81
+ }
82
+
83
+ /** The categories of every pattern this command matches (first 10,000 characters). */
84
+ function matchedCategories(tool: "bash" | "powershell", command: string): { text: string; categories: string[] } {
85
+ const text = command.length > 10_000 ? command.slice(0, 10_000) : command;
86
+ const patterns = tool === "powershell" ? POWERSHELL_PATTERNS : BASH_PATTERNS;
87
+ return { text, categories: patterns.filter(({ pattern }) => pattern.test(text)).map(({ category }) => category) };
88
+ }
89
+
90
+ /**
91
+ * Whether this shell command gets a gitStatus line above it in the transcript.
92
+ * Any qualifying pattern counts, not only the first match, so `git push
93
+ * --force x && rm -rf y` gets the line for its `rm -rf`: the line only ever
94
+ * adds ground truth.
95
+ */
96
+ export function wantsGitStatusMeta(tool: "bash" | "powershell", command: string): boolean {
97
+ return matchedCategories(tool, command).categories.some((category) => GIT_STATUS_CATEGORIES.has(category));
98
+ }
99
+
100
+ /** `git clean` deletes ignored files only with `-x`/`-X`. */
101
+ const GIT_CLEAN_IGNORED = /\bgit\s+clean\b[^;&|\n]*[ \t]-[a-zA-Z]*[xX]/i;
102
+
103
+ /**
104
+ * Whether this shell command can delete ignored files (`.env`, local builds),
105
+ * which `git status` hides and git cannot restore. Every non-git removal
106
+ * category can; of the git ones, only a `git clean -x`/`-X`.
107
+ */
108
+ export function reachesIgnoredFiles(tool: "bash" | "powershell", command: string): boolean {
109
+ const { text, categories } = matchedCategories(tool, command);
110
+ return categories.some((category) =>
111
+ category === "git_clean_force" ? GIT_CLEAN_IGNORED.test(text) : GIT_STATUS_CATEGORIES.has(category) && !category.startsWith("git_"),
112
+ );
113
+ }
114
+
115
+ /**
116
+ * The `git status` arguments for the line (the harness adds its own `-c`
117
+ * hardening), wider than Claude Code's `--ignore-submodules=dirty` (PR #8
118
+ * review). A dirty submodule always counts as modified: `rm -rf` deletes its
119
+ * work, and `git reset --hard --recurse-submodules` (or `submodule.recurse`,
120
+ * which the command line does not show) discards it. Ignored entries (`!!`,
121
+ * one per ignored directory) are listed only for a command that deletes them
122
+ * (`reachesIgnoredFiles`), so the line can withhold `{"clean":true}` there
123
+ * and keep it for a `git reset --hard` in a repo with an ignored `.env`.
124
+ */
125
+ export function gitStatusMetaArgs(reachesIgnored: boolean): readonly string[] {
126
+ const args = ["status", "--porcelain", "--ignore-submodules=none", "--untracked-files=normal"];
127
+ return reachesIgnored ? [...args, "--ignored=matching"] : args;
128
+ }
129
+
130
+ /** The line's gitStatus value: `{clean: true}`, or the tree's counts. */
131
+ export type GitStatusMeta = { clean: true } | { staged: number; modified: number; untracked: number };
132
+
133
+ /**
134
+ * Summarize `git status --porcelain` output as clean, or staged/modified/untracked
135
+ * counts. Undefined (no line: the classifier keeps presuming dirty) when only
136
+ * ignored entries remain, which the wider `gitStatusMetaArgs` list.
137
+ */
138
+ export function gitStatusMeta(porcelain: string): GitStatusMeta | undefined {
139
+ let staged = 0;
140
+ let modified = 0;
141
+ let untracked = 0;
142
+ let ignored = 0;
143
+ for (const line of porcelain.split("\n")) {
144
+ if (line.length < 2) continue;
145
+ const [index, worktree] = [line[0], line[1]];
146
+ if (index === "?" && worktree === "?") {
147
+ untracked++;
148
+ continue;
149
+ }
150
+ if (index === "!" && worktree === "!") {
151
+ ignored++;
152
+ continue;
153
+ }
154
+ if (index !== " " && index !== "?") staged++;
155
+ if (worktree !== " ") modified++;
156
+ }
157
+ if (staged !== 0 || modified !== 0 || untracked !== 0) return { staged, modified, untracked };
158
+ return ignored > 0 ? undefined : { clean: true };
159
+ }