@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,201 @@
1
+ #!/usr/bin/env bash
2
+ # Multi-slot writer for .claude/tmp/quality-gate-status.json.
3
+ #
4
+ # The gate file used to be single-slot (last writer wins): a TS failure
5
+ # followed by a Rust failure overwrote the TS record, and clearing the Rust
6
+ # file then re-opened the gate with the TS violation still live. The same
7
+ # clobber happened between two files of the SAME language.
8
+ #
9
+ # Now every failing file owns an entry under `entries` (keyed by file path).
10
+ # Top-level fields mirror the most recent failure and aggregate all violations,
11
+ # so readers (pre-tool gate, statusline, session-start, user-prompt-submit)
12
+ # keep their existing .status / .reason / .violations contract untouched.
13
+ #
14
+ # A legacy or foreign single-slot failing record (e.g. gate-status-hook's
15
+ # command failure) is preserved by seeding it into `entries` under its `file`,
16
+ # or "__global__" when it has none. Both jq programs below start with the same
17
+ # seed step — keep them in sync.
18
+ #
19
+ # CONCURRENCY: single-writer by assumption. Both functions read-merge-write via
20
+ # `mktemp` + `mv -f`, which has a TOCTOU window — two writers that read the same
21
+ # `existing` blob would each rewrite it and the last `mv` would drop the other's
22
+ # entry. This is safe today because PostToolUse hooks fire serially per tool
23
+ # call (one writer at a time). If a parallel-edit flow is ever added, guard both
24
+ # functions — AND the single-slot fallback write below, AND the separate
25
+ # single-slot passing write in post-tools/modules/gate-status.sh (which writes
26
+ # the same gate file) — with `flock` against a `${gate_file}.lock` sentinel; all
27
+ # of those paths race identically.
28
+ #
29
+ # Public API:
30
+ # gate_record_failure GATE_FILE FILE SOURCE REASON VIOLATIONS
31
+ # gate_clear_file GATE_FILE FILE SOURCE
32
+ #
33
+ # Telemetry (gate_fail / gate_clear): this file has no repo root of its own to
34
+ # hand telemetry_append, so it derives one from GATE_FILE's own path — every
35
+ # caller passes a path shaped <root>/.claude/tmp/quality-gate-status.json, so
36
+ # three dirname hops off GATE_FILE land back on <root>. telemetry.sh is
37
+ # sourced LAZILY (only on first actual emit, not at file load) and every emit
38
+ # is guarded by `command -v telemetry_append`, so an absent or older
39
+ # telemetry.sh is never a hard dependency for this file's callers.
40
+
41
+ # _gate_file_root GATE_FILE -> print <root> (three dirname hops off GATE_FILE).
42
+ _gate_file_root() {
43
+ dirname "$(dirname "$(dirname "$1")")"
44
+ }
45
+
46
+ # _gate_file_ensure_telemetry -> 0 iff telemetry_append is callable (already
47
+ # loaded, or successfully lazy-sourced from telemetry.sh next to this file).
48
+ _gate_file_ensure_telemetry() {
49
+ command -v telemetry_append >/dev/null 2>&1 && return 0
50
+ local lib_dir="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
51
+ [ -f "$lib_dir/telemetry.sh" ] || return 1
52
+ # shellcheck source=./telemetry.sh
53
+ . "$lib_dir/telemetry.sh"
54
+ command -v telemetry_append >/dev/null 2>&1
55
+ }
56
+
57
+ # gate_record_failure GATE_FILE FILE SOURCE REASON VIOLATIONS
58
+ # Adds/replaces this file's entry and marks the gate failing. Writes via a
59
+ # temp file so a jq failure can never truncate the gate to an unreadable
60
+ # (and therefore silently passing) state; on any error it falls back to the
61
+ # legacy single-slot write rather than dropping the failure.
62
+ gate_record_failure() {
63
+ local gate_file="$1" file="$2" source="$3" reason="$4" violations="$5"
64
+ local existing='{}' now tmp
65
+ now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
66
+ if [ -f "$gate_file" ]; then
67
+ existing=$(cat "$gate_file" 2>/dev/null) || existing='{}'
68
+ jq -e . <<< "$existing" >/dev/null 2>&1 || existing='{}'
69
+ fi
70
+ tmp=$(mktemp "${gate_file}.XXXXXX" 2>/dev/null) || tmp=""
71
+ if [ -n "$tmp" ] && jq \
72
+ --arg file "$file" --arg source "$source" --arg reason "$reason" \
73
+ --arg violations "$violations" --arg now "$now" '
74
+ (if (.entries? | type) == "object" then .entries
75
+ elif (.status // "") == "failing" then
76
+ { (.file // "__global__"): {
77
+ source: (.source // ""), reason: (.reason // ""),
78
+ violations: (.violations // ""), updatedAt: (.updatedAt // $now) } }
79
+ else {} end) as $prev
80
+ | ($prev + { ($file): {source: $source, reason: $reason,
81
+ violations: $violations, updatedAt: $now} }) as $entries
82
+ # `violations` aggregates every open entry, intentionally: readers (statusline,
83
+ # session-start, user-prompt-submit) must surface ALL unresolved violations,
84
+ # not just the latest. Growth is bounded by the count of distinct failing
85
+ # files in a session (each clears on the next passing edit), so it stays
86
+ # small in practice. If a long session ever shows this string bloating
87
+ # hot-path reads, add a per-entry cap here rather than dropping entries.
88
+ # sort_by updatedAt then key: the timestamp is second-resolution (BSD `date`
89
+ # on macOS has no %N sub-second granularity), so two failures in the SAME
90
+ # second tiebreak by filename — the top-level .file/.reason mirror may then
91
+ # not be the truly-latest. Cosmetic and self-correcting on the next edit;
92
+ # readers see every failure via `entries` regardless. Accepted over a
93
+ # non-portable %N or a cross-call monotonic counter.
94
+ | { status: "failing", reason: $reason, source: $source, file: $file,
95
+ violations: ([$entries | to_entries | sort_by(.value.updatedAt // "", .key)[]
96
+ | (.value.violations // "")] | join("")),
97
+ entries: $entries, updatedAt: $now }
98
+ ' <<< "$existing" > "$tmp" 2>/dev/null; then
99
+ mv -f "$tmp" "$gate_file"
100
+ else
101
+ [ -n "$tmp" ] && rm -f "$tmp"
102
+ # Fallback (mktemp or the merge jq failed): write a single-slot record so
103
+ # THIS failure is never lost. That collapses any pre-existing multi-slot
104
+ # `entries` to one record — emit a stderr breadcrumb naming the dropped
105
+ # count so a vanished-state debugging session can see it happened, rather
106
+ # than the entries disappearing silently. The count uses the SAME seed step
107
+ # both merge programs do (a legacy single-slot failing record becomes one
108
+ # `entries` slot under its file/__global__), so a clobbered legacy global
109
+ # record is counted too — not reported as 0.
110
+ local _dropped
111
+ _dropped=$(jq -r --arg f "$file" '
112
+ (if (.entries? | type) == "object" then .entries
113
+ elif (.status // "") == "failing" then { (.file // "__global__"): {} }
114
+ else {} end)
115
+ | keys | map(select(. != $f)) | length' <<< "$existing" 2>/dev/null || echo "")
116
+ if [ -n "$_dropped" ] && [ "$_dropped" -gt 0 ] 2>/dev/null; then
117
+ printf 'gate-file: primary write failed at %s; single-slot fallback dropped %s other entry(ies)\n' \
118
+ "$gate_file" "$_dropped" >&2
119
+ # stderr is easily lost in a hook-driven pipeline; also append a durable
120
+ # breadcrumb beside the gate file so a vanished-state investigation has
121
+ # something to read. Best-effort: never let a logging failure abort the write.
122
+ printf '%s primary write failed; single-slot fallback dropped %s entry(ies)\n' \
123
+ "$now" "$_dropped" >> "${gate_file}.dropped.log" 2>/dev/null || true
124
+ fi
125
+ jq -n --arg reason "$reason" --arg source "$source" --arg file "$file" \
126
+ --arg violations "$violations" --arg updatedAt "$now" \
127
+ '{status: "failing", reason: $reason, source: $source, file: $file,
128
+ violations: $violations, updatedAt: $updatedAt}' > "$gate_file" 2>/dev/null || true
129
+ fi
130
+
131
+ # gate_fail telemetry: this function always ends in an attempted write (primary
132
+ # or fallback), so it always represents a failure being recorded — one event
133
+ # per call, best-effort (never affects the write above). Semantics: gate_fail
134
+ # fires on every failing EDIT, including a re-record of the same violation on
135
+ # the same file — it is not deduped per distinct failure. gate_clear (below)
136
+ # is the complementary half: it fires only on a true failing->passing
137
+ # transition, never on a re-clear of an already-passing gate.
138
+ if _gate_file_ensure_telemetry; then
139
+ telemetry_append "$(_gate_file_root "$gate_file")" "gate_fail" \
140
+ "$(jq -cn --arg file "$file" --arg source "$source" '{file: $file, source: $source}')"
141
+ fi
142
+ }
143
+
144
+ # gate_clear_file GATE_FILE FILE SOURCE
145
+ # Removes this file's entry IF this source owns it. Other entries are promoted
146
+ # back to the top level (gate stays failing); "passing" is written only when
147
+ # no entry remains. A failing record owned by another source/file is left
148
+ # untouched, matching the old single-slot clear semantics.
149
+ gate_clear_file() {
150
+ local gate_file="$1" file="$2" source="$3"
151
+ local existing now owns tmp
152
+ [ -f "$gate_file" ] || return 0
153
+ existing=$(cat "$gate_file" 2>/dev/null) || return 0
154
+ # A malformed gate file can't be parsed, so a clear silently no-ops and the
155
+ # gate stays stuck failing until the next gate_record_failure rewrites it.
156
+ # Emit a breadcrumb so that stuck state is debuggable, not invisible.
157
+ if ! jq -e . <<< "$existing" >/dev/null 2>&1; then
158
+ printf 'gate-file: malformed JSON at %s; ignoring clear (gate stays failing until next write)\n' "$gate_file" >&2
159
+ return 0
160
+ fi
161
+ [ "$(jq -r '.status // ""' <<< "$existing" 2>/dev/null)" = "failing" ] || return 0
162
+ owns=$(jq -r --arg f "$file" --arg s "$source" '
163
+ if (.entries? | type) == "object"
164
+ then (((.entries[$f]? // {}) | .source // "") == $s)
165
+ else ((.source // "") == $s and (.file // "") == $f)
166
+ end' <<< "$existing" 2>/dev/null)
167
+ [ "$owns" = "true" ] || return 0
168
+ now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
169
+ tmp=$(mktemp "${gate_file}.XXXXXX" 2>/dev/null) || return 0
170
+ if jq --arg file "$file" --arg source "$source" --arg now "$now" '
171
+ ((if (.entries? | type) == "object" then .entries
172
+ elif (.status // "") == "failing" then
173
+ { (.file // "__global__"): {
174
+ source: (.source // ""), reason: (.reason // ""),
175
+ violations: (.violations // ""), updatedAt: (.updatedAt // $now) } }
176
+ else {} end) | del(.[$file])) as $left
177
+ | if ($left | length) == 0
178
+ then { status: "passing", source: $source, updatedAt: $now }
179
+ else (($left | to_entries | sort_by(.value.updatedAt // "", .key)) as $sorted
180
+ | ($sorted | last) as $latest
181
+ | { status: "failing",
182
+ reason: ($latest.value.reason // "Quality gate failing"),
183
+ source: ($latest.value.source // ""),
184
+ file: $latest.key,
185
+ violations: ([$sorted[] | (.value.violations // "")] | join("")),
186
+ entries: $left, updatedAt: $now })
187
+ end
188
+ ' <<< "$existing" > "$tmp" 2>/dev/null; then
189
+ mv -f "$tmp" "$gate_file"
190
+ # gate_clear telemetry: ONLY here, after the write actually lands — every
191
+ # early return above (no file / unreadable / malformed / not failing /
192
+ # other-source entry) emits nothing, and so does a mktemp or jq failure
193
+ # below this point (the `else` branch), since no transition happened.
194
+ if _gate_file_ensure_telemetry; then
195
+ telemetry_append "$(_gate_file_root "$gate_file")" "gate_clear" \
196
+ "$(jq -cn --arg file "$file" --arg source "$source" '{file: $file, source: $source}')"
197
+ fi
198
+ else
199
+ rm -f "$tmp"
200
+ fi
201
+ }
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env bash
2
+ # Gate enforcement modes — how a failed gate check reaches the user.
3
+ #
4
+ # Every toolu gate answers one question ("is this push reviewed?", "is the
5
+ # quality gate green?") and then has to decide how forcefully to say no. That
6
+ # second decision is this file's job, so a gate module owns its check and its
7
+ # reason text while the delivery is uniform and user-configurable.
8
+ #
9
+ # Modes:
10
+ # block permissionDecision "deny" — the tool call does not happen
11
+ # ask permissionDecision "ask" — the user is prompted, and decides
12
+ # advise additionalContext — the agent is told, nothing is stopped
13
+ # off nothing at all
14
+ #
15
+ # Two CLASSES of gate, and the difference only shows up when `ask` cannot be
16
+ # delivered:
17
+ #
18
+ # judgement gates (pushReview, qualityGate, commitGate, planLedger,
19
+ # docsSync, agentTier) — soft calls about process. `ask` on a host that
20
+ # cannot prompt degrades to `advise`: say the thing, don't stop the work.
21
+ #
22
+ # SECURITY GUARDRAILS ($TOOLU_GATE_GUARDRAILS: protectedFiles, mcpBlocker,
23
+ # bashCommands) — the ones standing between an agent and your secrets,
24
+ # your git internals, the hook framework itself, and arbitrary code
25
+ # execution. These now ASK by default instead of denying outright, so a
26
+ # human decides in the moment. That only holds if the human can actually
27
+ # be reached: on a host with no prompt, `ask` degrades to `block`, NOT to
28
+ # `advise`. Degrading a guardrail to advise would silently turn it off
29
+ # exactly where nobody is watching.
30
+ #
31
+ # Resolution precedence, first hit wins:
32
+ # 1. .gates.<name>.mode
33
+ # 2. the legacy top-level key (.docsSync.mode / .agentTier.mode only)
34
+ # 3. the .gates.preset table below
35
+ # 4. the built-in default preset (balanced)
36
+ #
37
+ # A value that is present but not a known mode warns once on stderr and falls
38
+ # through to the next layer — the same warn-and-fall-back discipline
39
+ # toolu_model uses, so a typo mis-delivers nothing.
40
+ #
41
+ # Public API:
42
+ # toolu_gate_preset print the resolved preset name
43
+ # toolu_gate_mode NAME print block|ask|advise|off for gate NAME
44
+ # toolu_gate_emit MODE REASON print the PreToolUse JSON for MODE
45
+ # toolu_gate_guardrail_warning HEADLINE DETAIL
46
+ # compose a guardrail's `ask` prompt text
47
+
48
+ _TOOLU_GATE_MODE_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
49
+ # shellcheck source=config.sh
50
+ . "$_TOOLU_GATE_MODE_LIB_DIR/config.sh"
51
+
52
+ TOOLU_GATE_MODES="block ask advise off"
53
+ TOOLU_GATE_PRESETS="strict balanced relaxed"
54
+ TOOLU_GATE_NAMES="pushReview qualityGate commitGate bashCommands planLedger docsSync agentTier protectedFiles mcpBlocker"
55
+ TOOLU_GATE_DEFAULT_PRESET="balanced"
56
+
57
+ # The security guardrails. Two things are special about this list:
58
+ # 1. `ask` degrades to `block` (not `advise`) where the host cannot prompt.
59
+ # 2. They ask at every preset except `strict`, which still denies outright.
60
+ # `off` remains available per-gate for anyone who wants it, deliberately and
61
+ # in writing, in their own config.
62
+ TOOLU_GATE_GUARDRAILS="protectedFiles mcpBlocker bashCommands"
63
+
64
+ # Gates whose pre-`gates.*` config key is still honored, as "<gate>:<path>".
65
+ # Only these two ever had a documented top-level mode key; a gate absent here
66
+ # has no legacy layer to consult.
67
+ _TOOLU_GATE_LEGACY_PATHS="docsSync:docsSync.mode agentTier:agentTier.mode"
68
+
69
+ _toolu_gate_warn() {
70
+ printf 'toolu-gate: %s\n' "$1" >&2
71
+ }
72
+
73
+ # _toolu_gate_preset_mode PRESET NAME -> mode for NAME under PRESET.
74
+ # The table is the whole policy; balanced is what ships.
75
+ _toolu_gate_preset_mode() {
76
+ local preset="$1" name="$2"
77
+ case "$preset" in
78
+ strict)
79
+ printf 'block'
80
+ ;;
81
+ balanced)
82
+ case "$name" in
83
+ qualityGate) printf 'block' ;;
84
+ protectedFiles|mcpBlocker|bashCommands) printf 'ask' ;;
85
+ *) printf 'advise' ;;
86
+ esac
87
+ ;;
88
+ relaxed)
89
+ case "$name" in
90
+ # `relaxed` relaxes judgement calls. A guardrail still asks: relaxed
91
+ # means "stop lecturing me", not "write my .env without telling me".
92
+ protectedFiles|mcpBlocker|bashCommands) printf 'ask' ;;
93
+ pushReview|qualityGate) printf 'advise' ;;
94
+ *) printf 'off' ;;
95
+ esac
96
+ ;;
97
+ *)
98
+ return 1
99
+ ;;
100
+ esac
101
+ }
102
+
103
+ # toolu_gate_preset -> the configured preset, or the built-in default.
104
+ toolu_gate_preset() {
105
+ # shellcheck disable=SC2086 # intentional word-split of the allowed-value list
106
+ toolu_string gates.preset "$TOOLU_GATE_DEFAULT_PRESET" $TOOLU_GATE_PRESETS
107
+ }
108
+
109
+ # _toolu_gate_read_mode PATH -> print the mode at PATH, or return 1.
110
+ # Returns 1 both when the key is absent and when it held something that is not
111
+ # a mode (toolu_string has already warned in the latter case), which is exactly
112
+ # the "fall through to the next layer" behavior both cases want.
113
+ _toolu_gate_read_mode() {
114
+ local value
115
+ # shellcheck disable=SC2086 # intentional word-split of the allowed-value list
116
+ value=$(toolu_string "$1" "__unset__" $TOOLU_GATE_MODES)
117
+ [ "$value" = "__unset__" ] && return 1
118
+ printf '%s' "$value"
119
+ }
120
+
121
+ # _toolu_gate_legacy_path NAME -> print NAME's legacy config path, or return 1.
122
+ _toolu_gate_legacy_path() {
123
+ local name="$1" entry
124
+ for entry in $_TOOLU_GATE_LEGACY_PATHS; do
125
+ if [ "${entry%%:*}" = "$name" ]; then
126
+ printf '%s' "${entry#*:}"
127
+ return 0
128
+ fi
129
+ done
130
+ return 1
131
+ }
132
+
133
+ # toolu_gate_mode NAME -> block|ask|advise|off.
134
+ #
135
+ # An unknown gate name is a caller typo, not a config problem: warn and print
136
+ # `block`, so a misspelled gate keeps enforcing rather than silently opening.
137
+ toolu_gate_mode() {
138
+ local name="$1" mode="" legacy_path preset
139
+ case " $TOOLU_GATE_NAMES " in
140
+ *" $name "*) ;;
141
+ *)
142
+ _toolu_gate_warn "unknown gate '$name' (known: $TOOLU_GATE_NAMES); enforcing block"
143
+ printf 'block'
144
+ return 1
145
+ ;;
146
+ esac
147
+
148
+ mode=$(_toolu_gate_read_mode "gates.${name}.mode") || mode=""
149
+
150
+ if [ -z "$mode" ] && legacy_path=$(_toolu_gate_legacy_path "$name"); then
151
+ mode=$(_toolu_gate_read_mode "$legacy_path") || mode=""
152
+ fi
153
+
154
+ if [ -z "$mode" ]; then
155
+ preset=$(toolu_gate_preset)
156
+ mode=$(_toolu_gate_preset_mode "$preset" "$name") || mode=""
157
+ fi
158
+
159
+ # Both fallbacks exhausted (only reachable if the preset table and the name
160
+ # list ever drift apart): enforce rather than open.
161
+ [ -n "$mode" ] || mode="block"
162
+
163
+ # `ask` needs a host that can prompt. Where none can, the gate still speaks —
164
+ # but WHAT it degrades to depends on the gate's class:
165
+ #
166
+ # judgement gate -> advise. Say it, don't stop the work.
167
+ # guardrail -> block. The whole point of asking is that a human
168
+ # decides; with no human reachable the safe answer is
169
+ # no. Degrading these to advise would hand an agent
170
+ # silent write access to .env on exactly the hosts
171
+ # where nobody is watching.
172
+ if [ "$mode" = "ask" ] && ! toolu_supports_ask; then
173
+ case " $TOOLU_GATE_GUARDRAILS " in
174
+ *" $name "*) mode="block" ;;
175
+ *) mode="advise" ;;
176
+ esac
177
+ fi
178
+
179
+ printf '%s' "$mode"
180
+ }
181
+
182
+ # toolu_gate_guardrail_warning HEADLINE DETAIL
183
+ # Compose the prompt text for a SECURITY GUARDRAIL's `ask`.
184
+ #
185
+ # Loud on purpose. Answering yes here overrides a protection that exists
186
+ # precisely so an agent cannot do this quietly, and a prompt that reads like an
187
+ # ordinary "allow this command?" trains people to wave it through. The banner,
188
+ # the WHY, and the "this call only" line are the three things a person needs to
189
+ # make a real decision in the two seconds they will actually spend on it.
190
+ toolu_gate_guardrail_warning() {
191
+ local headline="$1" detail="$2"
192
+ printf '%s' "\
193
+ ############################################################
194
+ ## ⚠️ SECURITY GUARDRAIL — OVERRIDE REQUESTED ⚠️ ##
195
+ ############################################################
196
+
197
+ $headline
198
+
199
+ WHY THIS IS GUARDED
200
+ $detail
201
+
202
+ Approving covers THIS ONE CALL. Nothing is remembered and the next attempt
203
+ asks again. If you did not just ask for this, the answer is no."
204
+ }
205
+
206
+ # toolu_gate_emit MODE REASON
207
+ # Print the PreToolUse hook JSON for MODE carrying REASON. `off` prints
208
+ # nothing. An unknown MODE is treated as `block`: a delivery bug must not
209
+ # become a silent allow.
210
+ toolu_gate_emit() {
211
+ local mode="$1" reason="$2"
212
+ case "$mode" in
213
+ off)
214
+ return 0
215
+ ;;
216
+ advise)
217
+ jq -n --arg reason "$reason" '{
218
+ hookSpecificOutput: {
219
+ hookEventName: "PreToolUse",
220
+ additionalContext: $reason
221
+ }
222
+ }'
223
+ ;;
224
+ ask)
225
+ jq -n --arg reason "$reason" '{
226
+ hookSpecificOutput: {
227
+ hookEventName: "PreToolUse",
228
+ permissionDecision: "ask",
229
+ permissionDecisionReason: $reason
230
+ }
231
+ }'
232
+ ;;
233
+ *)
234
+ [ "$mode" = "block" ] || _toolu_gate_warn "unknown mode '$mode'; emitting block"
235
+ jq -n --arg reason "$reason" '{
236
+ hookSpecificOutput: {
237
+ hookEventName: "PreToolUse",
238
+ permissionDecision: "deny",
239
+ permissionDecisionReason: $reason
240
+ }
241
+ }'
242
+ ;;
243
+ esac
244
+ }
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env bash
2
+ # Host adapter shared by toolu's hook runtime.
3
+ #
4
+ # Public API:
5
+ # toolu_host claude | codex
6
+ # toolu_config_root host-native writable config/data root
7
+ # toolu_project_root explicit/project git root, if any
8
+ # toolu_project_dirname .claude | .codex (overrideable)
9
+ # toolu_project_config project toolu.config.json path
10
+ # toolu_project_state_root <repo>/<host-dir>/tmp
11
+ # toolu_project_state_dir NAME state root child
12
+ # toolu_plugin_root / _data plugin paths supplied by the host
13
+ # toolu_invocation NS NAME /ns:name | $ns:name
14
+ # toolu_plugin_install_command SPEC host-native exact install command
15
+ # toolu_codex_plugin_snapshot_path canonical snapshot path
16
+ # toolu_snapshot_codex_plugins refresh snapshot at SessionStart
17
+ # toolu_codex_plugin_installed SPEC tri-state lookup (0 hit/miss, 2 unknown)
18
+
19
+ _toolu_host_warn() {
20
+ printf 'toolu-host: %s\n' "$1" >&2
21
+ }
22
+
23
+ toolu_host() {
24
+ case "${TOOLU_HOST_OVERRIDE:-}" in
25
+ claude|codex) printf '%s\n' "$TOOLU_HOST_OVERRIDE"; return 0 ;;
26
+ '') ;;
27
+ *) _toolu_host_warn "invalid TOOLU_HOST_OVERRIDE '${TOOLU_HOST_OVERRIDE}' (using environment detection)" ;;
28
+ esac
29
+
30
+ # Codex sets PLUGIN_ROOT and also exports CLAUDE_PLUGIN_ROOT for compatibility.
31
+ # The Codex-native signal must therefore be checked first.
32
+ if [ -n "${PLUGIN_ROOT:-}" ]; then
33
+ printf 'codex\n'
34
+ else
35
+ printf 'claude\n'
36
+ fi
37
+ }
38
+
39
+ toolu_config_root() {
40
+ if [ -n "${TOOLU_CONFIG_DIR:-}" ]; then
41
+ printf '%s\n' "$TOOLU_CONFIG_DIR"
42
+ elif [ "$(toolu_host)" = codex ]; then
43
+ printf '%s\n' "${CODEX_HOME:-$HOME/.codex}"
44
+ elif [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
45
+ printf '%s\n' "$CLAUDE_CONFIG_DIR"
46
+ else
47
+ printf '%s/.claude\n' "$HOME"
48
+ fi
49
+ }
50
+
51
+ toolu_project_root() {
52
+ local root="${TOOLU_PROJECT_DIR:-}"
53
+ if [ -z "$root" ] && [ "$(toolu_host)" = claude ]; then
54
+ root="${CLAUDE_PROJECT_DIR:-}"
55
+ fi
56
+ [ -n "$root" ] || root=$(git rev-parse --show-toplevel 2>/dev/null || true)
57
+ [ -n "$root" ] && printf '%s\n' "$root"
58
+ }
59
+
60
+ toolu_project_dirname() {
61
+ if [ -n "${TOOLU_PROJECT_CONFIG_DIRNAME:-}" ]; then
62
+ printf '%s\n' "$TOOLU_PROJECT_CONFIG_DIRNAME"
63
+ elif [ "$(toolu_host)" = codex ]; then
64
+ printf '.codex\n'
65
+ else
66
+ printf '.claude\n'
67
+ fi
68
+ }
69
+
70
+ toolu_project_config() {
71
+ local root
72
+ root=$(toolu_project_root)
73
+ [ -n "$root" ] || return 0
74
+ printf '%s/%s/toolu.config.json\n' "$root" "$(toolu_project_dirname)"
75
+ }
76
+
77
+ toolu_project_state_root() {
78
+ local root="${1:-}"
79
+ [ -n "$root" ] || root=$(toolu_project_root)
80
+ [ -n "$root" ] || return 0
81
+ printf '%s/%s/tmp\n' "$root" "$(toolu_project_dirname)"
82
+ }
83
+
84
+ toolu_project_state_dir() {
85
+ local name="${1:-}" root="${2:-}" base
86
+ [ -n "$name" ] || return 1
87
+ base=$(toolu_project_state_root "$root")
88
+ [ -n "$base" ] || return 0
89
+ printf '%s/%s\n' "$base" "$name"
90
+ }
91
+
92
+ toolu_plugin_root() {
93
+ if [ "$(toolu_host)" = codex ] && [ -n "${PLUGIN_ROOT:-}" ]; then
94
+ printf '%s\n' "$PLUGIN_ROOT"
95
+ elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
96
+ printf '%s\n' "$CLAUDE_PLUGIN_ROOT"
97
+ fi
98
+ }
99
+
100
+ toolu_plugin_data() {
101
+ if [ "$(toolu_host)" = codex ] && [ -n "${PLUGIN_DATA:-}" ]; then
102
+ printf '%s\n' "$PLUGIN_DATA"
103
+ elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then
104
+ printf '%s\n' "$CLAUDE_PLUGIN_DATA"
105
+ fi
106
+ }
107
+
108
+ toolu_invocation() {
109
+ local namespace="${1:-}" name="${2:-}"
110
+ [ -n "$namespace" ] && [ -n "$name" ] || return 1
111
+ if [ "$(toolu_host)" = codex ]; then
112
+ printf '$%s:%s\n' "$namespace" "$name"
113
+ else
114
+ printf '/%s:%s\n' "$namespace" "$name"
115
+ fi
116
+ }
117
+
118
+ toolu_plugin_install_command() {
119
+ local spec="${1:-}"
120
+ [ -n "$spec" ] || return 1
121
+ if [ "$(toolu_host)" = codex ]; then
122
+ printf 'codex plugin add %s\n' "$spec"
123
+ else
124
+ printf '/plugin install %s\n' "$spec"
125
+ fi
126
+ }
127
+
128
+ toolu_codex_plugin_snapshot_path() {
129
+ printf '%s\n' "${TOOLU_CODEX_PLUGIN_SNAPSHOT:-$(toolu_config_root)/toolu/codex-plugins.json}"
130
+ }
131
+
132
+ _toolu_write_codex_snapshot() {
133
+ local path="$1" payload="$2" dir tmp
134
+ dir=$(dirname "$path")
135
+ mkdir -p "$dir" 2>/dev/null || return 0
136
+ tmp="${path}.tmp.$$"
137
+ if printf '%s\n' "$payload" > "$tmp" 2>/dev/null; then
138
+ mv "$tmp" "$path" 2>/dev/null || rm -f "$tmp"
139
+ else
140
+ rm -f "$tmp"
141
+ fi
142
+ }
143
+
144
+ # Snapshot once on Codex SessionStart. Failures are represented explicitly so
145
+ # hot-path readers never invoke the CLI and never mistake an old cache for a
146
+ # definitive installed/absent result.
147
+ toolu_snapshot_codex_plugins() {
148
+ [ "$(toolu_host)" = codex ] || return 0
149
+ local path raw canonical
150
+ path=$(toolu_codex_plugin_snapshot_path)
151
+ canonical='{"version":1,"status":"indeterminate","plugins":[]}'
152
+
153
+ if command -v codex >/dev/null 2>&1 && raw=$(codex plugin list --json 2>/dev/null); then
154
+ if canonical=$(jq -ce '
155
+ if (.installed | type) != "array" then error("installed must be an array") else
156
+ {
157
+ version: 1,
158
+ status: "ready",
159
+ plugins: ([.installed[]
160
+ | select((has("installed") | not) or (.installed == true))
161
+ | select((has("enabled") | not) or (.enabled == true))
162
+ | (.pluginId // (if (.name | type) == "string" and (.marketplaceName | type) == "string"
163
+ then (.name + "@" + .marketplaceName) else empty end))
164
+ | select(type == "string" and length > 0)] | unique | sort)
165
+ }
166
+ end
167
+ ' <<<"$raw" 2>/dev/null); then
168
+ :
169
+ else
170
+ canonical='{"version":1,"status":"indeterminate","plugins":[]}'
171
+ fi
172
+ fi
173
+ _toolu_write_codex_snapshot "$path" "$canonical"
174
+ return 0
175
+ }
176
+
177
+ # Return contract matches detect_plugin_installed:
178
+ # 0 + spec => installed, 0 + empty => definitively absent, 2 => unknown.
179
+ toolu_codex_plugin_installed() {
180
+ local spec="${1:-}" path status
181
+ [ -n "$spec" ] || return 0
182
+ command -v jq >/dev/null 2>&1 || return 2
183
+ path=$(toolu_codex_plugin_snapshot_path)
184
+ [ -f "$path" ] || return 2
185
+ jq -e '.version == 1 and (.status | type == "string") and (.plugins | type == "array")' \
186
+ "$path" >/dev/null 2>&1 || return 2
187
+ status=$(jq -r '.status' "$path" 2>/dev/null) || return 2
188
+ [ "$status" = ready ] || return 2
189
+ if jq -e --arg spec "$spec" 'any(.plugins[]; . == $spec)' "$path" >/dev/null 2>&1; then
190
+ printf '%s\n' "$spec"
191
+ fi
192
+ return 0
193
+ }
194
+
195
+ # toolu_supports_ask -> 0 iff this host can prompt the user for a PreToolUse
196
+ # decision (`permissionDecision: "ask"`).
197
+ #
198
+ # Claude Code prompts; Codex's hook contract has no ask, so a gate that asked
199
+ # there would emit a decision the host drops on the floor — silently disabling
200
+ # the gate. Callers (toolu_gate_mode) degrade `ask` to `advise` instead, which
201
+ # still reaches the agent.
202
+ toolu_supports_ask() {
203
+ [ "$(toolu_host)" = codex ] && return 1
204
+ return 0
205
+ }