@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,93 @@
1
+ #!/usr/bin/env bash
2
+ # Post-tool check: Rust quality rules.
3
+ # Project-agnostic: no-op outside Rust projects or when cargo is missing.
4
+ # Unsafe-block exemptions for FFI crates come from
5
+ # $TOOLU_SETTINGS_DIR/rust-unsafe-exemptions.txt.
6
+ #
7
+ # Inputs (from parent dispatcher post-tools/mod.sh, via `export`):
8
+ # $tool_name - name of the tool being invoked
9
+ # $input - raw JSON payload on stdin
10
+ # $PROJECT_ROOT - repository root
11
+
12
+ : "${tool_name:=}"
13
+ : "${input:=}"
14
+ : "${PROJECT_ROOT:=$(pwd)}"
15
+
16
+ # Core lib comes from the toolu dispatcher via TOOLU_LIB_DIR (set by
17
+ # plugins/toolu/hooks/post-tools/mod.sh before registry dispatch). Outside
18
+ # that pipeline there is no relative path to it — fail SOFT: a quality check
19
+ # must never break a tool call by erroring.
20
+ [ -n "${TOOLU_LIB_DIR:-}" ] && [ -f "$TOOLU_LIB_DIR/detect.sh" ] || exit 0
21
+ # shellcheck source=../../../toolu/hooks/lib/detect.sh
22
+ . "$TOOLU_LIB_DIR/detect.sh"
23
+ # Threshold resolver (defaults + project/native overrides). Soft if absent.
24
+ # shellcheck source=../../../toolu/hooks/lib/quality-config.sh
25
+ [ -f "$TOOLU_LIB_DIR/quality-config.sh" ] && . "$TOOLU_LIB_DIR/quality-config.sh"
26
+ # Multi-slot gate writer (entries keyed by file — one hook's failure no longer
27
+ # clobbers another's). Soft if absent: fallbacks below keep the legacy
28
+ # single-slot behavior when the toolu lib predates gate-file.sh.
29
+ # shellcheck source=../../../toolu/hooks/lib/gate-file.sh
30
+ [ -f "$TOOLU_LIB_DIR/gate-file.sh" ] && . "$TOOLU_LIB_DIR/gate-file.sh"
31
+ command -v gate_record_failure >/dev/null 2>&1 || gate_record_failure() {
32
+ jq -n --arg reason "$4" --arg source "$3" --arg file "$2" --arg violations "$5" \
33
+ --arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
34
+ '{status: "failing", reason: $reason, source: $source, file: $file,
35
+ violations: $violations, updatedAt: $updatedAt}' > "$1"
36
+ }
37
+ command -v gate_clear_file >/dev/null 2>&1 || gate_clear_file() {
38
+ [ -f "$1" ] || return 0
39
+ local _src _file
40
+ _src=$(jq -r '.source // ""' "$1" 2>/dev/null || echo "")
41
+ _file=$(jq -r '.file // ""' "$1" 2>/dev/null || echo "")
42
+ if [ "$_src" = "$3" ] && [ "$_file" = "$2" ]; then
43
+ jq -n --arg source "$3" --arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
44
+ '{status: "passing", source: $source, updatedAt: $updatedAt}' > "$1"
45
+ fi
46
+ }
47
+ command -v rust_max_file_lines >/dev/null 2>&1 || rust_max_file_lines() { echo "${DEFAULT_RUST_MAX_FILE_LINES:-500}"; }
48
+ command -v rust_max_fn_lines >/dev/null 2>&1 || rust_max_fn_lines() { echo "${DEFAULT_RUST_MAX_FN_LINES:-50}"; }
49
+ command -v rust_max_impl_lines >/dev/null 2>&1 || rust_max_impl_lines() { echo "${DEFAULT_RUST_MAX_IMPL_LINES:-200}"; }
50
+ # Boolean flag reader (e.g. noMocks) — falls back to the caller's default when
51
+ # quality-config.sh predates this reader.
52
+ command -v quality_flag >/dev/null 2>&1 || quality_flag() { printf '%s' "$3"; }
53
+ # count_code_lines comes from detect.sh (sourced above) — no fallback needed.
54
+
55
+ # Load the merged config ONCE so TOOLU_CFG_LOADED sticks for the threshold
56
+ # lookups below — each runs in a $(...) subshell that inherits it and skips
57
+ # re-merging (otherwise every wrapper re-spawns the jq merge).
58
+ command -v toolu_load_config >/dev/null 2>&1 && toolu_load_config 2>/dev/null || true
59
+
60
+ [ "$(detect_rust)" = "rust" ] || exit 0
61
+ command -v cargo >/dev/null 2>&1 || exit 0
62
+ command -v jq >/dev/null 2>&1 || exit 0
63
+
64
+ TOOLU_SETTINGS_DIR=$(toolu_settings_dir)
65
+ EXEMPTIONS_FILE="$TOOLU_SETTINGS_DIR/rust-unsafe-exemptions.txt"
66
+
67
+ # read_list is sourced from lib/detect.sh.
68
+
69
+ fp_from_input=""
70
+ if [[ "$tool_name" == "Write" || "$tool_name" == "Edit" || "$tool_name" == "MultiEdit" ]]; then
71
+ fp_from_input=$(echo "$input" | jq -r '.tool_input.path // .tool_input.file_path // .tool_input.target_file // empty' 2>/dev/null || echo "")
72
+ fi
73
+ FILE_PATH="${CLAUDE_FILE_PATHS:-$fp_from_input}"
74
+
75
+ # Deleted files and move sources cannot be linted after apply_patch completes,
76
+ # but their path-owned quality failure must not remain active forever.
77
+ EDIT_OPERATION="${TOOLU_EDIT_OPERATION:-$(echo "$input" | jq -r '.tool_input.toolu_edit_operation // ""' 2>/dev/null || echo "")}"
78
+ EDIT_MOVED_TO="${TOOLU_EDIT_MOVED_TO:-$(echo "$input" | jq -r '.tool_input.toolu_edit_moved_to // ""' 2>/dev/null || echo "")}"
79
+ if [[ "$EDIT_OPERATION" == delete || -n "$EDIT_MOVED_TO" ]]; then
80
+ if [[ -n "$FILE_PATH" && "$FILE_PATH" =~ \.rs$ ]]; then
81
+ GATE_FILE="$(toolu_project_state_root "$PROJECT_ROOT")/quality-gate-status.json"
82
+ gate_clear_file "$GATE_FILE" "$FILE_PATH" "rust-quality-hook"
83
+ fi
84
+ exit 0
85
+ fi
86
+
87
+ [[ -z "$FILE_PATH" || ! -f "$FILE_PATH" ]] && exit 0
88
+ [[ ! "$FILE_PATH" =~ \.rs$ ]] && exit 0
89
+
90
+ MESSAGES=""
91
+ add_error() {
92
+ MESSAGES="${MESSAGES}${1}"$'\n'
93
+ }
@@ -0,0 +1,10 @@
1
+ RUST_MAX_FILE=$(rust_max_file_lines)
2
+ LINE_COUNT=$(count_code_lines "$FILE_PATH")
3
+ if [[ "$LINE_COUNT" -gt "$RUST_MAX_FILE" ]]; then
4
+ _split_hint="split into submodules"
5
+ [ -n "$(detect_clippy)" ] && _split_hint="$_split_hint (clippy enforces complexity here)"
6
+ _approx=""
7
+ has_unterminated_block "$FILE_PATH" && _approx=" (size approximated — an unterminated /* or a string containing /* may be affecting the count)"
8
+ add_error "File exceeds ${RUST_MAX_FILE}-line limit: $FILE_PATH ($LINE_COUNT code lines, blanks/comments excluded)${_approx} — $_split_hint"
9
+ fi
10
+
@@ -0,0 +1,84 @@
1
+ _has_inline_cfg_test=0
2
+ # Match bare `#[cfg(test)]` and `test` as the FIRST predicate of an all()/any()
3
+ # combinator (`#[cfg(all(test, feature = "x"))]`, `#[cfg(any(test, ...))]`).
4
+ # Deliberately NOT a loose `cfg(.*\btest\b`: that would false-positive on
5
+ # `#[cfg(not(test))]` (the opposite gate) and `#[cfg(feature = "test-utils")]`
6
+ # (test inside a string). A non-leading `test` predicate (`all(feature, test)`)
7
+ # is the rare miss we accept to keep zero false positives.
8
+ #
9
+ # EXEMPTION: a bare `#[cfg(test)]` that wires a BODYLESS `mod <name>;` decl —
10
+ # single-line (`#[cfg(test)] mod tests;`) or rustfmt's attribute-on-its-own-line
11
+ # two-line form (`#[cfg(test)]` then `mod tests;`) — is a pointer to an external
12
+ # module-sibling tests/ dir, not inline test code, and is not flagged. Further
13
+ # OUTER attributes may sit between the two: `#[path = "tests/stats.rs"]` is the
14
+ # only wiring a crate that bans mod.rs has for a module-sibling test file, so
15
+ # the decl is looked for past any attribute run, bounded so it cannot scan the
16
+ # whole file. Inner attributes (`#![...]`) are deliberately not skipped — they
17
+ # cannot precede a declaration, and neither is an attribute split across lines
18
+ # (`#[path =` / `"x.rs"]`) — same line-based limitation as 30-suppression.sh,
19
+ # and it fails closed: the file is reported rather than silently exempted.
20
+ # Only the BARE cfg(test) form earns this: the
21
+ # all()/any() combinator stays blocked unconditionally (even ahead of a bodyless
22
+ # decl), and any body (`mod tests {`, any line form) stays blocked. grep is
23
+ # line-based, so the multi-line pairing needs awk (getline the next lines); no
24
+ # \b — BSD awk has none, use a negated [[:alnum:]_] class (or line end) for the
25
+ # boundary instead.
26
+ if [[ "$FILE_PATH" == */src/* ]] \
27
+ && awk '
28
+ /^[[:space:]]*#\[cfg\((all|any)\(test([^[:alnum:]_]|$)/ { found=1; exit }
29
+ /^[[:space:]]*#\[cfg\(test\)\][[:space:]]*(pub[[:space:]]+)?mod[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]*;[[:space:]]*(\/\/.*)?$/ { next }
30
+ /^[[:space:]]*#\[cfg\(test\)\][[:space:]]*$/ {
31
+ nxt=""
32
+ # 8 attributes plus the decl line itself — each pass reads one line, so
33
+ # the bound has to leave room for the decl read or a decl behind the
34
+ # full 8 attributes is reported.
35
+ for (i = 0; i < 9; i++) {
36
+ if ((getline nxt) <= 0) { found=1; exit }
37
+ if (nxt !~ /^[[:space:]]*#\[[^!]/) break
38
+ }
39
+ if (nxt ~ /^[[:space:]]*(pub[[:space:]]+)?mod[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]*;[[:space:]]*(\/\/.*)?$/) { next }
40
+ found=1; exit
41
+ }
42
+ /^[[:space:]]*#\[cfg\(test\)\]/ { found=1; exit }
43
+ END { exit (found ? 0 : 1) }
44
+ ' "$FILE_PATH" 2>/dev/null; then
45
+ add_error "Inline #[cfg(test)] in $FILE_PATH — unit tests belong in a module-sibling tests/ dir wired by a bodyless #[cfg(test)] mod tests; decl; crate-root tests/ is for integration tests"
46
+ _has_inline_cfg_test=1
47
+ fi
48
+
49
+ # Test placement: a test-bearing .rs file must live under a tests/ dir, kept
50
+ # flat (only fixtures/helpers/common subdirs allowed — common/mod.rs is the
51
+ # cargo idiom for shared test helpers). Skip when the inline-#[cfg(test)] rule
52
+ # already fired: that's the canonical `src/lib.rs` with `#[cfg(test)] mod tests`
53
+ # pattern, where "move the file to tests/" is wrong (it would orphan the pub
54
+ # items) — the cfg(test) message already says to extract the tests.
55
+ _is_rust_test=0
56
+ case "$(basename "$FILE_PATH")" in
57
+ *_test.rs|*_tests.rs) _is_rust_test=1 ;;
58
+ esac
59
+ # #[bench] and #[wasm_bindgen_test] are deliberately NOT in the alternation:
60
+ # benches belong in benches/ (cargo convention) and wasm-bindgen tests have no
61
+ # single canonical home — "move to tests/" would be a false positive for both.
62
+ # Generalized over `#[<any::path>::test]` rather than a hardcoded runtime list,
63
+ # so test_log::test, trace_test::test, actix_web::test, etc. all trip the rule;
64
+ # `test_case` (the test_case crate's generator) is added explicitly. `#[rstest]`
65
+ # is its own alternation (the macro name is not `test`). `#[cfg(test)]` is NOT
66
+ # matched here — no `test`/`test_case` token follows `#[` — and stays owned by
67
+ # the inline-cfg(test) rule above.
68
+ if [[ "$_is_rust_test" -eq 0 ]] \
69
+ && grep -qE '^[[:space:]]*#\[([A-Za-z_][A-Za-z0-9_]*::)*(test|test_case)\b|^[[:space:]]*#\[rstest\b' "$FILE_PATH" 2>/dev/null; then
70
+ _is_rust_test=1
71
+ fi
72
+ if [[ "$_is_rust_test" -eq 1 && "$_has_inline_cfg_test" -eq 0 ]]; then
73
+ if [[ "$FILE_PATH" != */tests/* ]]; then
74
+ add_error "Rust test file outside tests/: $FILE_PATH — move to a sibling tests/ directory"
75
+ else
76
+ _after_tests="${FILE_PATH##*/tests/}"
77
+ if [[ "$_after_tests" == */* ]]; then
78
+ _subdir="${_after_tests%%/*}"
79
+ if [[ "$_subdir" != "fixtures" && "$_subdir" != "helpers" && "$_subdir" != "common" ]]; then
80
+ add_error "Rust test nested in tests/ subdirectory: $FILE_PATH — keep tests/ flat (only fixtures/helpers/common subdirs allowed)"
81
+ fi
82
+ fi
83
+ fi
84
+ fi
@@ -0,0 +1,26 @@
1
+
2
+ # Forbidden lint suppression: #[allow(...)] / #![allow(...)] / #[expect(...)]
3
+ # and the #[cfg_attr(..., allow(...))] / cfg_attr(..., expect(...)) back door.
4
+ # Known limitation: grep is line-based, so an attribute split across multiple
5
+ # lines (e.g. `#[cfg_attr(\n test,\n allow(...))]`) escapes detection. Rare;
6
+ # multi-line attribute parsing isn't worth a full tokenizer here.
7
+ #
8
+ # EXEMPTION (tests/ only): the file-level INNER header — `#![allow(...)]` and
9
+ # the `#![cfg_attr(...)]` form of it — is not flagged in a test file. The lints
10
+ # a test suppresses there (unwrap_used, expect_used, panic, float_cmp,
11
+ # too_many_lines) are ones test code violates on purpose: a test that must
12
+ # panic to fail is not a defect being hidden, and the toolu-conventions Rust
13
+ # template prescribes exactly this header. Only the INNER form earns it —
14
+ # a per-item `#[allow(...)]` inside a test file silences one real warning on
15
+ # one real item and stays banned, as everywhere else. Same shape as
16
+ # ts-quality's narrower @ts-expect-error exemption for .test/.spec files.
17
+ _rs_suppress_re='^[[:space:]]*#!?\[(allow|expect)\(|^[[:space:]]*#!?\[cfg_attr\([^]]*\b(allow|expect)\b'
18
+ _rs_suppress_hint=""
19
+ if [[ "$FILE_PATH" == */tests/* ]]; then
20
+ _rs_suppress_re='^[[:space:]]*#\[(allow|expect)\(|^[[:space:]]*#\[cfg_attr\([^]]*\b(allow|expect)\b'
21
+ _rs_suppress_hint=" In a test file only the file-level #![allow(...)] header is accepted."
22
+ fi
23
+ if grep -qE "$_rs_suppress_re" "$FILE_PATH" 2>/dev/null; then
24
+ add_error "Forbidden lint suppression (#[allow]/#[expect]/cfg_attr allow) in $FILE_PATH — remove it and fix the underlying warning in code. For unsafe_code, override in Cargo.toml [lints.rust].${_rs_suppress_hint}"
25
+ fi
26
+
@@ -0,0 +1,39 @@
1
+ # Forbidden unsafe blocks/functions — except for crates listed in the
2
+ # exemptions file (FFI crates, sandboxes, etc).
3
+ exempt=0
4
+ if [ -f "$EXEMPTIONS_FILE" ]; then
5
+ while IFS= read -r crate; do
6
+ [ -z "$crate" ] && continue
7
+ if [[ "$FILE_PATH" == *"$crate"* ]]; then
8
+ exempt=1
9
+ break
10
+ fi
11
+ done <<< "$(read_list "$EXEMPTIONS_FILE")"
12
+ fi
13
+ if [ "$exempt" -eq 0 ]; then
14
+ # Strip // line comments and /* */ blocks (incl. multi-line) before matching,
15
+ # so a commented-out or doc-mentioned `unsafe {` does not false-positive. The
16
+ # block-comment state machine mirrors count_code_lines in detect.sh. String
17
+ # literals containing the pattern are not handled — same heuristic limit as the
18
+ # other comment-stripping passes here. BSD awk (macOS) has no \b, so explicit
19
+ # boundaries are used instead.
20
+ if awk '
21
+ { line=$0
22
+ if (inblock) {
23
+ idx=index(line,"*/")
24
+ if (idx>0) { line=substr(line, idx+2); inblock=0 } else next
25
+ }
26
+ while ((s=index(line,"/*"))>0) {
27
+ rest=substr(line, s+2); e=index(rest,"*/")
28
+ if (e>0) { line=substr(line,1,s-1) substr(rest, e+2) }
29
+ else { line=substr(line,1,s-1); inblock=1; break }
30
+ }
31
+ sub(/\/\/.*$/, "", line) # // line comment
32
+ if (line ~ /(^|[^A-Za-z0-9_])unsafe[ \t]*(\{|fn )/) { found=1; exit }
33
+ }
34
+ END { exit(found ? 0 : 1) }
35
+ ' "$FILE_PATH" 2>/dev/null; then
36
+ add_error "Forbidden unsafe code in $FILE_PATH — refactor to safe alternative. Add crate to settings/rust-unsafe-exemptions.txt if it legitimately needs unsafe (FFI, sandboxing)."
37
+ fi
38
+ fi
39
+
@@ -0,0 +1,38 @@
1
+ RUST_MAX_FN=$(rust_max_fn_lines)
2
+ # Match fn with any leading visibility/qualifier combo: pub, pub(crate)/pub(super),
3
+ # async, const, unsafe, extern "C", and combinations thereof.
4
+ # The fn end is found with a brace-depth counter from the fn's own line, so a
5
+ # method inside an impl/mod is measured to ITS close (not the impl's), and an
6
+ # inner if/match/loop close can't end the range early. Body-less trait decls
7
+ # (`fn f();`) are released on the `;`. Single-line "..." strings and the
8
+ # '{'/'}' char literals are stripped before counting so a lone brace inside
9
+ # them cannot skew the depth. Known limitation: a brace inside a MULTI-LINE
10
+ # string (incl. raw strings) still leaks into the count — an unbalanced one
11
+ # leaves the fn unmeasured (fail-open), never falsely flagged short.
12
+ LONG_RS_FUNCS=$(awk -v max="$RUST_MAX_FN" -v q="'" '
13
+ !infn && /^[[:space:]]*(pub(\([^)]+\))?[[:space:]]+)?((async|const|unsafe|extern)([[:space:]]+"[^"]*")?[[:space:]]+)*fn / {
14
+ infn=1; start=NR; name=$0; depth=0; opened=0
15
+ }
16
+ infn {
17
+ line=$0
18
+ gsub(/\\"/, "", line) # escaped quotes would derail the span strip
19
+ gsub(/"[^"]*"/, "", line) # single-line string contents
20
+ gsub(q "[{}]" q, "", line) # brace char literals (lifetimes never contain braces)
21
+ no=gsub(/\{/, "{", line); nc=gsub(/\}/, "}", line)
22
+ depth += no - nc
23
+ if (no > 0) opened=1
24
+ if (opened && depth <= 0) {
25
+ len=NR-start
26
+ if (len > max) printf "%s:%d (%d lines)\n", name, start, len
27
+ infn=0
28
+ }
29
+ if (!opened && $0 ~ /;[[:space:]]*$/) infn=0
30
+ }
31
+ ' "$FILE_PATH" 2>/dev/null)
32
+ # Test files are exempt: a table-driven test is one long fn by nature, and the
33
+ # toolu-conventions Rust test header prescribes `clippy::too_many_lines` for
34
+ # exactly that reason — flagging it here would contradict the template.
35
+ if [[ -n "$LONG_RS_FUNCS" && "$_is_rust_test" -ne 1 ]]; then
36
+ add_error "Function too long in $FILE_PATH (>${RUST_MAX_FN} lines) — extract helpers."
37
+ fi
38
+
@@ -0,0 +1,28 @@
1
+ RUST_MAX_IMPL=$(rust_max_impl_lines)
2
+ # Brace-depth counter (same technique + string/char strip as the fn check):
3
+ # a bare `^\}` close-marker would be tripped early by a column-0 `}` from
4
+ # macro-generated or wide-rustfmt code inside the impl, undercounting it.
5
+ LONG_IMPL=$(awk -v max="$RUST_MAX_IMPL" -v q="'" '
6
+ !inimpl && /^[[:space:]]*(unsafe[[:space:]]+)?impl[[:space:]<]/ {
7
+ inimpl=1; start=NR; name=$0; depth=0; opened=0
8
+ }
9
+ inimpl {
10
+ line=$0
11
+ gsub(/\\"/, "", line)
12
+ gsub(/"[^"]*"/, "", line)
13
+ gsub(q "[{}]" q, "", line)
14
+ no=gsub(/\{/, "{", line); nc=gsub(/\}/, "}", line)
15
+ depth += no - nc
16
+ if (no > 0) opened=1
17
+ if (opened && depth <= 0) {
18
+ len=NR-start
19
+ if (len > max) printf "%s:%d (%d lines)\n", name, start, len
20
+ inimpl=0
21
+ }
22
+ }
23
+ ' "$FILE_PATH" 2>/dev/null)
24
+ # Exempt in test files, for the same reason as the fn-length rule above.
25
+ if [[ -n "$LONG_IMPL" && "$_is_rust_test" -ne 1 ]]; then
26
+ add_error "Impl block too large in $FILE_PATH (>${RUST_MAX_IMPL} lines) — split into trait impls or modules."
27
+ fi
28
+
@@ -0,0 +1,149 @@
1
+ # shellcheck shell=bash
2
+ # --- Error-handling rules (zero tolerance) ---
3
+ # src/ is production code, so any panic-on-error pattern here is a prod panic
4
+ # — EXCEPT in a test file. "tests must live in tests/" is true of the crate-root
5
+ # suite but not of a module-sibling one: a crate that bans mod.rs wires its
6
+ # colocated tests as src/<mod>/tests/<name>.rs, which is under src/ and is still
7
+ # test code. `.unwrap()` there is how a test reports a failed assertion, not a
8
+ # production panic. Same `_is_rust_test` gate 90-docs.sh already uses (set in
9
+ # 20-tests.sh: *_test.rs/*_tests.rs filename, or a #[test]/#[rstest]/#[test_case]
10
+ # attribute in the body).
11
+ if [[ "$FILE_PATH" == */src/* && "$_is_rust_test" -ne 1 ]] && command -v ast-grep >/dev/null 2>&1; then
12
+ # ONE ast-grep process for all six rules. `ast-grep run -p` re-parses the file
13
+ # per pattern; `scan --inline-rules` parses once and returns every rule's hits
14
+ # as JSON. The excerpt lines rebuilt from .file/.range/.lines below are
15
+ # byte-identical to what `run -p` printed. Mirrors ts-quality/78-error-ast.sh.
16
+ #
17
+ # severity is `warning` on purpose: `scan` exits non-zero when an ERROR-level
18
+ # rule matches, which would be indistinguishable from a crashed tool. At
19
+ # warning level both a clean and a matching run exit 0, so a non-zero exit (or
20
+ # any stderr) still means the tool genuinely broke.
21
+ ast_err_file="$(mktemp)"
22
+ ast_rules=$(cat <<'RS_AST_RULES'
23
+ id: unwrap
24
+ language: rust
25
+ severity: warning
26
+ message: unwrap on Result/Option
27
+ rule:
28
+ pattern: '$E.unwrap()'
29
+ ---
30
+ id: expect
31
+ language: rust
32
+ severity: warning
33
+ message: expect on Result/Option
34
+ rule:
35
+ pattern: '$E.expect($M)'
36
+ ---
37
+ id: panic
38
+ language: rust
39
+ severity: warning
40
+ message: panic!
41
+ rule:
42
+ pattern: 'panic!($$$)'
43
+ ---
44
+ id: todo
45
+ language: rust
46
+ severity: warning
47
+ message: todo!
48
+ rule:
49
+ pattern: 'todo!($$$)'
50
+ ---
51
+ id: unimplemented
52
+ language: rust
53
+ severity: warning
54
+ message: unimplemented!
55
+ rule:
56
+ pattern: 'unimplemented!($$$)'
57
+ ---
58
+ id: unreachable
59
+ language: rust
60
+ severity: warning
61
+ message: unreachable!
62
+ rule:
63
+ pattern: 'unreachable!($$$)'
64
+ RS_AST_RULES
65
+ )
66
+
67
+ ast_grep_failed=0
68
+ ast_grep_fail_detail=""
69
+ ast_lines=""
70
+ ast_json=$(ast-grep scan --inline-rules "$ast_rules" --json "$FILE_PATH" 2>"$ast_err_file")
71
+ ast_rc=$?
72
+ # Flatten every hit to "<ruleId>\t<file>:<line>:<source line>", one output line
73
+ # per matched source line.
74
+ #
75
+ # Two distinct failure modes, reported distinctly: ast-grep itself broke, or it
76
+ # exited 0 and returned something that is not the documented JSON array. Both
77
+ # mean the rules could not be verified, but collapsing them into one message
78
+ # sends the reader to the wrong tool.
79
+ ast_fail_stage=""
80
+ if [[ "$ast_rc" -ne 0 || -s "$ast_err_file" ]]; then
81
+ ast_grep_failed=1
82
+ ast_fail_stage="ast-grep"
83
+ elif ! ast_lines=$(printf '%s' "$ast_json" | jq -r '
84
+ .[] | . as $m
85
+ | ((($m.lines // "") | split("\n")) | to_entries[])
86
+ | "\($m.ruleId)\t\($m.file):\($m.range.start.line + 1 + .key):\(.value)"
87
+ ' 2>/dev/null); then
88
+ ast_grep_failed=1
89
+ ast_fail_stage="jq"
90
+ ast_lines=""
91
+ fi
92
+ if [[ "$ast_grep_failed" -ne 0 ]]; then
93
+ # Exit code plus the first stderr line (capped at ~200 chars) so the surfaced
94
+ # message tells the agent WHAT broke, not just THAT it broke.
95
+ ast_stderr_first=$(head -n 1 "$ast_err_file" 2>/dev/null | cut -c1-200)
96
+ if [[ "$ast_fail_stage" == "jq" ]]; then
97
+ ast_grep_fail_detail="ast-grep exited 0 but its output did not parse as the documented JSON array"
98
+ else
99
+ ast_grep_fail_detail="ast-grep exit ${ast_rc}${ast_stderr_first:+: $ast_stderr_first}"
100
+ fi
101
+ fi
102
+
103
+ # join_hits BLOCK... — concatenate non-empty hit blocks into $JOINED with one
104
+ # newline between them. Each block comes from a $(...) capture, which strips the
105
+ # trailing newline, so plain concatenation glued one block's last line onto the
106
+ # next block's first line. Sets a variable instead of printing to avoid a fork
107
+ # on a path that runs after every edit.
108
+ join_hits() {
109
+ local _part
110
+ JOINED=""
111
+ for _part in "$@"; do
112
+ [ -z "$_part" ] && continue
113
+ [ -n "$JOINED" ] && JOINED="${JOINED}"$'\n'
114
+ JOINED="${JOINED}${_part}"
115
+ done
116
+ }
117
+
118
+ # ast_hits RULE_ID LIMIT — this rule's excerpt lines, capped like before.
119
+ ast_hits() {
120
+ local _id="$1" _limit="$2" _row _rest
121
+ [ -n "$ast_lines" ] || return 0
122
+ while IFS=$'\t' read -r _row _rest; do
123
+ [ "$_row" = "$_id" ] && printf '%s\n' "$_rest"
124
+ done <<< "$ast_lines" | head -n "$_limit"
125
+ }
126
+
127
+ UNWRAP_HITS=$(ast_hits unwrap 5)
128
+ if [[ -n "$UNWRAP_HITS" ]]; then
129
+ add_error ".unwrap() in $FILE_PATH — use ? or match on Result/Option"$'\n'"${UNWRAP_HITS}"
130
+ fi
131
+ EXPECT_HITS=$(ast_hits expect 5)
132
+ if [[ -n "$EXPECT_HITS" ]]; then
133
+ add_error ".expect() in $FILE_PATH — use ? or match on Result/Option"$'\n'"${EXPECT_HITS}"
134
+ fi
135
+
136
+ PANIC_HITS=$(ast_hits panic 3)
137
+ TODO_HITS=$(ast_hits todo 3)
138
+ UNIMPL_HITS=$(ast_hits unimplemented 3)
139
+ UNREACH_HITS=$(ast_hits unreachable 3)
140
+ if [[ -n "$PANIC_HITS" || -n "$TODO_HITS" || -n "$UNIMPL_HITS" || -n "$UNREACH_HITS" ]]; then
141
+ join_hits "$PANIC_HITS" "$TODO_HITS" "$UNIMPL_HITS" "$UNREACH_HITS"
142
+ add_error "panic!/todo!/unimplemented!/unreachable! in $FILE_PATH — return a Result instead"$'\n'"${JOINED}"
143
+ fi
144
+
145
+ rm -f "$ast_err_file"
146
+ if [[ "$ast_grep_failed" -ne 0 ]]; then
147
+ add_error "ast-grep failed while scanning $FILE_PATH — ${ast_grep_fail_detail:-unknown error}; error-handling rules could not be verified. Fix the tool/file and re-edit"
148
+ fi
149
+ fi
@@ -0,0 +1,139 @@
1
+ # --- No-mocks (zero tolerance): forbid mock frameworks — real data/fixtures only ---
2
+ # Split scope, because mock *definitions* live in src/ while mock *imports* are
3
+ # pulled into test files:
4
+ # - src/**/*.rs: #[automock], #[cfg_attr(..., automock)], mock! { ... }
5
+ # (also the mock!(...) and mock![...] delimiter forms — mock! is a macro,
6
+ # so all three bracket kinds are valid Rust).
7
+ # - test files (tests/ placement, or *_test.rs / *_tests.rs basenames — the
8
+ # same path predicate 20-tests.sh uses; its attribute-based detection
9
+ # (#[test], #[rstest], ...) is not reused here since mock imports are `use`
10
+ # statements, not attribute-marked items): use mockall::...;, use faux::...;.
11
+ # Same ast-grep-scan pattern as 60-error-handling.sh: ONE process, severity
12
+ # warning (a matching rule and a clean run both exit 0 — only a genuine crash
13
+ # is distinguishable from a clean pass), jq flattens every hit to one excerpt
14
+ # line, add_error on a match or on tool breakage.
15
+ #
16
+ # Opt-out: lang.rust.noMocks (default true) via quality_flag.
17
+ if [[ "$(quality_flag rust noMocks true)" == "true" ]] && command -v ast-grep >/dev/null 2>&1; then
18
+ _nm_in_src=0
19
+ [[ "$FILE_PATH" == */src/* ]] && _nm_in_src=1
20
+
21
+ _nm_is_test_file=0
22
+ [[ "$FILE_PATH" == */tests/* ]] && _nm_is_test_file=1
23
+ case "$(basename "$FILE_PATH")" in
24
+ *_test.rs|*_tests.rs) _nm_is_test_file=1 ;;
25
+ esac
26
+
27
+ NM_RULES=""
28
+ if [[ "$_nm_in_src" -eq 1 ]]; then
29
+ NM_RULES=$(cat <<'RS_NOMOCK_SRC'
30
+ id: automock-attr
31
+ language: rust
32
+ severity: warning
33
+ message: '#[automock] mock definition'
34
+ rule:
35
+ pattern: '#[automock]'
36
+ ---
37
+ id: automock-cfg-attr
38
+ language: rust
39
+ severity: warning
40
+ message: cfg_attr(..., automock) mock definition
41
+ rule:
42
+ pattern: '#[cfg_attr($$$, automock)]'
43
+ ---
44
+ id: mock-macro
45
+ language: rust
46
+ severity: warning
47
+ message: 'mock! { ... } mock definition'
48
+ rule:
49
+ any:
50
+ - pattern: 'mock! { $$$ }'
51
+ - pattern: 'mock!($$$)'
52
+ - pattern: 'mock![$$$]'
53
+ RS_NOMOCK_SRC
54
+ )
55
+ fi
56
+ if [[ "$_nm_is_test_file" -eq 1 ]]; then
57
+ NM_TEST_RULES=$(cat <<'RS_NOMOCK_TEST'
58
+ id: mockall-import
59
+ language: rust
60
+ severity: warning
61
+ message: 'use mockall::...; mock import'
62
+ rule:
63
+ kind: use_declaration
64
+ regex: 'mockall::'
65
+ ---
66
+ id: faux-import
67
+ language: rust
68
+ severity: warning
69
+ message: 'use faux::...; mock import'
70
+ rule:
71
+ kind: use_declaration
72
+ regex: 'faux::'
73
+ RS_NOMOCK_TEST
74
+ )
75
+ if [[ -n "$NM_RULES" ]]; then
76
+ NM_RULES="${NM_RULES}"$'\n---\n'"${NM_TEST_RULES}"
77
+ else
78
+ NM_RULES="$NM_TEST_RULES"
79
+ fi
80
+ fi
81
+
82
+ if [[ -n "$NM_RULES" ]]; then
83
+ nm_err_file="$(mktemp)"
84
+ nm_grep_failed=0
85
+ nm_fail_detail=""
86
+ nm_lines=""
87
+ nm_json=$(ast-grep scan --inline-rules "$NM_RULES" --json "$FILE_PATH" 2>"$nm_err_file")
88
+ nm_rc=$?
89
+ # Two distinct failure modes, reported distinctly: ast-grep itself broke, or
90
+ # it exited 0 and returned something that is not the documented JSON array.
91
+ if [[ "$nm_rc" -ne 0 || -s "$nm_err_file" ]]; then
92
+ nm_grep_failed=1
93
+ nm_stderr_first=$(head -n 1 "$nm_err_file" 2>/dev/null | cut -c1-200)
94
+ nm_fail_detail="ast-grep exit ${nm_rc}${nm_stderr_first:+: $nm_stderr_first}"
95
+ elif [[ -z "$(printf '%s' "$nm_json" | tr -d '[:space:]')" ]]; then
96
+ # ast-grep exited 0 with genuinely empty stdout — jq also exits 0 on
97
+ # empty input with empty output, so left unchecked this would be
98
+ # indistinguishable from a clean "[]" scan and silently swallow a real
99
+ # breakage as "no hits". A well-behaved scan always emits at least "[]".
100
+ nm_grep_failed=1
101
+ nm_fail_detail="ast-grep exited 0 with empty output (expected at least the JSON array \"[]\")"
102
+ elif ! nm_lines=$(printf '%s' "$nm_json" | jq -r '
103
+ .[] | . as $m
104
+ | ((($m.lines // "") | split("\n")) | to_entries[])
105
+ | "\($m.ruleId)\t\($m.file):\($m.range.start.line + 1 + .key):\(.value)"
106
+ ' 2>/dev/null); then
107
+ nm_grep_failed=1
108
+ nm_fail_detail="ast-grep exited 0 but its output did not parse as the documented JSON array"
109
+ nm_lines=""
110
+ fi
111
+
112
+ # nm_hits RULE_ID LIMIT — this rule's excerpt lines, capped.
113
+ nm_hits() {
114
+ local _id="$1" _limit="$2" _row _rest
115
+ [ -n "$nm_lines" ] || return 0
116
+ while IFS=$'\t' read -r _row _rest; do
117
+ [ "$_row" = "$_id" ] && printf '%s\n' "$_rest"
118
+ done <<< "$nm_lines" | head -n "$_limit"
119
+ }
120
+
121
+ AUTOMOCK_HITS=$(nm_hits automock-attr 5; nm_hits automock-cfg-attr 5)
122
+ if [[ -n "$AUTOMOCK_HITS" ]]; then
123
+ add_error "no-mocks: #[automock] mock definition in $FILE_PATH — write against real data/fixtures instead"$'\n'"${AUTOMOCK_HITS}"
124
+ fi
125
+ MOCK_MACRO_HITS=$(nm_hits mock-macro 5)
126
+ if [[ -n "$MOCK_MACRO_HITS" ]]; then
127
+ add_error "no-mocks: mock! { ... } mock definition in $FILE_PATH — write against real data/fixtures instead"$'\n'"${MOCK_MACRO_HITS}"
128
+ fi
129
+ MOCK_IMPORT_HITS=$(nm_hits mockall-import 5; nm_hits faux-import 5)
130
+ if [[ -n "$MOCK_IMPORT_HITS" ]]; then
131
+ add_error "no-mocks: mockall/faux import in $FILE_PATH — write against real data/fixtures instead"$'\n'"${MOCK_IMPORT_HITS}"
132
+ fi
133
+
134
+ rm -f "$nm_err_file"
135
+ if [[ "$nm_grep_failed" -ne 0 ]]; then
136
+ add_error "ast-grep failed while scanning $FILE_PATH — ${nm_fail_detail:-unknown error}; no-mocks rules could not be verified. Fix the tool/file and re-edit"
137
+ fi
138
+ fi
139
+ fi
@@ -0,0 +1,32 @@
1
+ # --- Docs (soft advisory, never blocks) ---
2
+ # A public API item in src/ should carry a concise /// doc comment. Advisory
3
+ # only — collected separately from MESSAGES so it never sets the failing gate.
4
+ DOC_ADVISORY=""
5
+ if [[ "$FILE_PATH" == */src/* && ! "$_is_rust_test" -eq 1 ]]; then
6
+ _undoc=$(awk '
7
+ /^[[:space:]]*$/ { next } # blanks do not reset the doc context
8
+ /^[[:space:]]*#\[/ { next } # attributes sit between doc and item
9
+ {
10
+ if ($0 ~ /^[[:space:]]*pub(\([^)]+\))?[[:space:]]+(fn|struct|enum|trait|const|static|type|mod)[[:space:]]/) {
11
+ if (prev !~ /^[[:space:]]*(\/\/\/|\/\/!)/) printf "%d: %s\n", NR, $0
12
+ }
13
+ prev=$0
14
+ }
15
+ ' "$FILE_PATH" 2>/dev/null | head -3)
16
+ if [[ -n "$_undoc" ]]; then
17
+ DOC_ADVISORY="Public items missing a /// doc comment in $FILE_PATH — add a concise one-line doc:"$'\n'"${_undoc}"
18
+ fi
19
+ # Concise cap: flag doc-comment runs that have grown long.
20
+ _verbose_doc=$(awk '
21
+ /^[[:space:]]*\/\/[\/!]/ { if (run==0) start=NR; run++; next }
22
+ { if (run>12) printf "%d: doc block is %d lines — trim to the essentials\n", start, run; run=0 }
23
+ END { if (run>12) printf "%d: doc block is %d lines — trim to the essentials\n", start, run }
24
+ ' "$FILE_PATH" 2>/dev/null | head -2)
25
+ if [[ -n "$_verbose_doc" ]]; then
26
+ # Separator appended on its own line: ANSI-C quoting is NOT honored inside a
27
+ # ${var:+word} expansion, so it cannot be inlined there.
28
+ [[ -n "$DOC_ADVISORY" ]] && DOC_ADVISORY="${DOC_ADVISORY}"$'\n'
29
+ DOC_ADVISORY="${DOC_ADVISORY}Verbose doc comment in $FILE_PATH — docs must be present but concise:"$'\n'"${_verbose_doc}"
30
+ fi
31
+ fi
32
+