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
@@ -0,0 +1,724 @@
1
+ /**
2
+ * Option tables for the shell pre-gate's read-only commands (pure).
3
+ *
4
+ * The pre-gate may conclude "safe" only when it can prove a command reads and
5
+ * nothing else. Until 2026-09-23 it proved that from the command's NAME and
6
+ * never looked at the options: `git diff --output=f` wrote a file, `git grep
7
+ * -O<prog>` and `rg --pre=<prog>` ran a program, `git branch -D` deleted a
8
+ * branch, and `uniq in out` overwrote `out`, all fast-pathed as reads
9
+ * (SECURITY-REVIEW-2026-09-23 H1). Each read-only command now has an explicit
10
+ * table of the options it may carry; an option that is not in the table
11
+ * escalates. A missing entry costs one classifier call, never a bypass.
12
+ *
13
+ * The tables must agree with how the real parser consumes values, or an
14
+ * option can hide inside another option's value. A required value is taken
15
+ * from the rest of a short-option cluster or from the next word, whatever
16
+ * that word looks like (getopt and git's parse-options both do this), so
17
+ * `-S -- --output=f` hides nothing: `--` is `-S`'s value and `--output=f` is
18
+ * still parsed as an option. Where the arity is uncertain the tables say
19
+ * "none", which only ever turns a value into an operand that gets read-checked.
20
+ */
21
+
22
+ /**
23
+ * How an option takes its value:
24
+ * - `none`: never (`--name=v` is refused);
25
+ * - `optional`: only attached (`--abbrev=7`, `-M50%`), never the next word;
26
+ * - `required`: attached, or else the next word, unconditionally;
27
+ * - `lastarg`: git's LASTARG_DEFAULT (`--contains [<commit>]`): the next word
28
+ * unless it starts with `-` or there is none;
29
+ * - `two`: the next two words (jq's `--arg name value`).
30
+ */
31
+ export type Arity = "none" | "optional" | "required" | "lastarg" | "two";
32
+
33
+ export interface OptionSpec {
34
+ options: Record<string, Arity>;
35
+ /**
36
+ * Every option is accepted and takes no value, so every following word is
37
+ * checked as an operand. Only for commands with no option that writes,
38
+ * executes, reaches the network or reads a list of files to open.
39
+ */
40
+ lenient?: boolean;
41
+ /** `-5`-style numeric options (`head -5`, `git log -3`). */
42
+ numeric?: boolean;
43
+ /** Options whose value names a file the command reads (checked like an operand). */
44
+ fileValues?: readonly string[];
45
+ /** The most operands the command may take and still only read (`uniq in out` writes `out`). */
46
+ maxPositionals?: number;
47
+ /** The first operand is a program, not a file (jq's filter). */
48
+ programFirst?: boolean;
49
+ /** A reason the operands make the command more than a read, or undefined. */
50
+ operandReason?: (operands: readonly Word[]) => string | undefined;
51
+ /** A reason a combination of options makes the command more than a checked read, or undefined. */
52
+ optionsReason?: (seen: ReadonlySet<string>) => string | undefined;
53
+ /**
54
+ * The command reads through symlinks found INSIDE a directory operand, one
55
+ * level down, unless one of these options is present. `diff dir1 dir2`
56
+ * compares same-named entries and dereferences them, so a symlink in a
57
+ * directory operand reaches outside with no `-r` needed
58
+ * (PREGATE-REVIEW-2026-09-23 A1). The analyzer checks it, because only it
59
+ * knows which operands resolve to directories.
60
+ */
61
+ dereferencesDirEntries?: readonly string[];
62
+ }
63
+
64
+ /** A shell word as the pre-gate tokenizer produces it (shell-analysis.ts `Token`). */
65
+ export interface Word {
66
+ value: string;
67
+ /** An unquoted glob character makes bash expand the word. */
68
+ glob?: boolean;
69
+ }
70
+
71
+ export interface ParsedOptions<W extends Word = Word> {
72
+ /** Operands in order: words that are not options or option values. */
73
+ positionals: W[];
74
+ /** Values of `fileValues` options: files the command reads (an attached value is a word of its own). */
75
+ fileValues: Word[];
76
+ /** Every option seen, as written in the table (`-e`, `--regexp`). */
77
+ seen: Set<string>;
78
+ }
79
+
80
+ export type OptionCheck<W extends Word = Word> = { ok: true; parsed: ParsedOptions<W> } | { ok: false; reason: string };
81
+
82
+ function words(list: string): string[] {
83
+ return list.split(/\s+/).filter(Boolean);
84
+ }
85
+
86
+ /** Build a table from space-separated option lists, one per arity. */
87
+ export function table(arities: Partial<Record<Arity, string>>, extra: Omit<OptionSpec, "options"> = {}): OptionSpec {
88
+ const options: Record<string, Arity> = {};
89
+ for (const [arity, list] of Object.entries(arities) as [Arity, string][]) {
90
+ for (const option of words(list)) options[option] = arity;
91
+ }
92
+ return { options, ...extra };
93
+ }
94
+
95
+ /** The only fields merge() carries; anything else is an operand rule or hook that must sit on the merged spec. */
96
+ const MERGEABLE_FIELDS = new Set(["options", "numeric", "fileValues"]);
97
+
98
+ function merge(...specs: OptionSpec[]): OptionSpec {
99
+ const merged: OptionSpec = { options: {} };
100
+ for (const spec of specs) {
101
+ // Only option tables merge. A limit or hook would silently vanish from
102
+ // every merged subcommand, so a spec carrying any non-mergeable field is
103
+ // refused — by key, so a field added to OptionSpec later cannot slip past.
104
+ const extra = Object.keys(spec).find((key) => !MERGEABLE_FIELDS.has(key));
105
+ if (extra) throw new Error(`merge() only merges option tables; set '${extra}' on the merged spec itself`);
106
+ Object.assign(merged.options, spec.options);
107
+ if (spec.numeric) merged.numeric = true;
108
+ if (spec.fileValues) merged.fileValues = [...(merged.fileValues ?? []), ...spec.fileValues];
109
+ }
110
+ return merged;
111
+ }
112
+
113
+ const LENIENT: OptionSpec = { options: {}, lenient: true };
114
+
115
+ /**
116
+ * Parse `args` against `spec`. Refuses an option the table does not name, a
117
+ * value attached to an option that takes none, and more operands than
118
+ * `maxPositionals`. `--` ends the options; `-` alone is an operand (stdin).
119
+ */
120
+ export function checkOptions<W extends Word>(spec: OptionSpec, args: readonly W[]): OptionCheck<W> {
121
+ const parsed: ParsedOptions<W> = { positionals: [], fileValues: [], seen: new Set() };
122
+ const fileValueOptions = new Set(spec.fileValues ?? []);
123
+ let endOfOptions = false;
124
+
125
+ /** Record an option's value when it names a file; a separate word keeps its glob flag. */
126
+ const takeValue = (option: string, value: string | W | undefined) => {
127
+ if (value === undefined || !fileValueOptions.has(option)) return;
128
+ parsed.fileValues.push(typeof value === "string" ? { value } : value);
129
+ };
130
+
131
+ /**
132
+ * Consume the words a value-taking option at `i` takes when its value is
133
+ * not attached — the one place long options and short clusters agree on
134
+ * this — and return how many words were taken.
135
+ */
136
+ const takeSeparateValue = (option: string, arity: Arity, i: number): number => {
137
+ if (arity === "required") {
138
+ takeValue(option, args[i + 1]);
139
+ return 1;
140
+ }
141
+ if (arity === "lastarg") {
142
+ const next = args[i + 1];
143
+ if (next === undefined || next.value.startsWith("-")) return 0;
144
+ takeValue(option, next);
145
+ return 1;
146
+ }
147
+ if (arity === "two") {
148
+ takeValue(option, args[i + 2]);
149
+ return 2;
150
+ }
151
+ return 0;
152
+ };
153
+
154
+ for (let i = 0; i < args.length; i++) {
155
+ const word = args[i].value;
156
+ if (endOfOptions || word === "-" || !word.startsWith("-")) {
157
+ parsed.positionals.push(args[i]);
158
+ continue;
159
+ }
160
+ if (word === "--") {
161
+ endOfOptions = true;
162
+ continue;
163
+ }
164
+ if (spec.numeric && /^-[0-9]+$/.test(word)) continue;
165
+ if (spec.lenient) {
166
+ // Every option takes no value, so a cluster is all options.
167
+ if (word.startsWith("--")) parsed.seen.add(word.split("=", 1)[0]);
168
+ else for (const letter of word.slice(1)) parsed.seen.add(`-${letter}`);
169
+ continue;
170
+ }
171
+
172
+ if (word.startsWith("--")) {
173
+ const eq = word.indexOf("=");
174
+ const name = eq > 0 ? word.slice(0, eq) : word;
175
+ const arity = spec.options[name];
176
+ if (arity === undefined) return { ok: false, reason: `passes ${name}, an option not known to be read-only` };
177
+ parsed.seen.add(name);
178
+ if (eq > 0) {
179
+ if (arity === "none") return { ok: false, reason: `passes a value to ${name}, which takes none` };
180
+ takeValue(name, word.slice(eq + 1));
181
+ continue;
182
+ }
183
+ // `optional` takes its value only attached (`--abbrev=7`).
184
+ if (arity !== "optional") i += takeSeparateValue(name, arity, i);
185
+ continue;
186
+ }
187
+
188
+ // A short cluster: `-la` is `-l -a`; a value-taking letter ends it.
189
+ for (let j = 1; j < word.length; j++) {
190
+ const name = `-${word[j]}`;
191
+ const arity = spec.options[name];
192
+ if (arity === undefined) return { ok: false, reason: `passes ${name}, an option not known to be read-only` };
193
+ parsed.seen.add(name);
194
+ if (arity === "none") continue;
195
+ // The rest of the cluster is the value when there is one; otherwise a
196
+ // value-taking letter (not `optional`) takes the next word(s).
197
+ const rest = word.slice(j + 1);
198
+ if (rest) takeValue(name, rest);
199
+ else if (arity !== "optional") i += takeSeparateValue(name, arity, i);
200
+ break;
201
+ }
202
+ }
203
+
204
+ const optionsReason = spec.optionsReason?.(parsed.seen);
205
+ if (optionsReason) return { ok: false, reason: optionsReason };
206
+ if (spec.maxPositionals !== undefined && parsed.positionals.length > spec.maxPositionals) {
207
+ return { ok: false, reason: `takes ${parsed.positionals.length} operands; past ${spec.maxPositionals} the command writes` };
208
+ }
209
+ // The count above is of words as written; bash expands one glob into as
210
+ // many operands as it matches, so `uniq sample-*` reached uniq's output
211
+ // operand (AUTO-MODE-SECURITY-REVIEW-2026-09-24 H4).
212
+ const glob = spec.maxPositionals !== undefined ? parsed.positionals.find((word) => word.glob) : undefined;
213
+ if (glob) return { ok: false, reason: `passes the glob ${glob.value}, which can expand past the ${spec.maxPositionals} operand(s) it may take and still only read` };
214
+ return { ok: true, parsed };
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Plain commands
219
+ // ---------------------------------------------------------------------------
220
+
221
+ /*
222
+ * Options that follow symbolic links found while recursing are left out of
223
+ * every table (`grep -R`, `rg -L`, `du -L`, `tree -l`, find's `-L` and
224
+ * `-follow`), and `ls -LR` and `diff -r` are refused below. Operands are
225
+ * judged where they resolve, but a symlink deep inside an operand directory
226
+ * is never seen, so following it reads outside the working directory. Claude
227
+ * Code accepts these options; the forms that follow only operands (`-H`) stay.
228
+ */
229
+ const FOLLOWS_WHILE_RECURSING = "follows symbolic links while it recurses, so it can read outside the working directory";
230
+
231
+ /** The checksum tools without `-c`/`--check`, which reads every file its list names. */
232
+ const CHECKSUM = table({
233
+ none: "-b -t -s -w -z -0 -U --status --warn --strict --quiet --tag --binary --text --zero --ignore-missing --untagged --base64 --raw",
234
+ required: "-a -o -l --algorithm --length",
235
+ });
236
+
237
+ const GREP = table(
238
+ {
239
+ none:
240
+ "-a -b -c -E -F -G -H -h -i -I -l -L -n -o -q -r -s -U -v -w -x -y -z -Z -P -T --count --extended-regexp --fixed-strings --basic-regexp --perl-regexp --with-filename --no-filename --ignore-case --no-ignore-case --files-with-matches --files-without-match --line-number --only-matching --quiet --silent --recursive --no-messages --binary --invert-match --word-regexp --line-regexp --null-data --null --byte-offset --line-buffered --initial-tab",
241
+ optional: "--color --colour",
242
+ required:
243
+ "-A -B -C -m -e -f -d -D --after-context --before-context --context --max-count --regexp --file --include --exclude --exclude-dir --binary-files --directories --devices --label",
244
+ },
245
+ { numeric: true, fileValues: ["-f", "--file"] },
246
+ );
247
+
248
+ /**
249
+ * ripgrep without `--pre`/`--pre-glob` (run a program per file),
250
+ * `--hostname-bin` (runs a program) and `--ignore-file` (reads a named file).
251
+ */
252
+ const RG = table(
253
+ {
254
+ none:
255
+ "-i -s -S -w -x -v -l -c -n -N -H -I -o -p -q -u -U -F -P -a -z -0 -b -h -V --no-heading --heading --hidden --no-hidden --no-ignore --no-ignore-vcs --no-ignore-parent --no-ignore-dot --no-ignore-global --no-ignore-exclude --no-ignore-files --ignore --ignore-case --case-sensitive --smart-case --word-regexp --line-regexp --invert-match --files-with-matches --files-without-match --count --count-matches --line-number --no-line-number --with-filename --no-filename --only-matching --pretty --quiet --json --no-json --files --fixed-strings --pcre2 --no-pcre2 --multiline --no-multiline --multiline-dotall --text --null --null-data --trim --vimgrep --column --no-column --no-messages --stats --type-list --unrestricted --no-config --debug --trace --one-file-system --binary --no-require-git --crlf --passthru --include-zero --byte-offset --line-buffered --block-buffered --max-columns-preview --glob-case-insensitive --search-zip --no-search-zip --sort-files --help --version",
256
+ required:
257
+ "-e -f -g -t -T -A -B -C -m -d -j -M -r -E --regexp --file --glob --iglob --type --type-not --type-add --type-clear --after-context --before-context --context --max-count --max-depth --maxdepth --max-filesize --threads --max-columns --replace --context-separator --field-match-separator --field-context-separator --engine --encoding --path-separator --sort --sortr --color --colors --dfa-size-limit --regex-size-limit --hyperlink-format",
258
+ },
259
+ { numeric: true, fileValues: ["-f", "--file"] },
260
+ );
261
+
262
+ /**
263
+ * jq without `-f`, `--rawfile`, `--slurpfile` (read named files) and `-L`
264
+ * (module search path). A program naming `env` or `ENV` is jq's printenv: jq
265
+ * evaluates `$ENV` itself, so single quotes that keep bash from expanding it
266
+ * do not make it inert, and jq also accepts `$ ENV` with a space
267
+ * (AUTO-MODE-SECURITY-REVIEW-2026-09-24 H5, findings §32). `import` and
268
+ * `include` load modules from jq's search path.
269
+ */
270
+ const JQ = table(
271
+ {
272
+ none:
273
+ "-r -j -a -c -n -e -s -S -C -M -R -h --tab --raw-output --raw-output0 --join-output --ascii-output --compact-output --null-input --exit-status --slurp --sort-keys --color-output --monochrome-output --raw-input --seq --stream --stream-errors --unbuffered --args --jsonargs --help --version",
274
+ required: "--indent",
275
+ two: "--arg --argjson",
276
+ },
277
+ {
278
+ programFirst: true,
279
+ operandReason: ([program]) => {
280
+ if (!program) return undefined;
281
+ if (/\b(?:env|ENV)\b/.test(program.value)) return "runs a jq program that reads the process environment";
282
+ if (/\b(?:import|include)\b/.test(program.value)) return "runs a jq program that loads modules from jq's search path";
283
+ return undefined;
284
+ },
285
+ },
286
+ );
287
+
288
+ /** sort without `-o`/`--output` (writes), `-T` (writes temp files), `--compress-program` (runs a program), `--files0-from`. */
289
+ const SORT = table(
290
+ {
291
+ none:
292
+ "-b -d -f -g -i -M -h -n -R -r -V -c -C -m -s -u -z --ignore-leading-blanks --dictionary-order --ignore-case --general-numeric-sort --ignore-nonprinting --month-sort --human-numeric-sort --numeric-sort --random-sort --reverse --version-sort --merge --stable --unique --zero-terminated --debug",
293
+ optional: "--check",
294
+ required: "-k -t -S --key --field-separator --buffer-size --parallel --sort --random-source",
295
+ },
296
+ { fileValues: ["--random-source"] },
297
+ );
298
+
299
+ /** uniq's second operand is its output file. */
300
+ const UNIQ = table(
301
+ {
302
+ none: "-c -d -D -u -i -z --count --repeated --unique --ignore-case --zero-terminated",
303
+ optional: "--all-repeated --group",
304
+ required: "-f -s -w --skip-fields --skip-chars --check-chars",
305
+ },
306
+ { numeric: true, maxPositionals: 1 },
307
+ );
308
+
309
+ /** wc without `--files0-from` (a list of files to open). */
310
+ const WC = table({
311
+ none: "-c -l -m -w -L --bytes --chars --lines --words --max-line-length --help --version",
312
+ optional: "--total",
313
+ });
314
+
315
+ /** du without `-X`/`--exclude-from` and `--files0-from`. */
316
+ const DU = table({
317
+ none:
318
+ "-a -A -c -h -H -k -l -m -g -P -s -x -0 -b -D --si --apparent-size --all --bytes --total --summarize --dereference-args --no-dereference --one-file-system --human-readable --null --inodes --count-links --separate-dirs -S",
319
+ optional: "--time",
320
+ required: "-d -B -t -I --max-depth --block-size --threshold --exclude --time-style",
321
+ });
322
+
323
+ /** file without `-C` (compiles a magic file, writing it), `-m` and `-f` (read named files). */
324
+ const FILE = table({
325
+ none:
326
+ "-b -c -h -L -i -I -k -l -N -n -r -s -z -Z -0 -d -E -S --brief --dereference --no-dereference --keep-going --no-pad --raw --special-files --uncompress --uncompress-noreport --mime --mime-type --mime-encoding --extension --apple --no-sandbox --print0 --help --version",
327
+ required: "-e -P -F --exclude --exclude-quiet --parameter --separator",
328
+ });
329
+
330
+ /** tree without `-o` (writes), `-R`/`-H` (together write HTML files), `--fromfile`. */
331
+ const TREE = table({
332
+ none:
333
+ "-a -d -f -x -i -q -N -Q -p -u -g -s -h -D -F -v -t -c -U -r -n -C -A -S -J -X -1 --prune --noreport --si --du --dirsfirst --filesfirst --gitignore --matchdirs --ignore-case --info --inodes --device --help --version",
334
+ required: "-L -P -I --charset --filelimit --timefmt --sort",
335
+ });
336
+
337
+ /** diff without `--from-file`/`--to-file`/`-X` (their values are checked as reads) and `--paginate` (runs pr). */
338
+ const DIFF = table(
339
+ {
340
+ none:
341
+ "-a -b -B -c -d -e -E -f -H -i -n -N -p -P -q -r -s -t -T -u -w -y -Z --brief --recursive --ignore-case --ignore-all-space --ignore-space-change --ignore-blank-lines --ignore-tab-expansion --ignore-trailing-space --text --new-file --unidirectional-new-file --report-identical-files --side-by-side --suppress-common-lines --left-column --strip-trailing-cr --minimal --speed-large-files --no-dereference --expand-tabs --initial-tab --suppress-blank-empty --normal --show-c-function",
342
+ optional: "--unified --context --color",
343
+ required:
344
+ "-U -C -W -x -X -I -F -L -D -S --label --exclude --exclude-from --ignore-matching-lines --show-function-line --width --tabsize --ifdef --starting-file --from-file --to-file --palette --horizon-lines --line-format --old-line-format --new-line-format --unchanged-line-format --old-group-format --new-group-format --changed-group-format --unchanged-group-format",
345
+ },
346
+ {
347
+ fileValues: ["-X", "--exclude-from", "--from-file", "--to-file"],
348
+ // diff dereferences the entries of a directory operand (`-r` recurses,
349
+ // but even one level follows a symlink) unless told not to.
350
+ dereferencesDirEntries: ["--no-dereference"],
351
+ },
352
+ );
353
+
354
+ /** date without `-s`/`--set` (sets the clock) and `-f` (GNU: reads a file of dates); an operand other than `+FORMAT` sets the clock. */
355
+ const DATE = table(
356
+ {
357
+ none: "-u -R -j -n --utc --universal --rfc-email --debug --help --version",
358
+ optional: "-I --iso-8601 --rfc-3339",
359
+ required: "-d -r -v --date --reference",
360
+ },
361
+ {
362
+ // `-r FILE` / `--reference=FILE` read FILE's timestamp (BSD `-r` also takes seconds).
363
+ fileValues: ["-r", "--reference"],
364
+ operandReason: (operands) => (operands.some((word) => !word.value.startsWith("+")) ? "runs date with an operand, which sets the clock" : undefined),
365
+ },
366
+ );
367
+
368
+ /** hostname with no operand (an operand sets the hostname). */
369
+ const HOSTNAME = table({ none: "-s -f -d -i -I -A --short --fqdn --long --domain --ip-address --all-ip-addresses --all-fqdns" }, { maxPositionals: 0 });
370
+
371
+ /**
372
+ * The option table of every command the pre-gate may fast-path as a read.
373
+ * A command absent from this map is not read-only.
374
+ */
375
+ export const READ_ONLY_SPECS: Record<string, OptionSpec> = {
376
+ basename: LENIENT,
377
+ cat: LENIENT,
378
+ cksum: CHECKSUM,
379
+ column: LENIENT,
380
+ comm: LENIENT,
381
+ cut: LENIENT,
382
+ date: DATE,
383
+ diff: DIFF,
384
+ dirname: LENIENT,
385
+ du: DU,
386
+ echo: LENIENT,
387
+ false: LENIENT,
388
+ file: FILE,
389
+ fold: LENIENT,
390
+ head: LENIENT,
391
+ hostname: HOSTNAME,
392
+ id: LENIENT,
393
+ join: LENIENT,
394
+ jq: JQ,
395
+ ls: {
396
+ ...LENIENT,
397
+ optionsReason: (seen) =>
398
+ (seen.has("-L") || seen.has("--dereference")) && (seen.has("-R") || seen.has("--recursive")) ? `passes -L with -R, so it ${FOLLOWS_WHILE_RECURSING}` : undefined,
399
+ },
400
+ md5sum: CHECKSUM,
401
+ nl: LENIENT,
402
+ od: LENIENT,
403
+ paste: LENIENT,
404
+ // No options: bash's `printf -v NAME` assigns a shell variable, and
405
+ // `printf -v PATH ./bin; ls` would run ./bin/ls. A `%n` conversion assigns
406
+ // too, to the argument it consumes (`printf '%n' PATH` sets PATH to 0), with
407
+ // flags and length modifiers (`%.0n`, `%ln`) and on format reuse
408
+ // (AUTO-MODE-SECURITY-REVIEW-2026-09-24 H3, findings §32).
409
+ printf: {
410
+ options: {},
411
+ operandReason: ([format]) =>
412
+ format && /%[^%a-zA-Z]*[hlLqjzZt]*n/.test(format.value.replace(/%%/g, "")) ? "runs printf with a %n conversion, which assigns a shell variable" : undefined,
413
+ },
414
+ pwd: LENIENT,
415
+ readlink: LENIENT,
416
+ realpath: LENIENT,
417
+ rev: LENIENT,
418
+ sha1sum: CHECKSUM,
419
+ sha256sum: CHECKSUM,
420
+ shasum: CHECKSUM,
421
+ sort: SORT,
422
+ stat: LENIENT,
423
+ tail: LENIENT,
424
+ tr: LENIENT,
425
+ tree: TREE,
426
+ true: LENIENT,
427
+ uname: LENIENT,
428
+ uniq: UNIQ,
429
+ wc: WC,
430
+ which: LENIENT,
431
+ whoami: LENIENT,
432
+ yes: LENIENT,
433
+ // Pattern-first: the first operand is the pattern unless -e/-f supplies it.
434
+ grep: GREP,
435
+ egrep: GREP,
436
+ fgrep: GREP,
437
+ rg: RG,
438
+ };
439
+
440
+ /** Options through which a pattern-first command gets its pattern, making every operand a path. */
441
+ export const PATTERN_OPTIONS = new Set(["-e", "-f", "--regexp", "--file"]);
442
+
443
+ // ---------------------------------------------------------------------------
444
+ // find
445
+ // ---------------------------------------------------------------------------
446
+
447
+ /**
448
+ * find's expression primaries and operators, by how many words each consumes.
449
+ * The actions that execute or write (`-exec`, `-execdir`, `-ok`, `-okdir`,
450
+ * `-delete`, `-fprint*`, `-fls`) are deliberately absent.
451
+ */
452
+ const FIND_PRIMARIES: Record<string, 0 | 1> = Object.fromEntries([
453
+ ...words(
454
+ "-print -print0 -ls -prune -quit -true -false -empty -nouser -nogroup -readable -writable -executable -depth -xdev -mount -noleaf -daystart -ignore_readdir_race -noignore_readdir_race -not -a -and -o -or ! ( ) , -nowarn -warn",
455
+ ).map((p) => [p, 0 as const]),
456
+ ...words(
457
+ "-name -iname -path -ipath -wholename -iwholename -regex -iregex -type -xtype -size -newer -anewer -cnewer -mtime -mmin -atime -amin -ctime -cmin -Btime -Bmin -Bnewer -user -group -uid -gid -perm -maxdepth -mindepth -links -inum -samefile -lname -ilname -fstype -flags -regextype -printf -used -context",
458
+ ).map((p) => [p, 1 as const]),
459
+ ]);
460
+
461
+ /** Primaries whose value names a file find stats (read-checked like a starting point). */
462
+ const FIND_FILE_PRIMARIES = new Set(["-newer", "-anewer", "-cnewer", "-Bnewer", "-samefile"]);
463
+
464
+ /**
465
+ * Parse a find command line. Returns its starting points, the file values of
466
+ * `-newer`-style primaries and the indexes of the expression's primaries and
467
+ * operators (not their values), or the reason it is not read-only.
468
+ */
469
+ export function checkFind<W extends Word>(args: readonly W[]): { ok: true; paths: W[]; expression: number[] } | { ok: false; reason: string } {
470
+ let i = 0;
471
+ const paths: W[] = [];
472
+ const expression: number[] = [];
473
+ // Leading options: clusters of the no-value letters, `-O<level>`, and
474
+ // `-D <debug>` / BSD's `-f <path>` as separate words. Anything else that
475
+ // starts with `-` is the expression (`-fprint` is not `-f print`).
476
+ while (i < args.length) {
477
+ const word = args[i].value;
478
+ // No `-L` (follow every symlink): see FOLLOWS_WHILE_RECURSING.
479
+ if (/^-[HPEXdsx]+$/.test(word) || /^-O[0-9]$/.test(word)) {
480
+ i++;
481
+ } else if (word === "-D") {
482
+ i += 2;
483
+ } else if (word === "-f" && i + 1 < args.length) {
484
+ paths.push(args[i + 1]);
485
+ i += 2;
486
+ } else break;
487
+ }
488
+ // Starting points: everything up to the first expression word.
489
+ while (i < args.length) {
490
+ const word = args[i].value;
491
+ if (word.startsWith("-") || word === "!" || word === "(" || word === ")") break;
492
+ paths.push(args[i]);
493
+ i++;
494
+ }
495
+ // The expression.
496
+ for (; i < args.length; i++) {
497
+ const word = args[i].value;
498
+ const newer = /^-newer[aBcmt][aBcmt]$/.test(word);
499
+ const consumes = newer ? 1 : FIND_PRIMARIES[word];
500
+ if (consumes === undefined) return { ok: false, reason: `uses find ${word}, which is not a known read-only primary` };
501
+ expression.push(i);
502
+ if (consumes === 1) {
503
+ const value = args[i + 1];
504
+ if (value !== undefined && (FIND_FILE_PRIMARIES.has(word) || newer)) paths.push(value);
505
+ i++;
506
+ }
507
+ }
508
+ return { ok: true, paths, expression };
509
+ }
510
+
511
+ /** find's tests whose operand is a name or path pattern. */
512
+ export const FIND_PATTERN_TESTS: ReadonlySet<string> = new Set(["-name", "-iname", "-path", "-ipath", "-wholename", "-iwholename", "-regex", "-iregex", "-lname", "-ilname"]);
513
+
514
+ /**
515
+ * A read-only find's negated pattern operands, by index into `args`, or
516
+ * undefined when the find is not read-only (an action, an unknown primary)
517
+ * and this does not read its expression. `excluding` holds those that can
518
+ * only exclude files: under exactly one `!`/`-not` at the top level of an
519
+ * expression with no top-level `-o`/`-or` and no `,`, alone
520
+ * (`-not -path './.git/*'`) or in a negated group of pattern tests joined by
521
+ * `-o` (`! ( -path a -o -path b )`). Every file the find selects fails such a
522
+ * pattern. `selecting` holds the rest, which can select the files they name
523
+ * (`-not -name x -o -print`, `! ! -name x`, `-not ( -name x -empty )`).
524
+ */
525
+ export function findNegatedPatterns<W extends Word>(args: readonly W[]): { excluding: Set<number>; selecting: Set<number> } | undefined {
526
+ const find = checkFind(args);
527
+ if (!find.ok) return undefined;
528
+ const ops = find.expression;
529
+ const at = (k: number) => args[ops[k]]?.value;
530
+ const negation = (k: number) => at(k) === "!" || at(k) === "-not";
531
+ // Every pattern under any negation: those directly before it, and before each group around it.
532
+ const negated = new Set<number>();
533
+ const groupNegations: number[] = [];
534
+ let pending = 0;
535
+ for (let k = 0; k < ops.length; k++) {
536
+ const word = at(k);
537
+ if (negation(k)) {
538
+ pending++;
539
+ continue;
540
+ }
541
+ if (word === "(") groupNegations.push(pending);
542
+ else if (word === ")") groupNegations.pop();
543
+ else if (FIND_PATTERN_TESTS.has(word ?? "") && pending + groupNegations.reduce((sum, count) => sum + count, 0) > 0) negated.add(ops[k] + 1);
544
+ pending = 0;
545
+ }
546
+ const excluding = new Set<number>();
547
+ const result = () => ({ excluding, selecting: new Set([...negated].filter((index) => !excluding.has(index))) });
548
+ let depth = 0;
549
+ for (let k = 0; k < ops.length; k++) {
550
+ const word = at(k);
551
+ if (word === "(") depth++;
552
+ else if (word === ")" && --depth < 0) return result();
553
+ if (word === "," || (depth === 0 && (word === "-o" || word === "-or"))) return result();
554
+ }
555
+ if (depth !== 0) return result();
556
+ for (let k = 0; k < ops.length; k++) {
557
+ const word = at(k);
558
+ if (word === "(") depth++;
559
+ else if (word === ")") depth--;
560
+ if (depth !== 0 || !negation(k) || negation(k - 1) || negation(k + 1)) continue;
561
+ if (FIND_PATTERN_TESTS.has(at(k + 1) ?? "")) {
562
+ excluding.add(ops[k + 1] + 1);
563
+ } else if (at(k + 1) === "(") {
564
+ // The group's members, up to its `)`: pattern tests alternating with `-o`.
565
+ const members: number[] = [];
566
+ for (let m = k + 2; m < ops.length && at(m) !== ")"; m++) members.push(m);
567
+ const disjunction = members.length % 2 === 1 &&
568
+ members.every((member, n) => (n % 2 === 0 ? FIND_PATTERN_TESTS.has(at(member) ?? "") : at(member) === "-o" || at(member) === "-or"));
569
+ if (disjunction) for (let n = 0; n < members.length; n += 2) excluding.add(ops[members[n]] + 1);
570
+ }
571
+ }
572
+ return result();
573
+ }
574
+
575
+ // ---------------------------------------------------------------------------
576
+ // git
577
+ // ---------------------------------------------------------------------------
578
+
579
+ /**
580
+ * git's global options that may precede a read-only subcommand. `-C` is
581
+ * validated by the caller (it must stay inside the working directory);
582
+ * `-c`, `--config-env`, `--git-dir`, `--work-tree`, `--exec-path`,
583
+ * `--namespace` and everything else escalate: each can point git at a
584
+ * configuration or repository the check has not seen.
585
+ */
586
+ export const GIT_GLOBAL_SAFE = new Set([
587
+ "--no-pager",
588
+ "-P",
589
+ "--no-optional-locks",
590
+ "--literal-pathspecs",
591
+ "--glob-pathspecs",
592
+ "--noglob-pathspecs",
593
+ "--icase-pathspecs",
594
+ "--no-replace-objects",
595
+ "--version",
596
+ ]);
597
+
598
+ const GIT_COLOR = table({ none: "--no-color", optional: "--color" });
599
+
600
+ /**
601
+ * Diff output options shared by diff, log and show, without `--output`
602
+ * (writes), `--ext-diff`/`--textconv` (run configured programs) and `-O`
603
+ * (reads an order file).
604
+ */
605
+ const GIT_DIFF = merge(
606
+ GIT_COLOR,
607
+ table({
608
+ none:
609
+ "-p -u -s -D -R -a -b -w -W -z -t -m -c --patch --no-patch --raw --patch-with-raw --patch-with-stat --numstat --shortstat --cumulative --summary --name-only --name-status --no-color-moved --no-renames --rename-empty --no-rename-empty --check --full-index --binary --find-copies-harder --irreversible-delete --pickaxe-all --pickaxe-regex --no-relative --text --ignore-cr-at-eol --ignore-space-at-eol --ignore-space-change --ignore-all-space --ignore-blank-lines --function-context --exit-code --quiet --no-ext-diff --no-textconv --no-prefix --default-prefix --ita-invisible-in-index --ita-visible-in-index --minimal --patience --histogram --compact-summary --indent-heuristic --no-indent-heuristic --cc --dd --no-diff-merges --combined-all-paths --remerge-diff --no-abbrev",
610
+ optional:
611
+ "--stat --dirstat --submodule --color-moved --word-diff --color-words --abbrev -B --break-rewrites -M --find-renames -C --find-copies --relative --ignore-submodules",
612
+ required:
613
+ "-U -l -S -G -I --unified --color-moved-ws --word-diff-regex --ws-error-highlight --diff-filter --find-object --ignore-matching-lines --inter-hunk-context --src-prefix --dst-prefix --line-prefix --anchored --diff-algorithm --stat-width --stat-name-width --stat-count --stat-graph-width --output-indicator-new --output-indicator-old --output-indicator-context --diff-merges",
614
+ }),
615
+ );
616
+
617
+ /** Commit selection shared by log and shortlog. */
618
+ const GIT_REVS = table(
619
+ {
620
+ none:
621
+ "-i -E -F -P -g --all --not --all-match --invert-grep --regexp-ignore-case --extended-regexp --fixed-strings --perl-regexp --basic-regexp --merges --no-merges --no-min-parents --no-max-parents --first-parent --reverse --topo-order --date-order --author-date-order --follow --full-history --simplify-merges --simplify-by-decoration --dense --sparse --left-right --left-only --right-only --cherry-pick --cherry-mark --cherry --boundary --do-walk --walk-reflogs --reflog --bisect --stdin --ignore-missing",
622
+ optional: "--branches --tags --remotes --ancestry-path --no-walk",
623
+ required:
624
+ "-n --max-count --skip --since --after --until --before --since-as-filter --author --committer --grep --grep-reflog --min-parents --max-parents --glob --exclude",
625
+ },
626
+ { numeric: true },
627
+ );
628
+
629
+ /** Commit formatting shared by log and show, without `--show-signature` (runs the configured gpg program). */
630
+ const GIT_LOG_FORMAT = table({
631
+ none:
632
+ "--oneline --abbrev-commit --no-abbrev-commit --no-decorate --source --graph --relative-date --parents --children --no-notes --no-expand-tabs --log-size --mailmap --no-mailmap --use-mailmap --no-use-mailmap --clear-decorations --no-standard-notes --standard-notes --show-pulls",
633
+ optional: "--pretty --decorate --notes --expand-tabs --show-linear-break",
634
+ required: "--format --date --encoding --decorate-refs --decorate-refs-exclude",
635
+ });
636
+
637
+ /** List options of `git branch`; operands are allowed only with `-l`/`--list`, since a bare operand creates a branch. */
638
+ const GIT_BRANCH = table({
639
+ none: "-l -a -r -v -q -i --list --all --remotes --verbose --quiet --no-color --no-column --no-abbrev --ignore-case --omit-empty --show-current",
640
+ optional: "--color --column --abbrev",
641
+ required: "--points-at --sort --format",
642
+ lastarg: "--contains --no-contains --merged --no-merged",
643
+ });
644
+
645
+ /** List options of `git tag`, without `-v`/`--verify` (runs gpg); operands only with `-l`/`--list`. */
646
+ const GIT_TAG = table({
647
+ none: "-l -i --list --no-column --ignore-case --omit-empty --no-color",
648
+ optional: "-n --color --column",
649
+ required: "--points-at --sort --format",
650
+ lastarg: "--contains --no-contains --merged --no-merged",
651
+ });
652
+
653
+ export const GIT_SUBCOMMAND_SPECS: Record<string, OptionSpec> = {
654
+ blame: merge(
655
+ GIT_COLOR,
656
+ table({
657
+ none: "-b -f -n -p -l -t -s -e -w -c --root --show-stats --score-debug --show-name --show-number --porcelain --line-porcelain --incremental --show-email --reverse --first-parent --progress --no-progress --minimal --color-lines --color-by-age",
658
+ optional: "-M -C --abbrev",
659
+ required: "-L --date --encoding --ignore-rev",
660
+ }),
661
+ ),
662
+ branch: GIT_BRANCH,
663
+ "cat-file": table({
664
+ none: "-t -s -e -p -Z --batch-all-objects --buffer --unordered --follow-symlinks --allow-unknown-type --mailmap --no-mailmap --use-mailmap --no-use-mailmap",
665
+ optional: "--batch --batch-check --batch-command",
666
+ }),
667
+ describe: table({
668
+ none: "--all --tags --contains --exact-match --debug --long --always --first-parent",
669
+ optional: "--dirty --broken --abbrev",
670
+ required: "--candidates --match --exclude",
671
+ }),
672
+ diff: merge(GIT_DIFF, table({ none: "--cached --staged --merge-base -1 -2 -3 --base --ours --theirs" })),
673
+ grep: merge(
674
+ GIT_COLOR,
675
+ table(
676
+ {
677
+ none:
678
+ "-a -i -I -r -w -v -h -H -E -G -P -F -n -l -L -z -o -c -p -W -q --text --no-textconv --ignore-case --no-recursive --recursive --word-regexp --invert-match --full-name --extended-regexp --basic-regexp --perl-regexp --fixed-strings --line-number --column --files-with-matches --name-only --files-without-match --null --only-matching --count --break --heading --show-function --function-context --and --or --not --all-match --quiet --cached --untracked --no-exclude-standard --exclude-standard --recurse-submodules",
679
+ required: "-A -B -C -m -e --max-depth --after-context --before-context --context --max-count --threads",
680
+ },
681
+ { numeric: true },
682
+ ),
683
+ ),
684
+ log: merge(GIT_REVS, GIT_LOG_FORMAT, GIT_DIFF, table({ required: "-L" })),
685
+ "ls-files": table({
686
+ none: "-c -d -m -o -i -s -u -k -z -t -v -f --cached --deleted --modified --others --ignored --stage --unmerged --killed --directory --no-empty-directory --eol --deduplicate --full-name --recurse-submodules --debug --sparse --error-unmatch --exclude-standard --resolve-undo",
687
+ optional: "--abbrev",
688
+ required: "-x --exclude --exclude-per-directory --with-tree --format",
689
+ }),
690
+ "ls-tree": table({
691
+ none: "-d -r -t -l -z --long --name-only --name-status --object-only --full-name --full-tree",
692
+ optional: "--abbrev",
693
+ required: "--format",
694
+ }),
695
+ "rev-parse": table({
696
+ none:
697
+ "-q --verify --quiet --sq --not --symbolic --symbolic-full-name --all --show-toplevel --show-superproject-working-tree --show-prefix --show-cdup --git-dir --git-common-dir --absolute-git-dir --is-inside-git-dir --is-inside-work-tree --is-bare-repository --is-shallow-repository --shared-index-path --show-ref-format --local-env-vars --revs-only --no-revs --flags --no-flags --end-of-options",
698
+ optional: "--abbrev-ref --short --branches --tags --remotes --show-object-format",
699
+ required: "--glob --exclude --disambiguate --git-path --path-format --since --after --until --before --default --prefix",
700
+ }),
701
+ shortlog: merge(
702
+ GIT_REVS,
703
+ table({ none: "-n -s -e -c --numbered --summary --email --committer", optional: "--format -w", required: "--group" }),
704
+ ),
705
+ show: merge(GIT_LOG_FORMAT, GIT_DIFF, table({ none: "--first-parent" })),
706
+ "show-ref": table({
707
+ none: "-d -s -q --head --heads --branches --tags --dereference --verify --quiet --exists",
708
+ optional: "--hash --abbrev --exclude-existing",
709
+ }),
710
+ status: table({
711
+ none: "-s -b -v -z --short --branch --show-stash --long --verbose --no-column --ahead-behind --no-ahead-behind --renames --no-renames",
712
+ optional: "--porcelain -u --untracked-files --ignore-submodules --ignored --column --find-renames",
713
+ }),
714
+ tag: GIT_TAG,
715
+ };
716
+
717
+ /**
718
+ * Whether a git subcommand's operands are allowed: `branch` and `tag` create
719
+ * refs from a bare operand, so they only list when `-l`/`--list` is present.
720
+ */
721
+ export function gitOperandsAllowed(subcommand: string, parsed: ParsedOptions): boolean {
722
+ if (subcommand !== "branch" && subcommand !== "tag") return true;
723
+ return parsed.positionals.length === 0 || parsed.seen.has("-l") || parsed.seen.has("--list");
724
+ }