@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,116 @@
1
+ #!/usr/bin/env bash
2
+ # Push-review waivers — "yes, push anyway", remembered for exactly that diff.
3
+ #
4
+ # A PreToolUse hook never learns how the user answered its `ask`. PostToolUse
5
+ # does, implicitly: it runs only when the tool actually ran. So the two halves
6
+ # talk through a file.
7
+ #
8
+ # 1. push-review.sh asks, and records a PENDING waiver naming the diff SHA
9
+ # it asked about.
10
+ # 2. post-tools/modules/push-waiver.sh sees the push happened, and promotes
11
+ # the pending marker to a real waiver — but only when the push SUCCEEDED
12
+ # and the SHA still matches.
13
+ # 3. push-review.sh finds the waiver on the next push of that same diff and
14
+ # stays quiet. A new commit changes the SHA, so the waiver stops applying
15
+ # and the gate asks again.
16
+ #
17
+ # A refused ask simply leaves the pending marker behind; nothing promotes it,
18
+ # and the state sweeper reclaims it. Both writes are atomic (tmp + mv) so a
19
+ # half-written waiver can never read as a valid one.
20
+ #
21
+ # Public API:
22
+ # push_waiver_dir ROOT
23
+ # push_waiver_path ROOT SLUG
24
+ # push_waiver_pending_path ROOT SLUG
25
+ # push_waiver_matches ROOT SLUG SHA
26
+ # push_waiver_pend ROOT SLUG SHA BASE REASON_CODE
27
+ # push_waiver_promote ROOT SLUG SHA
28
+
29
+ _TOOLU_PUSH_WAIVER_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
30
+ # shellcheck source=host.sh
31
+ . "$_TOOLU_PUSH_WAIVER_LIB_DIR/host.sh"
32
+
33
+ PUSH_WAIVER_VERSION=1
34
+
35
+ # push_waiver_dir ROOT -> the push-review state directory for ROOT.
36
+ # $STATE_DIR overrides, matching push-review.sh and the toolu-review state
37
+ # writer, so tests and worktree flows point all three at the same place.
38
+ push_waiver_dir() {
39
+ local root="${1:-}"
40
+ printf '%s' "${STATE_DIR:-$(toolu_project_state_dir push-review "$root")}"
41
+ }
42
+
43
+ push_waiver_path() {
44
+ printf '%s/%s.waiver.json' "$(push_waiver_dir "$1")" "$2"
45
+ }
46
+
47
+ push_waiver_pending_path() {
48
+ printf '%s/%s.pending-waiver.json' "$(push_waiver_dir "$1")" "$2"
49
+ }
50
+
51
+ # _push_waiver_write FILE JSON -> atomic write, 0 on success.
52
+ # A failed mktemp or write leaves no file at all rather than a partial one;
53
+ # the caller decides whether that is fatal (it never is — a missing waiver
54
+ # just means the gate asks again).
55
+ _push_waiver_write() {
56
+ local file="$1" json="$2" tmp
57
+ mkdir -p "$(dirname "$file")" 2>/dev/null || return 1
58
+ tmp=$(mktemp "${file}.XXXXXX" 2>/dev/null) || return 1
59
+ if printf '%s\n' "$json" > "$tmp" 2>/dev/null; then
60
+ mv -f "$tmp" "$file" 2>/dev/null && return 0
61
+ fi
62
+ rm -f "$tmp" 2>/dev/null
63
+ return 1
64
+ }
65
+
66
+ # _push_waiver_sha FILE -> print the diff_sha of a v1 waiver-shaped FILE.
67
+ # Prints nothing (and returns 1) for a missing file, unreadable JSON, or a
68
+ # version this code does not understand — all of which must read as "no
69
+ # waiver", never as a match.
70
+ _push_waiver_sha() {
71
+ local file="$1" version sha
72
+ [ -f "$file" ] || return 1
73
+ version=$(jq -r '.version // ""' "$file" 2>/dev/null) || return 1
74
+ [ "$version" = "$PUSH_WAIVER_VERSION" ] || return 1
75
+ sha=$(jq -r '.diff_sha // ""' "$file" 2>/dev/null) || return 1
76
+ [ -n "$sha" ] || return 1
77
+ printf '%s' "$sha"
78
+ }
79
+
80
+ # push_waiver_matches ROOT SLUG SHA -> 0 iff a waiver covers exactly SHA.
81
+ push_waiver_matches() {
82
+ local root="$1" slug="$2" sha="$3" recorded
83
+ [ -n "$sha" ] || return 1
84
+ recorded=$(_push_waiver_sha "$(push_waiver_path "$root" "$slug")") || return 1
85
+ [ "$recorded" = "$sha" ]
86
+ }
87
+
88
+ # push_waiver_pend ROOT SLUG SHA BASE REASON_CODE
89
+ # Record that the user was asked about SHA. Overwrites any earlier pending
90
+ # marker: only the most recent question is answerable.
91
+ push_waiver_pend() {
92
+ local root="$1" slug="$2" sha="$3" base="$4" code="$5" json now
93
+ [ -n "$sha" ] || return 1
94
+ now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
95
+ json=$(jq -cn --argjson version "$PUSH_WAIVER_VERSION" --arg branch "$slug" \
96
+ --arg sha "$sha" --arg base "$base" --arg code "$code" --arg now "$now" \
97
+ '{version: $version, branch: $branch, diff_sha: $sha, base_branch: $base,
98
+ reason_code: $code, asked_at: $now}') || return 1
99
+ _push_waiver_write "$(push_waiver_pending_path "$root" "$slug")" "$json"
100
+ }
101
+
102
+ # push_waiver_promote ROOT SLUG SHA -> 0 iff a waiver was written.
103
+ # The SHA guard is the whole point: a pending marker from an older diff must
104
+ # not be cashed in by a push of different code.
105
+ push_waiver_promote() {
106
+ local root="$1" slug="$2" sha="$3" pending pending_sha json now
107
+ [ -n "$sha" ] || return 1
108
+ pending=$(push_waiver_pending_path "$root" "$slug")
109
+ pending_sha=$(_push_waiver_sha "$pending") || return 1
110
+ [ "$pending_sha" = "$sha" ] || return 1
111
+ now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
112
+ json=$(jq -c --arg now "$now" 'del(.asked_at) + {waived_at: $now}' "$pending" 2>/dev/null) || return 1
113
+ _push_waiver_write "$(push_waiver_path "$root" "$slug")" "$json" || return 1
114
+ rm -f "$pending" 2>/dev/null
115
+ return 0
116
+ }
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bash
2
+ # Quality-threshold resolver for the rust-quality / ts-quality hooks.
3
+ #
4
+ # Limits are NOT hardcoded in the quality modules. Each threshold resolves with
5
+ # this precedence (first hit wins, always a positive integer):
6
+ # 1. Project / user override — `lang.<lang>.<key>` in the merged toolu
7
+ # config (.claude/toolu.config.json), via lib/config.sh.
8
+ # 2. Native tool config (TS + maxFileLines only) — the `max-lines` rule from
9
+ # .eslintrc.json / .oxlintrc.json. Flat eslint config (eslint.config.{js,mjs,ts})
10
+ # is JS and NOT parseable here — skipped gracefully.
11
+ # 3. Built-in default (DEFAULT_* below).
12
+ #
13
+ # Every reader is guarded (jq present, file present) and 2>/dev/null — any
14
+ # failure falls through to the next layer. Never errors, never blocks a tool.
15
+ #
16
+ # Source via: . "$TOOLU_LIB_DIR/quality-config.sh"
17
+ # (pulls in config.sh + detect.sh from the same dir).
18
+
19
+ _QC_LIB_DIR="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
20
+ # shellcheck source=./config.sh
21
+ [ -f "$_QC_LIB_DIR/config.sh" ] && . "$_QC_LIB_DIR/config.sh"
22
+ # shellcheck source=./detect.sh
23
+ [ -f "$_QC_LIB_DIR/detect.sh" ] && . "$_QC_LIB_DIR/detect.sh"
24
+
25
+ # Built-in defaults — single source of truth, overridable from the environment
26
+ # (tests set these to assert fall-through without touching call sites).
27
+ : "${DEFAULT_TS_MAX_FILE_LINES:=300}"
28
+ : "${DEFAULT_TS_MAX_FN_LINES:=60}"
29
+ : "${DEFAULT_RUST_MAX_FILE_LINES:=500}"
30
+ : "${DEFAULT_RUST_MAX_FN_LINES:=50}"
31
+ : "${DEFAULT_RUST_MAX_IMPL_LINES:=200}"
32
+ : "${DEFAULT_PYTHON_MAX_FILE_LINES:=400}"
33
+ : "${DEFAULT_PYTHON_MAX_FN_LINES:=50}"
34
+
35
+ # One jq filter that normalizes every eslint/oxlint `max-lines` encoding to a
36
+ # positive integer (or empty): bare number, ["error", N], ["error", {"max": N}].
37
+ # "off" / 0 / negatives / missing all yield empty -> the layer is skipped.
38
+ _QC_ESLINT_MAXLINES_FILTER='
39
+ .rules?["max-lines"]?
40
+ | if type=="array" then (
41
+ if (.[0] == "off" or .[0] == 0) then empty # rule disabled by severity
42
+ else ( .[1] | if type=="object" then .max else . end )
43
+ end )
44
+ elif (type=="number" or type=="string") then .
45
+ else empty end
46
+ | (if type=="string" then (tonumber? // empty) else . end) # accept "120"
47
+ | if type=="number" and . > 0 then (.|floor|tostring) else empty end
48
+ '
49
+
50
+ # _qc_project_override LANG KEY -> echoes a positive int or "".
51
+ # Reads the already-merged config cache (config.sh handles user+project merge,
52
+ # malformed JSON, and missing jq).
53
+ _qc_project_override() {
54
+ local lang="$1" key="$2"
55
+ command -v toolu_load_config >/dev/null 2>&1 || return 0
56
+ toolu_load_config
57
+ [ "${_TOOLU_HAS_JQ:-0}" = "1" ] || return 0
58
+ [ -n "${TOOLU_CFG_JSON:-}" ] || return 0
59
+ jq -r --arg l "$lang" --arg k "$key" '
60
+ ((.lang? // {})[$l]? // {})[$k]?
61
+ | (if type=="string" then (tonumber? // empty) else . end) # accept "120"
62
+ | if type=="number" and . > 0 then (.|floor|tostring) else empty end
63
+ ' <<< "$TOOLU_CFG_JSON" 2>/dev/null
64
+ }
65
+
66
+ # quality_flag LANG KEY DEFAULT -> echoes "true" or "false".
67
+ # Boolean reader for `.lang.<lang>.<key>` in the merged config, for keys that
68
+ # ARE booleans (e.g. noMocks) — _qc_project_override above filters to positive
69
+ # integers and can't carry a boolean, so this is a separate reader rather than
70
+ # an extension of it. Only a literal JSON `true`/`false` is honored; anything
71
+ # else (key absent, wrong type, malformed config, no jq) falls back to DEFAULT
72
+ # unchanged. Never errors, never blocks a tool.
73
+ quality_flag() {
74
+ local lang="$1" key="$2" def="$3"
75
+ command -v toolu_load_config >/dev/null 2>&1 || { printf '%s' "$def"; return 0; }
76
+ toolu_load_config
77
+ [ "${_TOOLU_HAS_JQ:-0}" = "1" ] || { printf '%s' "$def"; return 0; }
78
+ [ -n "${TOOLU_CFG_JSON:-}" ] || { printf '%s' "$def"; return 0; }
79
+ local val
80
+ val=$(jq -r --arg l "$lang" --arg k "$key" '
81
+ ((.lang? // {})[$l]? // {})[$k]?
82
+ | if type == "boolean" then tostring else empty end
83
+ ' <<< "$TOOLU_CFG_JSON" 2>/dev/null)
84
+ case "$val" in
85
+ true|false) printf '%s' "$val" ;;
86
+ *) printf '%s' "$def" ;;
87
+ esac
88
+ }
89
+
90
+ # Memoize the git toplevel for this process. detect_project_root shells out to
91
+ # `git rev-parse` every call; the root can't change mid-hook, so cache it once
92
+ # (mirrors toolu_load_config's caching). A separate _CACHED flag is used so
93
+ # an EMPTY result (non-git cwd) is still treated as cached — keying on
94
+ # _QC_PROJECT_ROOT being non-empty would re-shell git on every call there. Tests
95
+ # re-source this file (resetting both) between cases.
96
+ _QC_PROJECT_ROOT=""
97
+ _QC_PROJECT_ROOT_CACHED=0
98
+ _qc_project_root() {
99
+ [ "$_QC_PROJECT_ROOT_CACHED" = "1" ] && { printf '%s' "$_QC_PROJECT_ROOT"; return 0; }
100
+ command -v detect_project_root >/dev/null 2>&1 || return 0
101
+ _QC_PROJECT_ROOT=$(detect_project_root)
102
+ _QC_PROJECT_ROOT_CACHED=1
103
+ printf '%s' "$_QC_PROJECT_ROOT"
104
+ }
105
+
106
+ # _qc_native_ts_max_lines -> echoes a positive int or "".
107
+ # Reads the config of the linter detect_ts_linter reports as ACTIVE, so a repo
108
+ # carrying BOTH .eslintrc.json and .oxlintrc.json (e.g. mid-migration) gets the
109
+ # limit from the one it actually runs — not whichever file is listed first.
110
+ # detect_ts_linter precedence is biome > oxc > eslint; biome's max-lines isn't
111
+ # parsed here (no machine-readable rule), so it falls through to the default.
112
+ # Limitation: only JSON config forms are parsed — `.eslintrc.yaml/.yml`, flat
113
+ # `eslint.config.*`, and `package.json#eslintConfig` fall through to the default.
114
+ # detect_ts_linter still reports "eslint" for those, so the over-limit advisory
115
+ # stays honest ("limit didn't come from its config").
116
+ # Limitation: only the top-level `.rules["max-lines"]` is read. A per-glob
117
+ # `overrides[].rules["max-lines"]` is intentionally NOT traversed — picking a
118
+ # value without matching the override's `files` glob to the edited file could
119
+ # enforce a limit that doesn't apply and wrongly BLOCK an edit. We fall through
120
+ # to the (stricter-or-equal) default instead; the over-limit advisory says so.
121
+ _qc_native_ts_max_lines() {
122
+ command -v jq >/dev/null 2>&1 || return 0
123
+ local root linter f v
124
+ root=$(_qc_project_root)
125
+ [ -n "$root" ] || return 0
126
+ # Map the active linter to its machine-readable config file.
127
+ linter=""
128
+ command -v detect_ts_linter >/dev/null 2>&1 && linter=$(detect_ts_linter)
129
+ case "$linter" in
130
+ oxc) f="$root/.oxlintrc.json" ;;
131
+ eslint) f="$root/.eslintrc.json" ;;
132
+ *) return 0 ;; # biome / none / unparseable form -> default
133
+ esac
134
+ [ -f "$f" ] || return 0
135
+ v=$(jq -r "$_QC_ESLINT_MAXLINES_FILTER" "$f" 2>/dev/null)
136
+ [ -n "$v" ] && printf '%s' "$v"
137
+ }
138
+
139
+ # quality_threshold LANG KEY DEFAULT -> always echoes a positive integer.
140
+ quality_threshold() {
141
+ local lang="$1" key="$2" def="$3" v
142
+ v=$(_qc_project_override "$lang" "$key")
143
+ [ -n "$v" ] && { printf '%s' "$v"; return 0; }
144
+ if [ "$lang" = "ts" ] && [ "$key" = "maxFileLines" ]; then
145
+ v=$(_qc_native_ts_max_lines)
146
+ [ -n "$v" ] && { printf '%s' "$v"; return 0; }
147
+ fi
148
+ printf '%s' "$def"
149
+ }
150
+
151
+ # count_code_lines lives in detect.sh (sourced above) — both lang modules
152
+ # hard-require detect.sh, so it needs no per-module fallback.
153
+
154
+ # Echo where ts maxFileLines actually resolved from: override | native | default.
155
+ # Lets the lang module word its advisory honestly — claiming "eslint enforces
156
+ # this" only when the limit really came from a parsed eslint/oxlint config, not
157
+ # when a linter is merely present with a config we can't read (e.g. .eslintrc.cjs).
158
+ ts_max_file_lines_source() {
159
+ [ -n "$(_qc_project_override ts maxFileLines)" ] && { printf 'override'; return; }
160
+ [ -n "$(_qc_native_ts_max_lines)" ] && { printf 'native'; return; }
161
+ printf 'default'
162
+ }
163
+
164
+ # Resolve the ts maxFileLines value AND its source in a single pass — prints
165
+ # "<value> <source>". The lang module needs both, and resolving them via two
166
+ # separate calls (ts_max_file_lines + ts_max_file_lines_source) repeats the
167
+ # override/native lookups; this does each lookup once.
168
+ ts_max_file_lines_resolved() {
169
+ local v
170
+ v=$(_qc_project_override ts maxFileLines)
171
+ [ -n "$v" ] && { printf '%s override' "$v"; return; }
172
+ v=$(_qc_native_ts_max_lines)
173
+ [ -n "$v" ] && { printf '%s native' "$v"; return; }
174
+ printf '%s default' "$DEFAULT_TS_MAX_FILE_LINES"
175
+ }
176
+
177
+ # Convenience wrappers the quality modules call.
178
+ ts_max_file_lines() { quality_threshold ts maxFileLines "$DEFAULT_TS_MAX_FILE_LINES"; }
179
+ ts_max_fn_lines() { quality_threshold ts maxFnLines "$DEFAULT_TS_MAX_FN_LINES"; }
180
+ rust_max_file_lines() { quality_threshold rust maxFileLines "$DEFAULT_RUST_MAX_FILE_LINES"; }
181
+ rust_max_fn_lines() { quality_threshold rust maxFnLines "$DEFAULT_RUST_MAX_FN_LINES"; }
182
+ rust_max_impl_lines() { quality_threshold rust maxImplLines "$DEFAULT_RUST_MAX_IMPL_LINES"; }
183
+ python_max_file_lines() { quality_threshold python maxFileLines "$DEFAULT_PYTHON_MAX_FILE_LINES"; }
184
+ python_max_fn_lines() { quality_threshold python maxFnLines "$DEFAULT_PYTHON_MAX_FN_LINES"; }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # Shared runtime registry for hook modules contributed by other plugins.
3
+ # Modules placed under <root>/<event>.d/<plugin-spec>__<name>.sh are executed
4
+ # by the core dispatcher after its built-in modules, gated on the owning plugin
5
+ # being installed. The "__" separator is load-bearing: plugin specs may contain
6
+ # "." (e.g. name@git.example.com) but must not contain "__". The registry holds
7
+ # GENERATED state (synced each session by each plugin's register.sh); the
8
+ # source of truth for a module is its own plugin.
9
+
10
+ _TOOLU_REGISTRY_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
11
+ # shellcheck source=host.sh
12
+ . "$_TOOLU_REGISTRY_LIB_DIR/host.sh"
13
+
14
+ # toolu_registry_root -> prints the registry root dir (not created).
15
+ toolu_registry_root() {
16
+ local agent_dir
17
+ agent_dir=$(toolu_config_root)
18
+ printf '%s/toolu' "$agent_dir"
19
+ }
20
+
21
+ # toolu_registry_event_dir EVENT -> prints "<root>/<event-slug>.d".
22
+ # Known events map to the repo's existing naming; others are CamelCase->kebab.
23
+ toolu_registry_event_dir() {
24
+ local event="$1" slug
25
+ case "$event" in
26
+ PreToolUse) slug="pre-tools" ;;
27
+ PostToolUse) slug="post-tools" ;;
28
+ *)
29
+ # Best-effort kebab-casing: consecutive-uppercase runs are NOT split
30
+ # (e.g. "MCPNotification" -> "mcpnotification"). Fine for the current
31
+ # Claude Code event names; extend the case table for anything odd.
32
+ slug=$(printf '%s' "$event" \
33
+ | sed -E 's/([a-z0-9])([A-Z])/\1-\2/g' \
34
+ | tr '[:upper:]' '[:lower:]')
35
+ ;;
36
+ esac
37
+ printf '%s/%s.d' "$(toolu_registry_root)" "$slug"
38
+ }
39
+
40
+ # Remove generated registry modules whose owning plugin is definitively absent
41
+ # from Codex's ready SessionStart snapshot. Missing/malformed/indeterminate
42
+ # snapshots prune nothing. Symlinks and un-namespaced files are never removed.
43
+ toolu_registry_prune_inactive() {
44
+ [ "$(toolu_host)" = codex ] || return 0
45
+ command -v jq >/dev/null 2>&1 || return 0
46
+
47
+ local snapshot root event_dir module base spec
48
+ snapshot=$(toolu_codex_plugin_snapshot_path)
49
+ [ -f "$snapshot" ] || return 0
50
+ jq -e '.version == 1 and .status == "ready" and (.plugins | type == "array")' \
51
+ "$snapshot" >/dev/null 2>&1 || return 0
52
+
53
+ root=$(toolu_registry_root)
54
+ for event_dir in "$root/pre-tools.d" "$root/post-tools.d"; do
55
+ [ -d "$event_dir" ] || continue
56
+ for module in "$event_dir"/*.sh; do
57
+ [ -f "$module" ] || continue
58
+ [ -L "$module" ] && continue
59
+ [ "${module%/*}" = "$event_dir" ] || continue
60
+ base=$(basename "$module")
61
+ case "$base" in
62
+ ?*__*.sh) spec=${base%%__*} ;;
63
+ *) continue ;;
64
+ esac
65
+ [ -n "$spec" ] || continue
66
+ if ! jq -e --arg spec "$spec" 'any(.plugins[]; . == $spec)' "$snapshot" >/dev/null 2>&1; then
67
+ rm -f -- "$module"
68
+ fi
69
+ done
70
+ done
71
+ return 0
72
+ }
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env bash
2
+ # Transient-state sweeper for <repo>/<host-dir>/tmp.
3
+ #
4
+ # toolu's gates key their state by branch, and nothing ever reclaimed it: a
5
+ # push-review state for a branch merged three weeks ago, a plan ledger for a
6
+ # branch that no longer exists, a quality-gate record naming files that were
7
+ # deleted. The pile is harmless right up until it is confusing, and the fix
8
+ # was `rm -rf .claude/tmp/*` by hand.
9
+ #
10
+ # This runs once at SessionStart and reclaims only what is provably spent:
11
+ #
12
+ # push-review/, plan-ledger/, docs-sync/ per-branch state — dropped when the
13
+ # branch is gone, merged into base, or older than the TTL. The CURRENT
14
+ # branch is never touched, however old: it is the one you are working on.
15
+ # quality-gate-status.json dropped when passing, or when failing about files
16
+ # that no longer exist. A failing record about live files SURVIVES — a
17
+ # violation is not fixed by forgetting it.
18
+ # telemetry/<slug>.jsonl trimmed to the retention window, never deleted
19
+ # wholesale; the history is the point.
20
+ #
21
+ # Anything else under tmp/ — including the permissions sentinel — is not ours
22
+ # and is left alone.
23
+ #
24
+ # Everything here is best-effort: a failure warns and returns 0, because a
25
+ # session must start whether or not old scratch files could be cleaned.
26
+ #
27
+ # Public API:
28
+ # toolu_sweep_state ROOT
29
+
30
+ _TOOLU_SWEEPER_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
31
+ # shellcheck source=config.sh
32
+ . "$_TOOLU_SWEEPER_LIB_DIR/config.sh"
33
+ # shellcheck source=detect.sh
34
+ . "$_TOOLU_SWEEPER_LIB_DIR/detect.sh"
35
+
36
+ TOOLU_SWEEP_DEFAULT_TTL_HOURS=24
37
+ TOOLU_SWEEP_DEFAULT_RETENTION_DAYS=7
38
+ TOOLU_SWEEP_BRANCH_DIRS="push-review plan-ledger docs-sync"
39
+
40
+ _toolu_sweep_warn() {
41
+ printf 'toolu-sweep: %s\n' "$1" >&2
42
+ }
43
+
44
+ # _toolu_sweep_number PATH DEFAULT -> a positive integer from config, or DEFAULT.
45
+ # A zero, a negative, or a non-number means "not configured" rather than an
46
+ # error — the same lenient reading the quality thresholds use.
47
+ _toolu_sweep_number() {
48
+ local path="$1" def="$2" val
49
+ toolu_load_config
50
+ if [ "$_TOOLU_HAS_JQ" != "1" ]; then
51
+ printf '%s' "$def"
52
+ return 0
53
+ fi
54
+ val=$(jq -r --arg p "$path" '
55
+ ($p | split(".")) as $ks
56
+ | (getpath($ks)) as $v
57
+ | if ($v | type) == "number" and $v > 0 then ($v | floor | tostring) else "" end
58
+ ' <<< "$TOOLU_CFG_JSON" 2>/dev/null)
59
+ [ -n "$val" ] && printf '%s' "$val" || printf '%s' "$def"
60
+ }
61
+
62
+ # _toolu_sweep_cutoff DAYS -> an ISO-8601 UTC timestamp DAYS ago.
63
+ # GNU date first, BSD/macOS second; prints nothing if neither works, which
64
+ # callers read as "cannot judge age, keep everything".
65
+ _toolu_sweep_cutoff() {
66
+ local days="$1"
67
+ date -u -d "-${days} days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
68
+ || date -u -v-"${days}"d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
69
+ || return 1
70
+ }
71
+
72
+ # _toolu_sweep_slug_of FILE -> the branch slug a state file belongs to.
73
+ # Strips the compound suffixes first so feat_x.waiver.json and
74
+ # feat_x.pending-waiver.json both resolve to feat_x.
75
+ _toolu_sweep_slug_of() {
76
+ local base
77
+ base=$(basename "$1")
78
+ base="${base%.json}"
79
+ base="${base%.pending-waiver}"
80
+ base="${base%.waiver}"
81
+ printf '%s' "$base"
82
+ }
83
+
84
+ # _toolu_sweep_reclaimable SLUG LIVE MERGED FILE TTL -> 0 iff FILE can go.
85
+ _toolu_sweep_reclaimable() {
86
+ local slug="$1" live="$2" merged="$3" file="$4" ttl_hours="$5"
87
+ case "$merged" in *" $slug "*) return 0 ;; esac
88
+ case "$live" in *" $slug "*) ;; *) return 0 ;; esac
89
+ # Branch still exists and is unmerged: age is the only remaining reason.
90
+ [ -n "$(find "$file" -maxdepth 0 -mmin "+$((ttl_hours * 60))" 2>/dev/null)" ]
91
+ }
92
+
93
+ # _toolu_sweep_branch_state ROOT STATE_ROOT TTL -> prune per-branch state dirs.
94
+ _toolu_sweep_branch_state() {
95
+ local root="$1" state_root="$2" ttl_hours="$3"
96
+ local base_branch current_branch current_slug live=" " merged=" " name dir file slug
97
+
98
+ current_branch=$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
99
+ current_slug=$(branch_slug "$current_branch")
100
+ base_branch=$(detect_base_branch "$root")
101
+
102
+ while IFS= read -r name; do
103
+ [ -n "$name" ] && live="${live}$(branch_slug "$name") "
104
+ done < <(git -C "$root" branch --format='%(refname:short)' 2>/dev/null)
105
+
106
+ # `--merged` sees ancestry only, so a SQUASH-merged branch never appears here.
107
+ # Those are reclaimed by the TTL path instead — which is exactly why the TTL
108
+ # exists alongside this check rather than being redundant with it.
109
+ while IFS= read -r name; do
110
+ [ -n "$name" ] && merged="${merged}$(branch_slug "$name") "
111
+ done < <(git -C "$root" branch --merged "$base_branch" --format='%(refname:short)' 2>/dev/null)
112
+
113
+ for dir in $TOOLU_SWEEP_BRANCH_DIRS; do
114
+ [ -d "$state_root/$dir" ] || continue
115
+ for file in "$state_root/$dir"/*.json; do
116
+ [ -f "$file" ] || continue
117
+ slug=$(_toolu_sweep_slug_of "$file")
118
+ [ "$slug" = "$current_slug" ] && continue
119
+ if _toolu_sweep_reclaimable "$slug" "$live" "$merged" "$file" "$ttl_hours"; then
120
+ rm -f "$file" 2>/dev/null || _toolu_sweep_warn "could not remove $file"
121
+ fi
122
+ done
123
+ done
124
+ }
125
+
126
+ # _toolu_sweep_gate_file GATE_FILE -> drop a spent quality-gate record.
127
+ _toolu_sweep_gate_file() {
128
+ local gate_file="$1" status live_violation entry
129
+ [ -f "$gate_file" ] || return 0
130
+ status=$(jq -r '.status // ""' "$gate_file" 2>/dev/null) || return 0
131
+ if [ "$status" = "passing" ]; then
132
+ rm -f "$gate_file" 2>/dev/null || _toolu_sweep_warn "could not remove $gate_file"
133
+ return 0
134
+ fi
135
+ [ "$status" = "failing" ] || return 0
136
+
137
+ # A failing record is only spent when every file it complains about is gone.
138
+ # Age alone must never clear it: that would silently reopen a real violation.
139
+ live_violation=0
140
+ while IFS= read -r entry; do
141
+ [ -n "$entry" ] || continue
142
+ [ "$entry" = "__global__" ] && { live_violation=1; break; }
143
+ [ -e "$entry" ] && { live_violation=1; break; }
144
+ done < <(jq -r 'if (.entries? | type) == "object" then (.entries | keys[]) else (.file // "__global__") end' \
145
+ "$gate_file" 2>/dev/null)
146
+
147
+ [ "$live_violation" = "1" ] && return 0
148
+ rm -f "$gate_file" 2>/dev/null || _toolu_sweep_warn "could not remove $gate_file"
149
+ }
150
+
151
+ # _toolu_sweep_telemetry DIR DAYS -> trim each jsonl to the retention window.
152
+ _toolu_sweep_telemetry() {
153
+ local dir="$1" days="$2" cutoff file tmp
154
+ [ -d "$dir" ] || return 0
155
+ cutoff=$(_toolu_sweep_cutoff "$days") || return 0
156
+ for file in "$dir"/*.jsonl; do
157
+ [ -f "$file" ] || continue
158
+ tmp=$(mktemp "${file}.XXXXXX" 2>/dev/null) || continue
159
+ # A file jq cannot parse is left exactly as it is: it may be another
160
+ # tool's, or a partially written line worth keeping for debugging.
161
+ if jq -c --arg cutoff "$cutoff" 'select((.t // "") >= $cutoff)' "$file" > "$tmp" 2>/dev/null; then
162
+ if [ -s "$tmp" ]; then
163
+ mv -f "$tmp" "$file" 2>/dev/null || rm -f "$tmp" 2>/dev/null
164
+ else
165
+ rm -f "$tmp" "$file" 2>/dev/null
166
+ fi
167
+ else
168
+ rm -f "$tmp" 2>/dev/null
169
+ fi
170
+ done
171
+ }
172
+
173
+ # toolu_sweep_state ROOT -> reclaim spent transient state. Always returns 0.
174
+ toolu_sweep_state() {
175
+ local root="${1:-}" state_root ttl_hours retention_days
176
+ [ -n "$root" ] || root=$(toolu_project_root)
177
+ [ -n "$root" ] || return 0
178
+ command -v jq >/dev/null 2>&1 || return 0
179
+
180
+ # Opt-out, and a hard requirement of git for the branch questions below.
181
+ toolu_enabled gates sweep || return 0
182
+ command -v git >/dev/null 2>&1 || return 0
183
+
184
+ state_root=$(toolu_project_state_root "$root")
185
+ [ -n "$state_root" ] && [ -d "$state_root" ] || return 0
186
+
187
+ ttl_hours=$(_toolu_sweep_number gates.stateTtlHours "$TOOLU_SWEEP_DEFAULT_TTL_HOURS")
188
+ retention_days=$(_toolu_sweep_number gates.telemetryRetentionDays "$TOOLU_SWEEP_DEFAULT_RETENTION_DAYS")
189
+
190
+ _toolu_sweep_branch_state "$root" "$state_root" "$ttl_hours"
191
+ _toolu_sweep_gate_file "$state_root/quality-gate-status.json"
192
+ _toolu_sweep_telemetry "$state_root/telemetry" "$retention_days"
193
+ return 0
194
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # Workflow telemetry append engine: the one write path every gate/lib event
3
+ # funnels through, so telemetry.enabled / jq-absent / no-branch all degrade
4
+ # the same way regardless of caller.
5
+ #
6
+ # Public API:
7
+ # telemetry_append REPO_ROOT EVENT [EXTRA_JSON_OBJECT]
8
+ # Appends one line `{"v":1,"t":<iso8601 UTC>,"branch":<branch>,
9
+ # "event":<name>, ...EXTRA_JSON_OBJECT}` to
10
+ # REPO_ROOT/<host-state>/tmp/telemetry/<branch_slug>.jsonl
11
+ # ($TELEMETRY_DIR overrides the directory). EXTRA_JSON_OBJECT defaults to
12
+ # "{}"; its keys are merged in but can never override v/t/branch/event.
13
+ # Always returns 0 — a telemetry bug must never fail the caller's real
14
+ # work — no-op (nothing written) when:
15
+ # - telemetry.enabled=false in the merged toolu config
16
+ # - jq is missing
17
+ # - HEAD is unborn/detached (mirrors push-review.sh's and docs-sync.sh's
18
+ # own detached-HEAD refusal: branch_slug would otherwise collapse
19
+ # every such caller onto the shared "_default" slug, mixing unrelated
20
+ # branches' events into one file)
21
+ # A malformed EXTRA_JSON_OBJECT, or an assembled line over 3900 bytes,
22
+ # is a soft failure: warns on stderr, appends nothing, still returns 0.
23
+ #
24
+ # CONCURRENCY: exactly one `printf '%s\n' >>` per call — a single O_APPEND
25
+ # write. That is atomic only while the line stays under the platform's
26
+ # atomic-pipe-write floor (historically 4 KB on POSIX), so the assembled
27
+ # line's byte length is checked before every write: over 3900 bytes
28
+ # (headroom under that floor) it is dropped rather than risking a
29
+ # torn/interleaved write with a concurrent writer — callers must still keep
30
+ # EXTRA_JSON_OBJECT small so their events aren't silently lost this way.
31
+ #
32
+ # Source via: . "${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}/telemetry.sh"
33
+
34
+ _TELEMETRY_LIB_DIR="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
35
+ # shellcheck source=./detect.sh
36
+ [ -f "$_TELEMETRY_LIB_DIR/detect.sh" ] && . "$_TELEMETRY_LIB_DIR/detect.sh"
37
+ # shellcheck source=./config.sh
38
+ [ -f "$_TELEMETRY_LIB_DIR/config.sh" ] && . "$_TELEMETRY_LIB_DIR/config.sh"
39
+
40
+ # telemetry_append REPO_ROOT EVENT [EXTRA_JSON_OBJECT]
41
+ telemetry_append() {
42
+ local repo_root="$1" event="$2" extra="${3:-}"
43
+ [ -n "$extra" ] || extra='{}'
44
+ [ -n "$repo_root" ] && [ -n "$event" ] || return 0
45
+
46
+ command -v jq >/dev/null 2>&1 || return 0
47
+ command -v git >/dev/null 2>&1 || return 0
48
+
49
+ # Default-on: absent/malformed config -> enabled; explicit false -> no-op.
50
+ if command -v toolu_enabled >/dev/null 2>&1; then
51
+ toolu_enabled telemetry enabled || return 0
52
+ fi
53
+
54
+ local branch
55
+ branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
56
+ [[ -z "$branch" || "$branch" == "HEAD" ]] && return 0
57
+
58
+ # branch_slug comes from detect.sh, sourced softly above — an older/absent
59
+ # detect.sh must not turn into an unbound-function crash for every caller.
60
+ command -v branch_slug >/dev/null 2>&1 || return 0
61
+
62
+ local slug now line dir file line_bytes
63
+ slug=$(branch_slug "$branch")
64
+ now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
65
+
66
+ # $extra merged FIRST so the protocol fields (v/t/branch/event) always win
67
+ # on key collision — a caller-supplied "branch" or "event" in extra can
68
+ # never spoof the real ones. A malformed or non-object $extra makes this
69
+ # jq call fail; treat that as the one "malformed extra JSON" soft-failure.
70
+ if ! line=$(jq -cn --argjson extra "$extra" --arg t "$now" --arg branch "$branch" \
71
+ --arg event "$event" '$extra + {v: 1, t: $t, branch: $branch, event: $event}' \
72
+ 2>/dev/null) || [ -z "$line" ]; then
73
+ printf 'telemetry: malformed extra JSON for event "%s"; skipping append\n' "$event" >&2
74
+ return 0
75
+ fi
76
+
77
+ # Keep the single O_APPEND write atomic (see CONCURRENCY above): a line
78
+ # over 3900 bytes risks crossing the platform's atomic-pipe-write floor, so
79
+ # drop it rather than risk tearing/interleaving with a concurrent writer.
80
+ line_bytes=$(printf '%s' "$line" | wc -c | tr -d ' ')
81
+ if [ "$line_bytes" -gt 3900 ]; then
82
+ printf 'telemetry: assembled line for event "%s" is %d bytes (>3900); skipping append\n' \
83
+ "$event" "$line_bytes" >&2
84
+ return 0
85
+ fi
86
+
87
+ dir="${TELEMETRY_DIR:-$(toolu_project_state_dir telemetry "$repo_root")}"
88
+ file="$dir/${slug}.jsonl"
89
+ mkdir -p "$dir" 2>/dev/null || return 0
90
+
91
+ printf '%s\n' "$line" >> "$file" 2>/dev/null
92
+
93
+ return 0
94
+ }