@toolu/opencode 6.8.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 (331) hide show
  1. package/generated/GENERATED-NOTES.md +16 -0
  2. package/generated/agents/toolu--architect.md +52 -0
  3. package/generated/agents/toolu--deep-explore.md +65 -0
  4. package/generated/agents/toolu--implementer.md +50 -0
  5. package/generated/agents/toolu--quick-task.md +54 -0
  6. package/generated/agents/toolu--research-agent.md +101 -0
  7. package/generated/commands/toolu--commit.md +8 -0
  8. package/generated/commands/toolu--review-and-commit.md +8 -0
  9. package/generated/opencode.toolu.json +116 -0
  10. package/generated/skills/toolu--brainstorm/SKILL.md +57 -0
  11. package/generated/skills/toolu--brainstorm/references/design-questions.md +36 -0
  12. package/generated/skills/toolu--commit--skill/SKILL.md +11 -0
  13. package/generated/skills/toolu--debug/SKILL.md +54 -0
  14. package/generated/skills/toolu--deep-research/SKILL.md +51 -0
  15. package/generated/skills/toolu--execution/SKILL.md +77 -0
  16. package/generated/skills/toolu--orchestrator/SKILL.md +145 -0
  17. package/generated/skills/toolu--orchestrator/references/delegation-cost.md +104 -0
  18. package/generated/skills/toolu--orchestrator/references/model-routing.md +112 -0
  19. package/generated/skills/toolu--plan/SKILL.md +56 -0
  20. package/generated/skills/toolu--plan/references/ledger.md +18 -0
  21. package/generated/skills/toolu--plan-review/SKILL.md +48 -0
  22. package/generated/skills/toolu--review-and-commit--skill/SKILL.md +10 -0
  23. package/generated/skills/toolu--setup/SKILL.md +24 -0
  24. package/generated/skills/toolu--setup/scripts/setup.sh +206 -0
  25. package/generated/skills/toolu--spec/SKILL.md +67 -0
  26. package/generated/skills/toolu--spec-review/SKILL.md +50 -0
  27. package/generated/skills/toolu--test/SKILL.md +49 -0
  28. package/package.json +51 -0
  29. package/plugins/agent-browser/.claude-plugin/plugin.json +18 -0
  30. package/plugins/agent-browser/.codex-plugin/plugin.json +7 -0
  31. package/plugins/agent-browser/README.md +18 -0
  32. package/plugins/agent-browser/hooks/hooks.json +15 -0
  33. package/plugins/agent-browser/hooks/session-start.sh +42 -0
  34. package/plugins/agent-browser/skills/agent-browser/SKILL.md +77 -0
  35. package/plugins/agent-browser/skills/agent-browser/scripts/agent-browser.sh +80 -0
  36. package/plugins/ast-grep/.claude-plugin/plugin.json +18 -0
  37. package/plugins/ast-grep/.codex-plugin/plugin.json +7 -0
  38. package/plugins/ast-grep/README.md +21 -0
  39. package/plugins/ast-grep/hooks/hooks.json +15 -0
  40. package/plugins/ast-grep/hooks/post-tools.d/byte-savings.sh +67 -0
  41. package/plugins/ast-grep/hooks/pre-tools.d/search-nudge.sh +170 -0
  42. package/plugins/ast-grep/hooks/register.sh +68 -0
  43. package/plugins/ast-grep/scripts/byte-savings-report.sh +28 -0
  44. package/plugins/ast-grep/skills/ast-grep/SKILL.md +127 -0
  45. package/plugins/ast-grep/skills/ast-grep/references/ast-grep-advanced.md +44 -0
  46. package/plugins/ast-grep/skills/ast-grep/scripts/ast-grep.sh +103 -0
  47. package/plugins/context7/.claude-plugin/plugin.json +18 -0
  48. package/plugins/context7/.codex-plugin/plugin.json +7 -0
  49. package/plugins/context7/README.md +19 -0
  50. package/plugins/context7/hooks/hooks.json +15 -0
  51. package/plugins/context7/hooks/session-start.sh +42 -0
  52. package/plugins/context7/skills/context7/SKILL.md +64 -0
  53. package/plugins/context7/skills/context7/scripts/search.sh +151 -0
  54. package/plugins/exa-search/.claude-plugin/plugin.json +18 -0
  55. package/plugins/exa-search/.codex-plugin/plugin.json +7 -0
  56. package/plugins/exa-search/README.md +19 -0
  57. package/plugins/exa-search/hooks/hooks.json +15 -0
  58. package/plugins/exa-search/hooks/session-start.sh +34 -0
  59. package/plugins/exa-search/skills/exa-search/SKILL.md +91 -0
  60. package/plugins/exa-search/skills/exa-search/scripts/search.sh +221 -0
  61. package/plugins/jev/.claude-plugin/plugin.json +19 -0
  62. package/plugins/jev/.codex-plugin/plugin.json +7 -0
  63. package/plugins/jev/README.md +61 -0
  64. package/plugins/jev/hooks/hooks.json +25 -0
  65. package/plugins/jev/hooks/lib/common.sh +49 -0
  66. package/plugins/jev/hooks/session-start.sh +45 -0
  67. package/plugins/jev/hooks/user-prompt-submit.sh +41 -0
  68. package/plugins/jev/skills/jev/SKILL.md +81 -0
  69. package/plugins/jev/skills/jev/evals/README.md +98 -0
  70. package/plugins/jev/skills/jev/references/problem-solving.md +198 -0
  71. package/plugins/jev/skills/jev/scripts/jev.sh +405 -0
  72. package/plugins/jira/.claude-plugin/plugin.json +18 -0
  73. package/plugins/jira/.codex-plugin/plugin.json +7 -0
  74. package/plugins/jira/README.md +42 -0
  75. package/plugins/jira/hooks/hooks.json +15 -0
  76. package/plugins/jira/hooks/session-start.sh +34 -0
  77. package/plugins/jira/skills/jira/SKILL.md +123 -0
  78. package/plugins/jira/skills/jira/scripts/jira.sh +70 -0
  79. package/plugins/jira/skills/jira/scripts/lib/adf.sh +17 -0
  80. package/plugins/jira/skills/jira/scripts/lib/attachment.sh +94 -0
  81. package/plugins/jira/skills/jira/scripts/lib/board.sh +58 -0
  82. package/plugins/jira/skills/jira/scripts/lib/http.sh +184 -0
  83. package/plugins/jira/skills/jira/scripts/lib/issue.sh +159 -0
  84. package/plugins/jira/skills/jira/scripts/lib/paginate.sh +45 -0
  85. package/plugins/jira/skills/jira/scripts/lib/plan-parse.sh +70 -0
  86. package/plugins/jira/skills/jira/scripts/lib/plan-run.sh +115 -0
  87. package/plugins/jira/skills/jira/scripts/lib/plan-store.sh +168 -0
  88. package/plugins/jira/skills/jira/scripts/lib/plan.sh +108 -0
  89. package/plugins/jira/skills/jira/scripts/lib/project.sh +42 -0
  90. package/plugins/jira/skills/jira/scripts/lib/raw.sh +15 -0
  91. package/plugins/jira/skills/jira/scripts/lib/search.sh +41 -0
  92. package/plugins/jira/skills/jira/scripts/lib/sprint.sh +88 -0
  93. package/plugins/jira/skills/jira/scripts/lib/user.sh +45 -0
  94. package/plugins/jira/skills/jira/scripts/lib/worklog.sh +56 -0
  95. package/plugins/pr-babysit/.claude-plugin/plugin.json +25 -0
  96. package/plugins/pr-babysit/.codex-plugin/plugin.json +7 -0
  97. package/plugins/pr-babysit/README.md +38 -0
  98. package/plugins/pr-babysit/commands/babysit.md +13 -0
  99. package/plugins/pr-babysit/hooks/check-toolu.sh +20 -0
  100. package/plugins/pr-babysit/hooks/hooks.json +15 -0
  101. package/plugins/pr-babysit/scripts/babysit-tick.sh +133 -0
  102. package/plugins/pr-babysit/scripts/collect-pr.sh +224 -0
  103. package/plugins/pr-babysit/scripts/lib/common.sh +146 -0
  104. package/plugins/pr-babysit/scripts/lib/gh.sh +150 -0
  105. package/plugins/pr-babysit/scripts/lib/lock.sh +84 -0
  106. package/plugins/pr-babysit/scripts/lib/normalize.sh +98 -0
  107. package/plugins/pr-babysit/scripts/lib/state.sh +37 -0
  108. package/plugins/pr-babysit/scripts/parse-verdict.sh +171 -0
  109. package/plugins/pr-babysit/scripts/record.sh +69 -0
  110. package/plugins/pr-babysit/scripts/reduce-state.sh +245 -0
  111. package/plugins/pr-babysit/scripts/reply-thread.sh +90 -0
  112. package/plugins/pr-babysit/scripts/resolve-thread.sh +71 -0
  113. package/plugins/pr-babysit/skills/babysit/SKILL.md +31 -0
  114. package/plugins/pr-babysit/skills/babysit/references/helper.md +209 -0
  115. package/plugins/pr-babysit/workflows/babysit.md +722 -0
  116. package/plugins/python-quality/.claude-plugin/plugin.json +24 -0
  117. package/plugins/python-quality/.codex-plugin/plugin.json +6 -0
  118. package/plugins/python-quality/README.md +23 -0
  119. package/plugins/python-quality/hooks/check-toolu.sh +20 -0
  120. package/plugins/python-quality/hooks/concerns/00-preamble.sh +85 -0
  121. package/plugins/python-quality/hooks/concerns/10-size-file.sh +6 -0
  122. package/plugins/python-quality/hooks/concerns/20-tests.sh +37 -0
  123. package/plugins/python-quality/hooks/concerns/30-suppression.sh +33 -0
  124. package/plugins/python-quality/hooks/concerns/50-size-fn.sh +49 -0
  125. package/plugins/python-quality/hooks/concerns/70-no-mocks.sh +76 -0
  126. package/plugins/python-quality/hooks/concerns/90-docs.sh +52 -0
  127. package/plugins/python-quality/hooks/concerns/99-finalize.sh +36 -0
  128. package/plugins/python-quality/hooks/hooks.json +19 -0
  129. package/plugins/python-quality/hooks/register.sh +65 -0
  130. package/plugins/rust-quality/.claude-plugin/plugin.json +24 -0
  131. package/plugins/rust-quality/.codex-plugin/plugin.json +6 -0
  132. package/plugins/rust-quality/README.md +24 -0
  133. package/plugins/rust-quality/hooks/check-toolu.sh +20 -0
  134. package/plugins/rust-quality/hooks/concerns/00-preamble.sh +93 -0
  135. package/plugins/rust-quality/hooks/concerns/10-size-file.sh +10 -0
  136. package/plugins/rust-quality/hooks/concerns/20-tests.sh +84 -0
  137. package/plugins/rust-quality/hooks/concerns/30-suppression.sh +26 -0
  138. package/plugins/rust-quality/hooks/concerns/40-unsafe.sh +39 -0
  139. package/plugins/rust-quality/hooks/concerns/50-size-fn.sh +38 -0
  140. package/plugins/rust-quality/hooks/concerns/55-size-impl.sh +28 -0
  141. package/plugins/rust-quality/hooks/concerns/60-error-handling.sh +149 -0
  142. package/plugins/rust-quality/hooks/concerns/70-no-mocks.sh +139 -0
  143. package/plugins/rust-quality/hooks/concerns/90-docs.sh +32 -0
  144. package/plugins/rust-quality/hooks/concerns/99-finalize.sh +36 -0
  145. package/plugins/rust-quality/hooks/hooks.json +19 -0
  146. package/plugins/rust-quality/hooks/register.sh +65 -0
  147. package/plugins/statusline/.claude-plugin/plugin.json +19 -0
  148. package/plugins/statusline/.codex-plugin/plugin.json +7 -0
  149. package/plugins/statusline/README.md +108 -0
  150. package/plugins/statusline/commands/setup.md +30 -0
  151. package/plugins/statusline/hooks/hooks.json +15 -0
  152. package/plugins/statusline/hooks/session-start.sh +45 -0
  153. package/plugins/statusline/scripts/collect-status.sh +147 -0
  154. package/plugins/statusline/scripts/setup.sh +93 -0
  155. package/plugins/statusline/scripts/status.sh +55 -0
  156. package/plugins/statusline/skills/status/SKILL.md +13 -0
  157. package/plugins/statusline/statusline.sh +177 -0
  158. package/plugins/toolu/.claude-plugin/plugin.json +19 -0
  159. package/plugins/toolu/.codex-plugin/plugin.json +7 -0
  160. package/plugins/toolu/README.md +35 -0
  161. package/plugins/toolu/agents/.gitkeep +0 -0
  162. package/plugins/toolu/agents/architect.md +52 -0
  163. package/plugins/toolu/agents/deep-explore.md +65 -0
  164. package/plugins/toolu/agents/implementer.md +50 -0
  165. package/plugins/toolu/agents/quick-task.md +54 -0
  166. package/plugins/toolu/agents/research-agent.md +107 -0
  167. package/plugins/toolu/assets/agents/architect.toml +9 -0
  168. package/plugins/toolu/assets/agents/deep-explore.toml +9 -0
  169. package/plugins/toolu/assets/agents/implementer.toml +9 -0
  170. package/plugins/toolu/assets/agents/quick-task.toml +9 -0
  171. package/plugins/toolu/assets/agents/research-agent.toml +9 -0
  172. package/plugins/toolu/commands/.gitkeep +0 -0
  173. package/plugins/toolu/commands/commit.md +5 -0
  174. package/plugins/toolu/commands/review-and-commit.md +5 -0
  175. package/plugins/toolu/hooks/docs/gates.md +157 -0
  176. package/plugins/toolu/hooks/docs/model-routing.md +14 -0
  177. package/plugins/toolu/hooks/docs/post-compaction.md +3 -0
  178. package/plugins/toolu/hooks/docs/push-review.md +123 -0
  179. package/plugins/toolu/hooks/docs/session-start-python.md +3 -0
  180. package/plugins/toolu/hooks/docs/session-start-rust.md +4 -0
  181. package/plugins/toolu/hooks/docs/session-start-ts.md +3 -0
  182. package/plugins/toolu/hooks/docs/session-start.md +16 -0
  183. package/plugins/toolu/hooks/hooks.json +76 -0
  184. package/plugins/toolu/hooks/lib/config.sh +355 -0
  185. package/plugins/toolu/hooks/lib/detect.sh +929 -0
  186. package/plugins/toolu/hooks/lib/diff-sha.sh +32 -0
  187. package/plugins/toolu/hooks/lib/dispatch.sh +365 -0
  188. package/plugins/toolu/hooks/lib/docs-sync-config.sh +81 -0
  189. package/plugins/toolu/hooks/lib/edit-records.sh +157 -0
  190. package/plugins/toolu/hooks/lib/gate-file.sh +201 -0
  191. package/plugins/toolu/hooks/lib/gate-mode.sh +244 -0
  192. package/plugins/toolu/hooks/lib/host.sh +205 -0
  193. package/plugins/toolu/hooks/lib/permissions.sh +125 -0
  194. package/plugins/toolu/hooks/lib/plan-ledger-parse.sh +219 -0
  195. package/plugins/toolu/hooks/lib/plan-ledger-preflight.sh +137 -0
  196. package/plugins/toolu/hooks/lib/plan-ledger.sh +749 -0
  197. package/plugins/toolu/hooks/lib/push-waiver.sh +116 -0
  198. package/plugins/toolu/hooks/lib/quality-config.sh +184 -0
  199. package/plugins/toolu/hooks/lib/registry.sh +72 -0
  200. package/plugins/toolu/hooks/lib/state-sweeper.sh +194 -0
  201. package/plugins/toolu/hooks/lib/telemetry.sh +94 -0
  202. package/plugins/toolu/hooks/lib/verdict.sh +477 -0
  203. package/plugins/toolu/hooks/post-tools/mod.sh +42 -0
  204. package/plugins/toolu/hooks/post-tools/modules/gate-status.sh +140 -0
  205. package/plugins/toolu/hooks/post-tools/modules/push-waiver.sh +97 -0
  206. package/plugins/toolu/hooks/pre-compact.sh +18 -0
  207. package/plugins/toolu/hooks/pre-tools/agent-tier.sh +111 -0
  208. package/plugins/toolu/hooks/pre-tools/mod.sh +37 -0
  209. package/plugins/toolu/hooks/pre-tools/modules/bash-commands.sh +187 -0
  210. package/plugins/toolu/hooks/pre-tools/modules/code-edit-rules.sh +78 -0
  211. package/plugins/toolu/hooks/pre-tools/modules/commit-gate.sh +70 -0
  212. package/plugins/toolu/hooks/pre-tools/modules/docs-sync.sh +137 -0
  213. package/plugins/toolu/hooks/pre-tools/modules/mcp-blocker.sh +151 -0
  214. package/plugins/toolu/hooks/pre-tools/modules/plan-ledger.sh +201 -0
  215. package/plugins/toolu/hooks/pre-tools/modules/protected-files.sh +177 -0
  216. package/plugins/toolu/hooks/pre-tools/modules/push-review.sh +273 -0
  217. package/plugins/toolu/hooks/pre-tools/modules/quality-gate.sh +87 -0
  218. package/plugins/toolu/hooks/session-start.sh +379 -0
  219. package/plugins/toolu/hooks/user-prompt-submit.sh +197 -0
  220. package/plugins/toolu/scripts/context-budget.sh +111 -0
  221. package/plugins/toolu/scripts/debug-log.sh +98 -0
  222. package/plugins/toolu/scripts/debug-stack.sh +108 -0
  223. package/plugins/toolu/scripts/debug-testfail.sh +97 -0
  224. package/plugins/toolu/settings/.gitkeep +0 -0
  225. package/plugins/toolu/settings/README.md +127 -0
  226. package/plugins/toolu/settings/bash-allowlist.txt +12 -0
  227. package/plugins/toolu/settings/bash-denylist.txt +25 -0
  228. package/plugins/toolu/settings/code-edit-rules.json +67 -0
  229. package/plugins/toolu/settings/commit-prefixes.txt +12 -0
  230. package/plugins/toolu/settings/mcp-blocklist.txt +8 -0
  231. package/plugins/toolu/settings/permissions.fragment.json +36 -0
  232. package/plugins/toolu/settings/protected-files.txt +30 -0
  233. package/plugins/toolu/settings/rust-unsafe-exemptions.txt +5 -0
  234. package/plugins/toolu/settings/toolu.config.example.json +130 -0
  235. package/plugins/toolu/skills/.gitkeep +0 -0
  236. package/plugins/toolu/skills/brainstorm/SKILL.md +57 -0
  237. package/plugins/toolu/skills/brainstorm/references/design-questions.md +36 -0
  238. package/plugins/toolu/skills/commit/SKILL.md +11 -0
  239. package/plugins/toolu/skills/debug/SKILL.md +54 -0
  240. package/plugins/toolu/skills/debug/evals/README.md +17 -0
  241. package/plugins/toolu/skills/debug/evals/cases.json +24 -0
  242. package/plugins/toolu/skills/debug/evals/results.json +34 -0
  243. package/plugins/toolu/skills/deep-research/SKILL.md +51 -0
  244. package/plugins/toolu/skills/execution/SKILL.md +77 -0
  245. package/plugins/toolu/skills/orchestrator/SKILL.md +145 -0
  246. package/plugins/toolu/skills/orchestrator/references/delegation-cost.md +104 -0
  247. package/plugins/toolu/skills/orchestrator/references/model-routing.md +112 -0
  248. package/plugins/toolu/skills/plan/SKILL.md +56 -0
  249. package/plugins/toolu/skills/plan/references/ledger.md +18 -0
  250. package/plugins/toolu/skills/plan-review/SKILL.md +48 -0
  251. package/plugins/toolu/skills/review-and-commit/SKILL.md +10 -0
  252. package/plugins/toolu/skills/setup/SKILL.md +24 -0
  253. package/plugins/toolu/skills/setup/scripts/setup.sh +206 -0
  254. package/plugins/toolu/skills/spec/SKILL.md +67 -0
  255. package/plugins/toolu/skills/spec-review/SKILL.md +50 -0
  256. package/plugins/toolu/skills/test/SKILL.md +49 -0
  257. package/plugins/toolu/workflows/commit.md +21 -0
  258. package/plugins/toolu/workflows/host-mapping.md +22 -0
  259. package/plugins/toolu/workflows/review-and-commit.md +54 -0
  260. package/plugins/toolu/workflows/semantic-judgments.md +35 -0
  261. package/plugins/toolu-review/.claude-plugin/plugin.json +18 -0
  262. package/plugins/toolu-review/.codex-plugin/plugin.json +7 -0
  263. package/plugins/toolu-review/README.md +17 -0
  264. package/plugins/toolu-review/hooks/hooks.json +15 -0
  265. package/plugins/toolu-review/hooks/session-start.sh +34 -0
  266. package/plugins/toolu-review/skills/review/SKILL.md +90 -0
  267. package/plugins/toolu-review/skills/review/scripts/write-state.sh +189 -0
  268. package/plugins/ts-quality/.claude-plugin/plugin.json +24 -0
  269. package/plugins/ts-quality/.codex-plugin/plugin.json +6 -0
  270. package/plugins/ts-quality/README.md +24 -0
  271. package/plugins/ts-quality/hooks/check-toolu.sh +20 -0
  272. package/plugins/ts-quality/hooks/concerns/00-preamble.sh +123 -0
  273. package/plugins/ts-quality/hooks/concerns/10-imports.sh +33 -0
  274. package/plugins/ts-quality/hooks/concerns/15-type-as.sh +9 -0
  275. package/plugins/ts-quality/hooks/concerns/20-tests.sh +26 -0
  276. package/plugins/ts-quality/hooks/concerns/25-size-file.sh +40 -0
  277. package/plugins/ts-quality/hooks/concerns/30-size-fn.sh +53 -0
  278. package/plugins/ts-quality/hooks/concerns/35-react-hooks.sh +7 -0
  279. package/plugins/ts-quality/hooks/concerns/40-factory.sh +5 -0
  280. package/plugins/ts-quality/hooks/concerns/45-typeguard.sh +6 -0
  281. package/plugins/ts-quality/hooks/concerns/50-type-dup.sh +16 -0
  282. package/plugins/ts-quality/hooks/concerns/55-naming.sh +11 -0
  283. package/plugins/ts-quality/hooks/concerns/60-console.sh +6 -0
  284. package/plugins/ts-quality/hooks/concerns/65-suppression.sh +12 -0
  285. package/plugins/ts-quality/hooks/concerns/70-ui-confirm.sh +8 -0
  286. package/plugins/ts-quality/hooks/concerns/72-ui-radix.sh +12 -0
  287. package/plugins/ts-quality/hooks/concerns/74-react-props.sh +6 -0
  288. package/plugins/ts-quality/hooks/concerns/76-toast.sh +10 -0
  289. package/plugins/ts-quality/hooks/concerns/78-error-ast.sh +242 -0
  290. package/plugins/ts-quality/hooks/concerns/80-throw-literal.sh +23 -0
  291. package/plugins/ts-quality/hooks/concerns/85-no-mocks.sh +109 -0
  292. package/plugins/ts-quality/hooks/concerns/90-duplication.sh +30 -0
  293. package/plugins/ts-quality/hooks/concerns/92-docs.sh +36 -0
  294. package/plugins/ts-quality/hooks/concerns/94-handler.sh +15 -0
  295. package/plugins/ts-quality/hooks/concerns/99-finalize.sh +46 -0
  296. package/plugins/ts-quality/hooks/hooks.json +19 -0
  297. package/plugins/ts-quality/hooks/register.sh +65 -0
  298. package/src/adapter/__tests__/evaluate.test.ts +82 -0
  299. package/src/adapter/__tests__/permission-map.test.ts +91 -0
  300. package/src/adapter/evaluate.ts +54 -0
  301. package/src/adapter/permission-map.ts +177 -0
  302. package/src/bootstrap/__tests__/bootstrap.test.ts +148 -0
  303. package/src/bootstrap/entrypoint.ts +18 -0
  304. package/src/bootstrap/index.ts +5 -0
  305. package/src/bootstrap/readiness.ts +59 -0
  306. package/src/bootstrap/result.ts +21 -0
  307. package/src/bootstrap/runtime.ts +99 -0
  308. package/src/bootstrap/test-helpers.ts +25 -0
  309. package/src/host/__tests__/host.test.ts +37 -0
  310. package/src/host/detect.ts +46 -0
  311. package/src/host/index.ts +10 -0
  312. package/src/host/roots.ts +59 -0
  313. package/src/host/types.ts +14 -0
  314. package/src/inventory/__tests__/inventory.test.ts +69 -0
  315. package/src/inventory/index.ts +9 -0
  316. package/src/inventory/manifest.ts +46 -0
  317. package/src/inventory/scan.ts +66 -0
  318. package/src/inventory/selection.ts +92 -0
  319. package/src/inventory/types.ts +22 -0
  320. package/src/lifecycle/__tests__/lifecycle.test.ts +7 -0
  321. package/src/lifecycle/index.ts +2 -0
  322. package/src/lifecycle/table.ts +42 -0
  323. package/src/plugin/__tests__/plugin-stub.test.ts +6 -0
  324. package/src/plugin/plugin-stub.ts +7 -0
  325. package/src/plugin/toolu.ts +142 -0
  326. package/src/preflight/__tests__/preflight.test.ts +20 -0
  327. package/src/preflight/check.ts +75 -0
  328. package/src/preflight/index.ts +2 -0
  329. package/src/select/__tests__/select.test.ts +52 -0
  330. package/src/select/index.ts +2 -0
  331. package/src/select/resolve.ts +87 -0
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-tool check: on `git push`, flag when the branch diff changes code but
3
+ # touches no documentation surface — the docs-sync backstop for the toolu
4
+ # workflow's "Docs in sync" convention. Silenced by a diff-sha-keyed
5
+ # attestation the agent writes when no doc change is warranted.
6
+ #
7
+ # Project-agnostic. Behavior is controlled by `docsSync.mode`
8
+ # (advise|block|off, via toolu_string — unrecognized values warn and fall
9
+ # back to advise):
10
+ # - advise (default): emits hookSpecificOutput.additionalContext (merged by
11
+ # dispatch.sh) and NEVER a permissionDecision — it informs, it does not
12
+ # gate (push-review.sh is the gate).
13
+ # - block: denies the push (permissionDecision: "deny") when code changed
14
+ # with no doc surface and no matching attestation. A valid attestation
15
+ # still allows the push in this mode.
16
+ # - off: the check is fully disabled — no telemetry, no output, exit 0.
17
+ # Not just "suppress the message": an off branch must be indistinguishable
18
+ # from docs-sync not existing at all.
19
+ #
20
+ # Inputs (exported by pre-tools/mod.sh): $tool_name, $input (JSON, also stdin).
21
+ # Attestation: <host-state>/tmp/docs-sync/<branch-slug>.json (override $DOCS_SYNC_STATE_DIR).
22
+ # Base branch: $DOCS_SYNC_BASE override, else detect_base_branch.
23
+
24
+ # pipefail so a `git diff | git hash-object` failure surfaces rather than
25
+ # yielding the well-known empty-blob SHA from a broken pipe.
26
+ set -o pipefail
27
+
28
+ : "${tool_name:=}"
29
+ : "${input:=}"
30
+
31
+ _toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
32
+ # shellcheck source=../../lib/detect.sh
33
+ . "$_toolu_lib/detect.sh"
34
+ # shellcheck source=../../lib/config.sh
35
+ . "$_toolu_lib/config.sh"
36
+ # shellcheck source=../../lib/docs-sync-config.sh
37
+ . "$_toolu_lib/docs-sync-config.sh"
38
+ # shellcheck source=../../lib/diff-sha.sh
39
+ . "$_toolu_lib/diff-sha.sh"
40
+ # shellcheck source=../../lib/telemetry.sh
41
+ . "$_toolu_lib/telemetry.sh"
42
+ # shellcheck source=../../lib/gate-mode.sh
43
+ . "$_toolu_lib/gate-mode.sh"
44
+
45
+ # Degrade silent on anything that isn't a real push in a real branch.
46
+ [[ "$tool_name" != "Bash" ]] && exit 0
47
+ command -v jq >/dev/null 2>&1 || exit 0
48
+ command -v git >/dev/null 2>&1 || exit 0
49
+
50
+ command=$(echo "$input" | jq -r '.tool_input.command // ""')
51
+ is_git_push "$command" || exit 0
52
+
53
+ current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
54
+ # Detached HEAD or no branch — nothing to slug/key on; stay silent.
55
+ [[ -z "$current_branch" || "$current_branch" == "HEAD" ]] && exit 0
56
+
57
+ base_branch="${DOCS_SYNC_BASE:-$(detect_base_branch)}"
58
+ # Missing base or pushing the base itself: no feature diff to judge.
59
+ git rev-parse --verify --quiet "$base_branch" >/dev/null 2>&1 || exit 0
60
+ [[ "$current_branch" == "$base_branch" ]] && exit 0
61
+
62
+ # Changed paths drive surface matching; an empty list means nothing to nudge.
63
+ changed=$(git diff --name-only "${base_branch}...HEAD" 2>/dev/null || echo "")
64
+ [[ -z "$changed" ]] && exit 0
65
+
66
+ # Content-addressed diff sha (mirrors push-review.sh) keys the attestation, so
67
+ # changing the code invalidates a stale "not-needed" attestation.
68
+ diff_sha=$(toolu_diff_sha . "$base_branch")
69
+
70
+ # Classify the changed paths against the configurable glob sets. `case` fnmatch
71
+ # treats a single `*` as crossing `/`, so `docs/*.md` covers nested paths.
72
+ surfaces=$(docs_sync_surfaces)
73
+ surface_excludes=$(docs_sync_surface_excludes)
74
+ code_surfaces=$(docs_sync_code_surfaces)
75
+
76
+ _matches_any() {
77
+ local path="$1" glob
78
+ while IFS= read -r glob; do
79
+ [[ -z "$glob" ]] && continue
80
+ # shellcheck disable=SC2254 # $glob is an intentional fnmatch pattern.
81
+ case "$path" in
82
+ $glob) return 0 ;;
83
+ esac
84
+ done <<< "$2"
85
+ return 1
86
+ }
87
+
88
+ has_doc=0 has_code=0
89
+ while IFS= read -r path; do
90
+ [[ -z "$path" ]] && continue
91
+ # A doc hit is a surface match that is NOT carved out by an exclude (e.g.
92
+ # release notes, which `docs/*.md` would otherwise swallow since `*` crosses `/`).
93
+ if _matches_any "$path" "$surfaces" && ! _matches_any "$path" "$surface_excludes"; then
94
+ has_doc=1
95
+ fi
96
+ if _matches_any "$path" "$code_surfaces"; then has_code=1; fi
97
+ done <<< "$changed"
98
+
99
+ # Nudge only when code changed AND no doc surface did.
100
+ { [[ "$has_code" == 1 && "$has_doc" == 0 ]]; } || exit 0
101
+
102
+ # off must be indistinguishable from the module not existing: no telemetry
103
+ # (not even docs_attested for an otherwise-valid attestation below), no
104
+ # output. Read here (rather than at top-of-file) so an off branch still pays
105
+ # no cost beyond the code/doc classification already needed either way.
106
+ mode=$(toolu_gate_mode docsSync)
107
+ [[ "$mode" == "off" ]] && exit 0
108
+
109
+ # Attestation gate: a fresh (diff-sha-matching) attestation silences the nudge
110
+ # (in both advise and block mode).
111
+ slug=$(branch_slug "$current_branch")
112
+ state_dir=${DOCS_SYNC_STATE_DIR:-$(toolu_project_state_dir docs-sync)}
113
+ state_file="$state_dir/${slug}.json"
114
+ if [[ -f "$state_file" && -n "$diff_sha" ]]; then
115
+ attested_sha=$(jq -r '.diff_sha // ""' "$state_file" 2>/dev/null || echo "")
116
+ if [[ "$attested_sha" == "$diff_sha" ]]; then
117
+ attested_decision=$(jq -r '.decision // ""' "$state_file" 2>/dev/null || echo "")
118
+ telemetry_append "$(detect_project_root)" "docs_attested" \
119
+ "$(jq -cn --arg d "$attested_decision" '{decision: $d}')"
120
+ exit 0
121
+ fi
122
+ fi
123
+
124
+ telemetry_append "$(detect_project_root)" "docs_nudge"
125
+
126
+ # Shared instructions (the attestation escape hatch) for both modes; only the
127
+ # closing sentence and the JSON shape (additionalContext vs. deny) differ.
128
+ # The instructions (and the attestation escape hatch) are the same in every
129
+ # mode; only the closing sentence changes, because what happens next does.
130
+ case "$mode" in
131
+ block) consequence="Push denied until a doc is updated or a valid attestation is written." ;;
132
+ ask) consequence="Approve to push anyway, or update the doc first." ;;
133
+ *) consequence="Advisory only — this does not block the push." ;;
134
+ esac
135
+
136
+ toolu_gate_emit "$mode" "docs-sync: this branch changes code but no documentation surface (README / docs/*.md / SKILL.md). Update the doc that describes this behavior, OR attest none is needed by writing $state_file with { \"version\": 1, \"diff_sha\": \"$diff_sha\", \"decision\": \"not-needed\", \"note\": \"why\" }. $consequence"
137
+ exit 0
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ # Block MCP tool usage for listed servers — redirect to CLI scripts.
3
+ # Data-driven: server name prefixes come from $TOOLU_SETTINGS_DIR/mcp-blocklist.txt.
4
+ #
5
+ # Delivery is a MODE (gates.mcpBlocker.mode), not a constant. It ships as
6
+ # `ask`: the user is prompted and decides per call, instead of the call being
7
+ # refused with no way through. `block` restores the old hard deny; `advise`
8
+ # only warns; `off` disables it. On a host that cannot prompt, `ask` degrades
9
+ # to `block` — see lib/gate-mode.sh.
10
+ #
11
+ # Inputs (from parent dispatcher pre-tools/mod.sh, via `export`):
12
+ # $tool_name - name of the tool being invoked
13
+
14
+ : "${tool_name:=}"
15
+
16
+ _toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
17
+ # shellcheck source=../../lib/detect.sh
18
+ . "$_toolu_lib/detect.sh"
19
+ # shellcheck source=../../lib/config.sh
20
+ . "$_toolu_lib/config.sh"
21
+ # shellcheck source=../../lib/gate-mode.sh
22
+ . "$_toolu_lib/gate-mode.sh"
23
+
24
+ command -v jq >/dev/null 2>&1 || exit 0
25
+
26
+ # This module is wired as a STANDALONE PreToolUse hook (matcher mcp__), so it
27
+ # does NOT go through pre-tools/mod.sh and the parent never `export`s
28
+ # tool_name. Claude Code delivers the hook input as JSON on STDIN, including
29
+ # .tool_name. When tool_name is not in the env, read it from stdin. Env keeps
30
+ # precedence so env-driven tests still pass; stdin is consumed exactly once.
31
+ if [[ -z "$tool_name" ]]; then
32
+ _mcp_stdin=$(cat 2>/dev/null || true)
33
+ if [[ -n "$_mcp_stdin" ]]; then
34
+ tool_name=$(jq -r '.tool_name // empty' <<<"$_mcp_stdin" 2>/dev/null || true)
35
+ fi
36
+ fi
37
+
38
+ TOOLU_SETTINGS_DIR=$(toolu_settings_dir)
39
+ LIST_FILE="$TOOLU_SETTINGS_DIR/mcp-blocklist.txt"
40
+
41
+ # Only inspect mcp__<server>__* tool names.
42
+ case "$tool_name" in
43
+ mcp__*__*) ;;
44
+ *) exit 0 ;;
45
+ esac
46
+
47
+ # Fast-path: the matcher is mcp__, so every MCP tool call routes through
48
+ # here. Skip config-load work entirely when there is no
49
+ # blocklist file AND no user/project config to consult — the common case for
50
+ # anyone who has not opted into the gate.
51
+ if [ ! -f "$LIST_FILE" ] && ! toolu_config_exists; then
52
+ exit 0
53
+ fi
54
+
55
+ # Extract the server segment: mcp__<server>__rest
56
+ rest="${tool_name#mcp__}"
57
+ server="${rest%%__*}"
58
+
59
+ # read_list is sourced from lib/detect.sh.
60
+
61
+ toolu_load_config
62
+ # `objects` guards against a malformed `.mcp` (string/array) — to_entries
63
+ # would error on a non-object; with the guard it is simply a no-op.
64
+ disabled_from_cfg=$(jq -r '.mcp // {} | objects | to_entries[] | select(.value == false) | .key' \
65
+ <<< "${TOOLU_CFG_JSON:-}" 2>/dev/null)
66
+
67
+ file_list="$(read_list "$LIST_FILE")"
68
+
69
+ # Track WHERE the matching entry came from so the deny reason can point the
70
+ # user at the right file to edit.
71
+ blocked_source=""
72
+ # Redirect hint captured from the matching entry (file list only); surfaced in
73
+ # the deny reason so a denied call points at its replacement. Empty when the
74
+ # matched entry carries no hint.
75
+ matched_redirect=""
76
+ match_entry() {
77
+ local list_input="$1" name prefix redirect
78
+ while IFS= read -r name; do
79
+ [ -z "$name" ] && continue
80
+ # An entry may carry an optional " -> <text>" redirect hint after the
81
+ # server prefix (e.g. `claude_ai_Atlassian -> use the jira skill instead`).
82
+ # Split it off so the prefix alone drives the match; without this the arrow
83
+ # suffix would defeat the prefix-match below. Config-derived lists are bare
84
+ # server keys and never carry a hint.
85
+ if [[ "$name" == *" -> "* ]]; then
86
+ prefix="${name%% -> *}"
87
+ redirect="${name#* -> }"
88
+ else
89
+ prefix="$name"
90
+ redirect=""
91
+ fi
92
+ # Trim surrounding whitespace so a stray space can't defeat the exact match.
93
+ prefix="${prefix#"${prefix%%[![:space:]]*}"}"
94
+ prefix="${prefix%"${prefix##*[![:space:]]}"}"
95
+ [ -z "$prefix" ] && continue
96
+ # Prefix match: an entry of `claude_ai_` blocks `claude_ai_Canva`. An
97
+ # exact server name is the special case of a prefix with no suffix.
98
+ case "$server" in
99
+ "$prefix"*) matched_redirect="$redirect"; return 0 ;;
100
+ esac
101
+ done <<< "$list_input"
102
+ return 1
103
+ }
104
+
105
+ if match_entry "$file_list"; then
106
+ blocked_source="file"
107
+ elif match_entry "$disabled_from_cfg"; then
108
+ blocked_source="config"
109
+ fi
110
+
111
+ if [ -n "$blocked_source" ]; then
112
+ # Same validate-then-fail-closed discipline as protected-files.sh: an
113
+ # unresolvable mode blocks, and says that it did.
114
+ mode_note=""
115
+ mode=$(toolu_gate_mode mcpBlocker) || mode=""
116
+ case " $TOOLU_GATE_MODES " in
117
+ *" $mode "*) ;;
118
+ *)
119
+ printf 'toolu-mcp-blocker: could not resolve gates.mcpBlocker.mode (got %s); blocking\n' \
120
+ "${mode:-<empty>}" >&2
121
+ mode_note=" [toolu could not resolve gates.mcpBlocker.mode, so this failed closed]"
122
+ mode="block"
123
+ ;;
124
+ esac
125
+
126
+ if [ "$blocked_source" = "file" ]; then
127
+ origin="listed in settings/mcp-blocklist.txt"
128
+ else
129
+ origin="disabled in your toolu config (mcp.${server}=false)"
130
+ fi
131
+
132
+ headline="Claude is calling MCP tool \"$tool_name\", on the blocked server \"$server\" ($origin)."
133
+ detail="Blocked MCP servers are ones this project has decided not to reach through an MCP bridge — usually because a CLI path exists that is auditable and scoped, where the MCP tool is neither."
134
+ [ -n "$matched_redirect" ] && detail="$detail Use instead: $matched_redirect"
135
+
136
+ case "$mode" in
137
+ ask)
138
+ reason=$(toolu_gate_guardrail_warning "$headline" "$detail") || reason=""
139
+ [ -n "$reason" ] || reason="$headline $detail"
140
+ ;;
141
+ advise)
142
+ reason="MCP server \"$server\" is blocked ($origin). $detail The call was NOT stopped — gates.mcpBlocker.mode is 'advise'."
143
+ ;;
144
+ *)
145
+ reason="MCP server \"$server\" is blocked ($origin). $detail"
146
+ ;;
147
+ esac
148
+
149
+ toolu_gate_emit "$mode" "${reason}${mode_note}"
150
+ fi
151
+ exit 0
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-tool gate: block `git push` until every plan-ledger step is fresh-green.
3
+ # READ-ONLY — this gate never runs a step's `check`; it only reads the ledger
4
+ # the checker (plan-ledger.sh) wrote. A step is "fresh-green" iff its recorded
5
+ # status is green AND its recorded diff_sha equals the current branch diff_sha,
6
+ # so a green that predates a later code change is treated as stale, not done.
7
+ #
8
+ # Inputs (from parent dispatcher pre-tools/mod.sh, via `export`):
9
+ # $tool_name - name of the tool being invoked
10
+ # $input - raw JSON payload (also piped to stdin; this module reads the env var)
11
+ #
12
+ # Ledger file: <project-root>/<host-state>/tmp/plan-ledger/<branch-slug>.json
13
+ # Override the parent dir via $LEDGER_DIR for testing.
14
+ #
15
+ # Decisions:
16
+ # - tool != Bash, or not a `git push` -> allow (exit 0, no output).
17
+ # - ledger absent, push diff touches a code file -> allow + stderr advisory.
18
+ # - ledger absent, no code files -> allow silently.
19
+ # - ledger unparseable / version != 1 -> deny (fail closed).
20
+ # - summary.total == 0 / steps empty -> allow (no-op).
21
+ # - every step fresh-green -> allow.
22
+ # - any red/pending/stale step -> deny, listing each.
23
+ # - planLedger.blockOnUncoveredAcs=true + an UNCOVERED spec AC -> deny,
24
+ # naming the AC id(s) (default false: advisory ac_coverage count only).
25
+
26
+ # pipefail so a `git diff | git hash-object` failure surfaces; without it an
27
+ # empty diff stream succeeds and yields the well-known empty-blob SHA.
28
+ set -o pipefail
29
+
30
+ : "${tool_name:=}"
31
+ : "${input:=}"
32
+
33
+ _toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
34
+ # shellcheck source=../../lib/detect.sh
35
+ . "$_toolu_lib/detect.sh"
36
+ # shellcheck source=../../lib/plan-ledger-parse.sh
37
+ . "$_toolu_lib/plan-ledger-parse.sh"
38
+
39
+ [[ "$tool_name" != "Bash" ]] && exit 0
40
+
41
+ command -v jq >/dev/null 2>&1 || exit 0
42
+ command -v git >/dev/null 2>&1 || exit 0
43
+
44
+ command=$(echo "$input" | jq -r '.tool_input.command // ""')
45
+
46
+ # Push detection (strip_heredocs + boundary-anchored regex) shared via detect.sh.
47
+ is_git_push "$command" || exit 0
48
+
49
+ # Sourced here (past the Edit/Write/Grep and non-push cheap exits above) so
50
+ # every other tool call skips the extra jq-merge/telemetry-lib load these four
51
+ # pull in — only an actual `git push` pays for them.
52
+ # shellcheck source=../../lib/diff-sha.sh
53
+ . "$_toolu_lib/diff-sha.sh"
54
+ # shellcheck source=../../lib/plan-ledger.sh
55
+ . "$_toolu_lib/plan-ledger.sh"
56
+ # shellcheck source=../../lib/telemetry.sh
57
+ . "$_toolu_lib/telemetry.sh"
58
+ # shellcheck source=../../lib/config.sh
59
+ . "$_toolu_lib/config.sh"
60
+ # shellcheck source=../../lib/gate-mode.sh
61
+ . "$_toolu_lib/gate-mode.sh"
62
+
63
+ # How firmly this gate speaks is configurable (lib/gate-mode.sh); `off` means
64
+ # it has nothing to say, so skip the git plumbing entirely.
65
+ mode=$(toolu_gate_mode planLedger)
66
+ [ "$mode" = "off" ] && exit 0
67
+
68
+ # Base branch: env override > detect_base_branch (must agree with the checker).
69
+ base_branch="${PUSH_REVIEW_BASE:-$(detect_base_branch)}"
70
+
71
+ current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
72
+ slug=$(branch_slug "$current_branch")
73
+
74
+ # Ledger lives next to push-review's state, keyed by the same branch slug.
75
+ ledger_dir="${LEDGER_DIR:-$(toolu_project_state_dir plan-ledger "$(detect_project_root)")}"
76
+ state_file="$ledger_dir/${slug}.json"
77
+
78
+ # Ledger absent: never deny on absence. Nudge only when the push diff touches a
79
+ # code file (escape hatch for mechanical work — see spec Non-Goal 3).
80
+ if [[ ! -f "$state_file" ]]; then
81
+ code_files=$(git diff --no-color "${base_branch}...HEAD" --name-only 2>/dev/null \
82
+ | grep -E '\.(ts|tsx|js|jsx|rs|sh|py|go)$' || true)
83
+ if [[ -n "$code_files" ]]; then
84
+ echo "plan-ledger: no plan ledger for this change; if non-trivial, run plan" >&2
85
+ fi
86
+ exit 0
87
+ fi
88
+
89
+ # Read + validate the ledger. Unparseable -> fail closed (deny).
90
+ ledger=$(pl_read_ledger "$state_file") || {
91
+ jq -n --arg file "$state_file" '{
92
+ "hookSpecificOutput": {
93
+ "hookEventName": "PreToolUse",
94
+ "permissionDecision": "deny",
95
+ "permissionDecisionReason": ("plan-ledger: unparseable ledger at " + $file + "; delete and re-run `bash plugins/toolu/hooks/lib/plan-ledger.sh run <plan_doc>`")
96
+ }
97
+ }'
98
+ exit 0
99
+ }
100
+
101
+ # Schema gate: version must be 1 (fail closed on mismatch).
102
+ version=$(jq -r '.version // ""' <<<"$ledger" 2>/dev/null || echo "")
103
+ if [[ "$version" != "1" ]]; then
104
+ toolu_gate_emit "$mode" "plan-ledger: ledger schema mismatch at $state_file (version=\"$version\", expected 1); delete and re-run \`bash plugins/toolu/hooks/lib/plan-ledger.sh run <plan_doc>\`"
105
+ exit 0
106
+ fi
107
+
108
+ # No-op ledger: total 0 or no steps -> allow (identical to absent, spec crit12).
109
+ total=$(jq -r '.summary.total // 0' <<<"$ledger" 2>/dev/null || echo "0")
110
+ step_count=$(jq -r '.steps | length' <<<"$ledger" 2>/dev/null || echo "0")
111
+ if [[ "$total" == "0" || "$step_count" == "0" ]]; then
112
+ exit 0
113
+ fi
114
+
115
+ # Current branch diff_sha — the content hash the checker stamps when a step runs.
116
+ current_diff_sha=$(toolu_diff_sha . "$base_branch")
117
+ if [[ -z "$current_diff_sha" ]]; then
118
+ # git diff failed (disk full, etc): allow so the underlying push surfaces the
119
+ # real error rather than denying on an indeterminate read.
120
+ echo "plan-ledger: git diff ${base_branch}...HEAD failed; allowing push to surface real error" >&2
121
+ exit 0
122
+ fi
123
+
124
+ # ac_coverage telemetry (report-only counts; never affects the decision below).
125
+ # Mirrors lib/plan-ledger.sh's pl_cmd_status AC-coverage resolution: read the
126
+ # ledger's plan_doc, resolve its **Spec:** field, and reuse pl_ac_coverage_lines
127
+ # (sourced from lib/plan-ledger.sh above) to get the per-AC report, then count
128
+ # covered vs UNCOVERED lines in it. Spec-less plans print no report -> no event.
129
+ ac_plan_doc=$(jq -r '.plan_doc // ""' <<<"$ledger" 2>/dev/null || echo "")
130
+ if [[ -n "$ac_plan_doc" ]]; then
131
+ ac_root=$(detect_project_root)
132
+ [[ -f "$ac_plan_doc" ]] || { [[ -n "$ac_root" && -f "$ac_root/$ac_plan_doc" ]] && ac_plan_doc="$ac_root/$ac_plan_doc"; }
133
+ if [[ -f "$ac_plan_doc" ]]; then
134
+ ac_spec_field=$(pl_doc_field "$ac_plan_doc" Spec)
135
+ ac_spec_path="$ac_spec_field"
136
+ case "$(printf '%s' "$ac_spec_field" | tr '[:upper:]' '[:lower:]')" in
137
+ ""|none) : ;;
138
+ *) [[ -f "$ac_spec_path" ]] || { [[ -n "$ac_root" && -f "$ac_root/$ac_spec_field" ]] && ac_spec_path="$ac_root/$ac_spec_field"; } ;;
139
+ esac
140
+ ac_report=$(pl_ac_coverage_lines "$ledger" "$current_diff_sha" "$ac_spec_path" 2>/dev/null)
141
+ if [[ -n "$ac_report" ]]; then
142
+ ac_total=$(grep -c '^ AC-' <<<"$ac_report" || true)
143
+ ac_uncovered=$(grep -Ec '^ AC-[^:]+: UNCOVERED' <<<"$ac_report" || true)
144
+ ac_covered=$(( ac_total - ac_uncovered ))
145
+ telemetry_append "$ac_root" "ac_coverage" \
146
+ "$(jq -cn --argjson c "$ac_covered" --argjson u "$ac_uncovered" '{covered: $c, uncovered: $u}')"
147
+
148
+ # Promotion (spec component 8): planLedger.blockOnUncoveredAcs (default
149
+ # false) turns UNCOVERED spec ACs from an advisory count into a push
150
+ # deny. The event above already recorded the counts either way.
151
+ if [[ "$ac_uncovered" -gt 0 ]] && toolu_flag_true planLedger blockOnUncoveredAcs; then
152
+ ac_uncovered_lines=$(grep -E '^ AC-[^:]+: UNCOVERED' <<<"$ac_report" | sed -E 's/^ //')
153
+ ac_reason=$(printf 'plan-ledger: push blocked — uncovered spec AC id(s):\n%s\n\ncover with a fresh-green step referencing it in ac_refs, or set planLedger.blockOnUncoveredAcs=false' "$ac_uncovered_lines")
154
+ toolu_gate_emit "$mode" "$ac_reason"
155
+ exit 0
156
+ fi
157
+ fi
158
+ fi
159
+ fi
160
+
161
+ plan_doc_hint=$(jq -r '.plan_doc // "<plan_doc>"' <<<"$ledger" 2>/dev/null || echo "<plan_doc>")
162
+
163
+ # Build the list of non-fresh-green steps. A step is fresh-green iff
164
+ # status==green AND diff_sha==current; a green with a stale diff_sha is `stale`.
165
+ # `effective` is the status the agent sees: green -> stale when sha drifts.
166
+ blockers=$(jq -r --arg cur "$current_diff_sha" '
167
+ .steps[]
168
+ | (if (.status == "green" and .diff_sha == $cur) then "green"
169
+ elif (.status == "green") then "stale"
170
+ else (.status // "pending") end) as $eff
171
+ | select($eff != "green")
172
+ | (.id // "?") + ": " + $eff + " — " + (.title // "")
173
+ ' <<<"$ledger" 2>/dev/null || echo "")
174
+
175
+ # A step may be fresh-green on its own declared `paths` — enough to keep
176
+ # iteration fast, not enough to push on. The gate requires a --verify run:
177
+ # every step judged against the whole branch diff, so a narrow or stale scope
178
+ # declaration cannot hold a green through a change it failed to account for.
179
+ # A ledger written before per-step scope existed has no `verified_sha` key at
180
+ # all, and every step in it was judged against the whole branch diff — which is
181
+ # exactly what --verify means. Requiring the field of those would deny every
182
+ # push until the plan is re-run, for no safety gained. The new contract applies
183
+ # only to ledgers that carry the key.
184
+ has_verified=$(jq -r 'has("verified_sha")' <<<"$ledger" 2>/dev/null || echo "false")
185
+ verified_sha=$(jq -r '.verified_sha // ""' <<<"$ledger" 2>/dev/null || echo "")
186
+ if [[ -z "$blockers" && "$has_verified" == "true" && "$verified_sha" != "$current_diff_sha" ]]; then
187
+ reason=$(printf 'plan-ledger: every step is green, but not verified against the current diff.\n\nScoped runs judge a step on its own `paths`; the push gate judges it on the whole branch.\n\nrun: bash plugins/toolu/hooks/lib/plan-ledger.sh run %s --verify' "$plan_doc_hint")
188
+ toolu_gate_emit "$mode" "$reason"
189
+ exit 0
190
+ fi
191
+
192
+ # All fresh-green AND verified -> allow.
193
+ if [[ -z "$blockers" ]]; then
194
+ exit 0
195
+ fi
196
+
197
+ # Otherwise deny, listing each non-fresh-green step + remediation.
198
+ plan_doc="$plan_doc_hint"
199
+ reason=$(printf 'plan-ledger: push blocked — steps not fresh-green:\n%s\n\nrun: bash plugins/toolu/hooks/lib/plan-ledger.sh run %s' "$blockers" "$plan_doc")
200
+ toolu_gate_emit "$mode" "$reason"
201
+ exit 0
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-tool check: Protect quality infrastructure files from edits.
3
+ # Data-driven: globs come from $TOOLU_SETTINGS_DIR/protected-files.txt.
4
+ #
5
+ # Delivery is a MODE (gates.protectedFiles.mode), not a constant. It ships as
6
+ # `ask`: the user is prompted with a loud warning and decides, per call. It
7
+ # used to be an unconditional deny with no way through in-session, which is
8
+ # what github.com/Falconiere/toolu/issues/176 reported — an agent could be told
9
+ # "yes, edit that .env.example" by the user and STILL be refused, with the deny
10
+ # text claiming an override existed that did not.
11
+ #
12
+ # `block` restores the old hard deny; `advise` only warns; `off` disables the
13
+ # check. On a host that cannot prompt, `ask` degrades to `block`, never to
14
+ # `advise` — see lib/gate-mode.sh.
15
+ #
16
+ # Inputs (from parent dispatcher pre-tools/mod.sh, via `export`):
17
+ # $tool_name - name of the tool being invoked
18
+ # $input - raw JSON payload on stdin
19
+
20
+ : "${tool_name:=}"
21
+ : "${input:=}"
22
+
23
+ _toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}/../../lib}"
24
+ # shellcheck source=../../lib/detect.sh
25
+ . "$_toolu_lib/detect.sh"
26
+ # shellcheck source=../../lib/gate-mode.sh
27
+ . "$_toolu_lib/gate-mode.sh"
28
+
29
+ # MultiEdit is in the PreToolUse matcher and carries .tool_input.file_path just
30
+ # like Edit/Write — omitting it here would let a MultiEdit silently bypass the
31
+ # protected-files deny (a security-equivalent hole).
32
+ #
33
+ # Bash/Shell are also in scope: a structured Edit/Write on a protected path is
34
+ # denied, but the same bytes written via `sed -i`, a redirect, or `python3 -c
35
+ # "open(...).write(...)"` had no path-shaped tool_input to check and sailed
36
+ # through — the exact gap reported in
37
+ # github.com/Falconiere/toolu/issues/176. bash_write_targets (lib/detect.sh)
38
+ # extracts candidate write targets from the command; each is checked against
39
+ # the same protected-files.txt patterns as Edit/Write.
40
+ [[ "$tool_name" != "Edit" && "$tool_name" != "Write" && "$tool_name" != "MultiEdit" \
41
+ && "$tool_name" != "Bash" && "$tool_name" != "Shell" ]] && exit 0
42
+ command -v jq >/dev/null 2>&1 || exit 0
43
+
44
+ TOOLU_SETTINGS_DIR=$(toolu_settings_dir)
45
+ LIST_FILE="$TOOLU_SETTINGS_DIR/protected-files.txt"
46
+
47
+ [ -f "$LIST_FILE" ] || exit 0
48
+
49
+ declare -a candidates=()
50
+ if [[ "$tool_name" == "Bash" || "$tool_name" == "Shell" ]]; then
51
+ command_str=$(echo "$input" | jq -r '.tool_input.command // ""')
52
+ [ -z "$command_str" ] && exit 0
53
+ # Deduplicated: the same path can surface twice (once in the main command,
54
+ # once via a command substitution the extractor recurses into), and checking
55
+ # one path against the whole pattern list twice buys nothing.
56
+ while IFS= read -r target; do
57
+ [ -n "$target" ] || continue
58
+ case " ${candidates[*]-} " in
59
+ *" $target "*) continue ;;
60
+ esac
61
+ candidates+=("$target")
62
+ done < <(bash_write_targets "$command_str")
63
+ [ ${#candidates[@]} -eq 0 ] && exit 0
64
+ else
65
+ file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
66
+ [ -z "$file_path" ] && exit 0
67
+ candidates=("$file_path")
68
+ fi
69
+
70
+ # read_list is sourced from lib/detect.sh.
71
+
72
+ # glob_match <pattern> <path>
73
+ glob_match() {
74
+ local pattern="$1"
75
+ local path="$2"
76
+ # Enable extended globs for ** to match path segments.
77
+ shopt -s extglob globstar 2>/dev/null || true
78
+ # shellcheck disable=SC2053 # intentional glob match on RHS
79
+ [[ "$path" == $pattern ]]
80
+ }
81
+
82
+ list=$(read_list "$LIST_FILE")
83
+
84
+ # Normalize absolute paths (Edit/Write sends absolute; a Bash write target is
85
+ # whatever the command wrote literally, often already relative) into
86
+ # repo-relative so patterns like "hooks/lib/**" can match. Falls back to the
87
+ # input unchanged outside a git repo (test sandboxes etc).
88
+ for candidate in "${candidates[@]}"; do
89
+ rel_path=$(to_relative_path "$candidate")
90
+ matched=""
91
+ while IFS= read -r pattern; do
92
+ [ -z "$pattern" ] && continue
93
+ # Try as-is.
94
+ if glob_match "$pattern" "$rel_path"; then
95
+ matched="$pattern"
96
+ break
97
+ fi
98
+ # For patterns that do not already start with **/, also try anchored
99
+ # anywhere under the repo. This keeps "hooks/lib/**" matching even when
100
+ # the path arrives as e.g. "subtree/hooks/lib/x.sh".
101
+ if [[ "$pattern" != \*\*/* ]] && glob_match "**/$pattern" "$rel_path"; then
102
+ matched="$pattern"
103
+ break
104
+ fi
105
+ # Also check basename for patterns without a path separator.
106
+ if [[ "$pattern" != */* ]]; then
107
+ base=$(basename "$rel_path")
108
+ if glob_match "$pattern" "$base"; then
109
+ matched="$pattern"
110
+ break
111
+ fi
112
+ fi
113
+ done <<< "$list"
114
+
115
+ if [ -n "$matched" ]; then
116
+ # Resolve the delivery mode, then VALIDATE it. Falling through to the
117
+ # emit's `*)` arm would also block, but silently: a deny the user cannot
118
+ # explain is a deny they work around. Fail closed AND say why.
119
+ mode_note=""
120
+ mode=$(toolu_gate_mode protectedFiles) || mode=""
121
+ case " $TOOLU_GATE_MODES " in
122
+ *" $mode "*) ;;
123
+ *)
124
+ printf 'toolu-protected-files: could not resolve gates.protectedFiles.mode (got %s); blocking\n' \
125
+ "${mode:-<empty>}" >&2
126
+ mode_note=" [toolu could not resolve gates.protectedFiles.mode, so this failed closed]"
127
+ mode="block"
128
+ ;;
129
+ esac
130
+
131
+ if [[ "$tool_name" == "Bash" || "$tool_name" == "Shell" ]]; then
132
+ headline="This command would WRITE to $candidate, a protected path (matches \"$matched\")."
133
+ else
134
+ headline="Claude is trying to edit $candidate, a protected path (matches \"$matched\")."
135
+ fi
136
+
137
+ # Why THIS path is guarded, in the words that matter to the person
138
+ # deciding. A generic "it is protected" teaches nothing and gets waved
139
+ # through; naming the actual stake is the difference between a real
140
+ # decision and a reflex.
141
+ case "$rel_path" in
142
+ *.env.example|*.env.template|*.env.sample)
143
+ # Matched by the .env.* glob, but this one is meant to be committed.
144
+ # Calling a template a secrets file is wrong, and a prompt that
145
+ # overstates its case is one people learn to click through.
146
+ detail="This is an example/template env file. It is committed on purpose, so it should carry placeholders and never live values — it is guarded because a real credential pasted here is a credential published to the repo." ;;
147
+ .env|.env.*|*secrets*)
148
+ detail="This is a secrets file. Approving lets an agent read or rewrite live credentials, and anything it writes here can leak into logs, commits, or a diff you push." ;;
149
+ .git/*|*/.git/*)
150
+ detail="This is git's internal state. Approving lets an agent rewrite refs, hooks, or config — including hooks that run on your machine at every commit." ;;
151
+ *hooks/*|*skills/*)
152
+ detail="This is toolu's own enforcement code — the hooks that run every other gate. Approving lets an agent edit the thing that is supposed to be watching it, which is how a guardrail gets quietly switched off." ;;
153
+ *)
154
+ detail="This path is listed in settings/protected-files.txt because edits to it are hard to notice and expensive to get wrong." ;;
155
+ esac
156
+
157
+ case "$mode" in
158
+ ask)
159
+ reason=$(toolu_gate_guardrail_warning "$headline" "$detail") || reason=""
160
+ # A prompt with no text is worse than a blunt one — the user is being
161
+ # asked to approve a security override and must see what for.
162
+ [ -n "$reason" ] || reason="$headline $detail"
163
+ ;;
164
+ advise)
165
+ reason="Protected path $candidate (matches \"$matched\"). $detail The write was NOT stopped — gates.protectedFiles.mode is 'advise'."
166
+ ;;
167
+ *)
168
+ reason="$headline $detail Blocked by gates.protectedFiles.mode='block' (see plugins/toolu/hooks/docs/gates.md)."
169
+ ;;
170
+ esac
171
+
172
+ toolu_gate_emit "$mode" "${reason}${mode_note}"
173
+ exit 0
174
+ fi
175
+ done
176
+
177
+ exit 0