@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,98 @@
1
+ #!/usr/bin/env bash
2
+ # normalize.sh — the jq vocabulary shared by collect-pr.sh and reduce-state.sh.
3
+ #
4
+ # Sourced. Every shape rule lives here so the collector, the reducer and the
5
+ # tests read one definition of "CI reviewer", "check state" and "thread
6
+ # comment". Programs are functions that print jq text (not variables) so a
7
+ # sourced-only lib stays shellcheck-clean without exporting large strings into
8
+ # every child process's environment.
9
+
10
+ # CI-reviewer login set: REST (`user.login`) keeps the `[bot]` suffix, GraphQL
11
+ # (`author.login`) drops it. Exact membership — never a `[bot]` substring test,
12
+ # which misreads the GraphQL form as human.
13
+ pb_ci_reviewers_json() { printf '%s' '["github-actions","github-actions[bot]","claude","claude[bot]"]'; }
14
+
15
+ # pb_jq_defs -> jq `def`s to prepend to any program.
16
+ pb_jq_defs() {
17
+ cat <<EOF
18
+ def ci_reviewers: $(pb_ci_reviewers_json);
19
+ def is_ci_reviewer: . as \$l | (ci_reviewers | index(\$l)) != null;
20
+ def check_state:
21
+ if .__typename == "StatusContext" then
22
+ (if .state == "SUCCESS" then "pass"
23
+ elif (.state == "PENDING" or .state == "EXPECTED") then "pending"
24
+ else "fail" end)
25
+ else
26
+ (if .status != "COMPLETED" then "pending"
27
+ elif (.conclusion == "SUCCESS" or .conclusion == "NEUTRAL" or .conclusion == "SKIPPED") then "pass"
28
+ else "fail" end)
29
+ end;
30
+ def check_name: (.name // .context // "unknown");
31
+ def check_url: (.detailsUrl // .targetUrl // null);
32
+ def ci_status:
33
+ (map(check_state)) as \$s
34
+ | if (\$s | length) == 0 then "pending"
35
+ elif (\$s | index("fail")) != null then "fail"
36
+ elif (\$s | index("pending")) != null then "pending"
37
+ else "pass" end;
38
+ def thread_comment: {id, databaseId, body, author: (.author.login // null), authorType: (.author.__typename // null), createdAt, url};
39
+ EOF
40
+ }
41
+
42
+ # Merge `gh api graphql --paginate --slurp` reviewThreads pages (array of page
43
+ # objects) into the snapshot thread shape.
44
+ pb_jq_threads_from_pages() {
45
+ pb_jq_defs
46
+ cat <<'EOF'
47
+ [.[].data.repository.pullRequest.reviewThreads.nodes[]]
48
+ | map({id, isResolved, isOutdated, path, line,
49
+ comments: (.comments.nodes | map(thread_comment)),
50
+ commentsHasNextPage: .comments.pageInfo.hasNextPage,
51
+ commentsEndCursor: .comments.pageInfo.endCursor})
52
+ EOF
53
+ }
54
+
55
+ # Merge the per-thread `node(id:)` comment pages (array of page objects).
56
+ pb_jq_thread_comments_from_pages() {
57
+ pb_jq_defs
58
+ printf '%s\n' '[.[].data.node.comments.nodes[]] | map(thread_comment)'
59
+ }
60
+
61
+ # Flatten `gh api --paginate --slurp` REST pages (array of arrays).
62
+ pb_jq_rest_items() { printf '%s\n' '[.[][]]'; }
63
+
64
+ pb_jq_issue_comment() {
65
+ printf '%s\n' '{id, body, author: (.user.login // null), authorType: (.user.type // null), createdAt: .created_at, updatedAt: .updated_at, url: .html_url}'
66
+ }
67
+
68
+ pb_jq_review() {
69
+ printf '%s\n' '{id, state, body, author: (.user.login // null), authorType: (.user.type // null), submittedAt: .submitted_at, url: .html_url, commitId: .commit_id}'
70
+ }
71
+
72
+ # Fields requested from `gh pr view --json`.
73
+ pb_pr_view_fields() {
74
+ printf '%s' 'number,title,url,author,state,baseRefName,headRefName,headRefOid,statusCheckRollup,mergeable,reviewDecision'
75
+ }
76
+
77
+ # GraphQL documents. Both declare $endCursor so `gh api graphql --paginate`
78
+ # drives the cursor; $pageSize keeps pagination testable on real PRs.
79
+ pb_gql_threads() {
80
+ cat <<'EOF'
81
+ query($owner:String!,$repo:String!,$number:Int!,$pageSize:Int!,$endCursor:String){
82
+ repository(owner:$owner,name:$repo){ pullRequest(number:$number){
83
+ reviewThreads(first:$pageSize,after:$endCursor){
84
+ pageInfo{hasNextPage endCursor}
85
+ nodes{ id isResolved isOutdated path line
86
+ comments(first:$pageSize){ pageInfo{hasNextPage endCursor}
87
+ nodes{ id databaseId body author{login __typename} createdAt url } } } } } } }
88
+ EOF
89
+ }
90
+
91
+ pb_gql_thread_comments() {
92
+ cat <<'EOF'
93
+ query($id:ID!,$pageSize:Int!,$endCursor:String){
94
+ node(id:$id){ ... on PullRequestReviewThread {
95
+ comments(first:$pageSize,after:$endCursor){ pageInfo{hasNextPage endCursor}
96
+ nodes{ id databaseId body author{login __typename} createdAt url } } } } }
97
+ EOF
98
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ # state.sh — load and update one slot's state file for the write-side scripts.
3
+ #
4
+ # Sourced after common.sh and lock.sh. The write side (reply-thread.sh,
5
+ # resolve-thread.sh, record.sh) always: takes the slot lock, validates the
6
+ # state, performs its action, and records the outcome atomically. This file
7
+ # holds the shared load/update so each script carries only its own action.
8
+
9
+ # pb_state_load STATE_FILE — validate (version 2) and export the slot
10
+ # identity: PB_STATE_REPO, PB_STATE_NUMBER, PB_STATE_HEAD. Exits through
11
+ # pb_fail on a missing or malformed file.
12
+ pb_state_load() {
13
+ local state_file="$1"
14
+ [ -f "$state_file" ] || pb_fail state_malformed "state file not found: $state_file (run babysit-tick.sh first)" '{"source":"state"}'
15
+ pb_json_valid "$state_file" || pb_fail state_malformed "state file is not valid JSON: $state_file" '{"source":"state"}'
16
+ jq -e '.version == 2 and (.repo | type == "string") and (.number | type == "number")' "$state_file" >/dev/null 2>&1 \
17
+ || pb_fail state_malformed "state file is not a version-2 pr-babysit state: $state_file" "$(jq -c '{source:"state", version:(.version // null)}' "$state_file")"
18
+ PB_STATE_REPO=$(jq -r '.repo' "$state_file")
19
+ PB_STATE_NUMBER=$(jq -r '.number' "$state_file")
20
+ # These are spliced into REST paths: refuse anything but owner/name and an
21
+ # integer, whatever a hand-edited state file says.
22
+ # GitHub owner/name: no leading dot, so `.`/`..` segments cannot form.
23
+ [[ "$PB_STATE_REPO" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*/[A-Za-z0-9_][A-Za-z0-9._-]*$ ]] \
24
+ || pb_fail state_malformed "state file repo is not owner/name: $PB_STATE_REPO" '{"source":"state"}'
25
+ [[ "$PB_STATE_NUMBER" =~ ^[0-9]+$ ]] \
26
+ || pb_fail state_malformed "state file number is not an integer: $PB_STATE_NUMBER" '{"source":"state"}'
27
+ PB_STATE_HEAD=$(jq -r '.pr.headSha // ""' "$state_file")
28
+ export PB_STATE_REPO PB_STATE_NUMBER PB_STATE_HEAD
29
+ }
30
+
31
+ # pb_state_update STATE_FILE FILTER [JQ_ARGS...] — apply a jq filter to the
32
+ # state and write it back atomically. The filter must yield the whole state.
33
+ pb_state_update() {
34
+ local state_file="$1" filter="$2"; shift 2
35
+ pb_atomic_write_json "$state_file" jq -c "$@" "$filter" "$state_file" \
36
+ || pb_fail state_malformed "could not update $state_file" '{"source":"state"}'
37
+ }
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # parse-verdict.sh — turn the CI review bot's issue comment (stdin) into a
3
+ # deterministic JSON verdict the babysit loop acts on. Keeps the parsing OUT of
4
+ # the LLM prompt so behaviour is testable (scripts/__tests__/parse-verdict.bats
5
+ # against a captured real comment) and stable.
6
+ #
7
+ # stdin : raw comment body (markdown)
8
+ # stdout: { is_review_comment, state, complete, verdict, verdict_label, findings[], must_fix[] }
9
+ # state: in_progress | complete | unknown
10
+ # - unknown → no checkbox checklist found (cannot judge completeness; the
11
+ # caller degrades to GitHub check-conclusion behaviour)
12
+ # - in_progress → ≥1 unchecked `- [ ]` (review still running; do not act)
13
+ # - complete → ≥1 checkbox AND none unchecked
14
+ # findings[]: { path, line|null, severity, text, key } (key = path:line:sha1(text)[:8])
15
+ # must_fix[]: free-text lines from `### Top-N must-fix`
16
+ #
17
+ # state can also be "provider_error": the action posted a comment but the model
18
+ # never produced a usable review — "Review incomplete — provider error", every
19
+ # file marked unreviewed. That is NOT a verdict about the code, and the check
20
+ # still reports success, so a caller that reads only `verdict` sees a plain
21
+ # `changes` with no findings and cannot tell "reviewed, nothing actionable"
22
+ # from "never reviewed". complete is false for this state.
23
+ #
24
+ # must_fix is NOT a duplicate of findings. The bot populates the two sections
25
+ # independently, and they disagree in both directions: a review has reported
26
+ # `Findings (0)` while Top-N listed three actionable items, and an `approved`
27
+ # verdict has shipped with Top-N still populated. A caller that reads only
28
+ # findings[] therefore both misses work and over-trusts a pass. These are plain
29
+ # sentences, not `path:line` findings, so they carry no key and cannot be
30
+ # resolved as threads — surface them to a human rather than acting on them
31
+ # mechanically.
32
+ #
33
+ # Identification is by MARKER, robust to both header states the bot uses
34
+ # ("PR Review in Progress" → "Code Review —"): a CI job link, a "Code Review"
35
+ # header, or an "agent-merge" verdict label. No marker → is_review_comment:false.
36
+ set -o pipefail
37
+
38
+ command -v jq >/dev/null 2>&1 || { echo "parse-verdict.sh: jq required" >&2; exit 2; }
39
+
40
+ input=$(cat 2>/dev/null || true)
41
+
42
+ _empty() { jq -nc '{is_review_comment:false, state:"unknown", complete:false, verdict:"none", verdict_label:"", findings:[]}'; }
43
+
44
+ # Empty / unreadable stdin → not a review comment.
45
+ [ -n "${input//[[:space:]]/}" ] || { _empty; exit 0; }
46
+
47
+ # --- Identify: marker-based, both states ---
48
+ # Precondition: the babysit pre-filters comments to the CI Toolu Code Review
49
+ # poster (`github-actions[bot]`; legacy `claude[bot]` still accepted) before calling
50
+ # this; the markers below are anchored to the bot's own structures
51
+ # (review headings, the [View job] CI link, a backticked agent-merge label) so a
52
+ # stray "actions/runs/…" or "agent-merge-…" in arbitrary prose won't false-positive.
53
+ is_review=false
54
+ if printf '%s' "$input" | grep -qE '^### Code Review|^### PR Review in Progress|\[View job\]\([^)]*actions/runs/[0-9]+|`agent-merge-[a-z]|^`(merge-approved|request-changes)`$'; then
55
+ is_review=true
56
+ fi
57
+ if [ "$is_review" != true ]; then _empty; exit 0; fi
58
+
59
+ # --- Completeness: checkbox state only ---
60
+ unchecked=$(printf '%s\n' "$input" | grep -cE '^[[:space:]]*-[[:space:]]\[[[:space:]]\]' || true)
61
+ checked=$(printf '%s\n' "$input" | grep -cE '^[[:space:]]*-[[:space:]]\[[xX]\]' || true)
62
+ boxes=$((unchecked + checked))
63
+ if [ "$boxes" -eq 0 ]; then state="unknown"
64
+ elif [ "$unchecked" -gt 0 ]; then state="in_progress"
65
+ else state="complete"
66
+ fi
67
+ complete=false; [ "$state" = complete ] && complete=true
68
+
69
+ # --- Verdict --- the machine-readable label is AUTHORITATIVE.
70
+ # Don't infer from prose when a label exists: finding TEXT routinely discusses
71
+ # "Changes requested"/"approved" (e.g. a finding about verdict parsing itself),
72
+ # and a whole-body grep would misclassify an approved PR as "changes". Fall back
73
+ # to prose only when no label is present (changes-first there, as the safe bias).
74
+ #
75
+ # The label has two historical shapes and BOTH must parse:
76
+ # legacy (<=PR#31): `agent-merge-approved` / `agent-merge-blocked`
77
+ # current: `merge-approved` / `request-changes`
78
+ # Resolution order matters. The checklist line `- [x] Set verdict label (`X`)`
79
+ # is preferred because it is emitted before the findings section, so a finding
80
+ # that quotes a label verbatim cannot shadow it. Next the standalone trailing
81
+ # label chip (current shape emits only that). Only then a bare legacy scan.
82
+ verdict_label=$(printf '%s\n' "$input" \
83
+ | grep -oE 'Set verdict label \(`[^`]+`\)' | head -1 | grep -oE '`[^`]+`' | tr -d '`' || true)
84
+ if [ -z "$verdict_label" ]; then
85
+ verdict_label=$(printf '%s\n' "$input" \
86
+ | grep -oE '^`(agent-merge-[a-z-]+|merge-approved|request-changes)`$' | tail -1 | tr -d '`' || true)
87
+ fi
88
+ if [ -z "$verdict_label" ]; then
89
+ verdict_label=$(printf '%s' "$input" | grep -oE 'agent-merge-[a-z-]+' | head -1 || true)
90
+ fi
91
+
92
+ # `merge-approved` contains "approved"; `request-changes` contains "changes";
93
+ # `agent-merge-blocked` contains "blocked". Test approved first so the shared
94
+ # substring "merge" in either label can never decide the branch.
95
+ if [[ "$verdict_label" == *approved* ]]; then
96
+ verdict="approved"
97
+ elif [[ "$verdict_label" == *blocked* || "$verdict_label" == *changes* ]]; then
98
+ verdict="changes"
99
+ else
100
+ # No label. Prefer the single `**Verdict:**` summary line over a whole-body
101
+ # grep: the current bot bolds the KEY (`**Verdict:** ✅ Approved`), not the
102
+ # word, so the legacy `**Approved**` pattern never matches it.
103
+ vline=$(printf '%s\n' "$input" | grep -m1 -E '^\*\*Verdict:\*\*' || true)
104
+ if printf '%s' "$vline" | grep -qiE 'changes requested'; then verdict="changes"
105
+ elif printf '%s' "$vline" | grep -qiE 'approved'; then verdict="approved"
106
+ elif printf '%s' "$input" | grep -qiE '\*\*Changes requested\*\*|changes-requested'; then verdict="changes"
107
+ elif printf '%s' "$input" | grep -qiE '\*\*Approved\*\*'; then verdict="approved"
108
+ else verdict="none"
109
+ fi
110
+ fi
111
+
112
+ # --- Provider error: the action ran, the model did not. Detected before the
113
+ # findings block because there is nothing to find — the review never happened,
114
+ # and reporting its absence as a normal `changes` verdict is what lets a
115
+ # transient failure look like a considered judgement.
116
+ verdict_line=$(printf '%s\n' "$input" | grep -m1 -E '^\*\*Verdict:\*\*' || true)
117
+ if printf '%s' "$verdict_line" | grep -qiE 'review incomplete|provider error'; then
118
+ state="provider_error"
119
+ complete=false
120
+ fi
121
+
122
+ # --- Findings: only the `### Findings` … next `### ` block, lines of the form
123
+ # `path[:line]`: severity: text
124
+ _sha1() { (sha1sum 2>/dev/null || shasum 2>/dev/null || echo nohash) | cut -c1-8; }
125
+ # Tolerate a decorated header (`### Findings`, `### Findings (6)`) — exact-match
126
+ # would miss a count suffix and silently report zero findings.
127
+ findings_block=$(printf '%s\n' "$input" | awk '/^### Findings([[:space:]]|$)/{f=1;next} /^### /{f=0} f')
128
+ findings_json="[]"
129
+ while IFS= read -r line; do
130
+ [[ "$line" =~ ^\`([^\`]+)\`:\ (blocker|high|medium|low|nit):\ (.*)$ ]] || continue
131
+ raw_path="${BASH_REMATCH[1]}"; sev="${BASH_REMATCH[2]}"; text="${BASH_REMATCH[3]}"
132
+ if [[ "$raw_path" =~ ^(.+):([0-9]+)$ ]]; then path="${BASH_REMATCH[1]}"; ln="${BASH_REMATCH[2]}"; else path="$raw_path"; ln=""; fi
133
+ h=$(printf '%s' "$text" | _sha1)
134
+ key="${path}:${ln}:${h}"
135
+ obj=$(jq -nc --arg path "$path" --arg line "$ln" --arg severity "$sev" --arg text "$text" --arg key "$key" \
136
+ '{path:$path, line:(if $line=="" then null else ($line|tonumber) end), severity:$severity, text:$text, key:$key}') || continue
137
+ findings_json=$(jq -c --argjson o "$obj" '. + [$o]' <<<"$findings_json")
138
+ done <<< "$findings_block"
139
+
140
+ # --- Top-N must-fix: free-text lines until the next heading or <details>.
141
+ # Same decorated-header tolerance as Findings above; entries may be bare lines
142
+ # or markdown list items.
143
+ must_fix_block=$(printf '%s\n' "$input" | awk 'BEGIN{IGNORECASE=1} /^###[[:space:]]+Top-N[[:space:]]+must-fix([[:space:]]|$)/{f=1;next} /^### |^<details>/{f=0} f')
144
+ must_fix_json="[]"
145
+ while IFS= read -r line; do
146
+ line="${line#"${line%%[![:space:]]*}"}"
147
+ line="${line%"${line##*[![:space:]]}"}"
148
+ [ -n "$line" ] || continue
149
+ # Strip a leading list marker only — "- " or "1. ". A blanket "first word"
150
+ # strip ate the first word of every unmarked line ("Fix the silent
151
+ # swallowing..." became "the silent swallowing...").
152
+ line="${line#- }"
153
+ line="${line#\* }"
154
+ [[ "$line" =~ ^[0-9]+\.[[:space:]]+(.*)$ ]] && line="${BASH_REMATCH[1]}"
155
+ [ -n "$line" ] || continue
156
+ if ! next_json=$(jq -c --arg t "$line" '. + [$t]' <<<"$must_fix_json"); then
157
+ printf 'parse-verdict: could not record a Top-N line; dropping it: %s\n' "$line" >&2
158
+ continue
159
+ fi
160
+ must_fix_json="$next_json"
161
+ done <<< "$must_fix_block"
162
+
163
+ jq -nc \
164
+ --argjson is_review "$is_review" \
165
+ --arg state "$state" \
166
+ --argjson complete "$complete" \
167
+ --arg verdict "$verdict" \
168
+ --arg verdict_label "${verdict_label:-}" \
169
+ --argjson findings "$findings_json" \
170
+ --argjson must_fix "$must_fix_json" \
171
+ '{is_review_comment:$is_review, state:$state, complete:$complete, verdict:$verdict, verdict_label:$verdict_label, findings:$findings, must_fix:$must_fix}'
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # record.sh — hand the agent's decisions back to the reducer.
3
+ #
4
+ # Usage: record.sh flag-injection --state-file <path> --thread <graphqlId>
5
+ # record.sh round --state-file <path> --had-rejection true|false [--fix-pushed]
6
+ # record.sh status --state-file <path> --status complete|escalated|cancelled
7
+ #
8
+ # flag-injection the reducer drops the thread from actionable and from the
9
+ # resolution audit, and lists it under flaggedInjection.
10
+ # round end of Step 4, before the push: this round's finding keys
11
+ # become lastRoundFindingKeys, lastRoundHadRejection is set,
12
+ # and --fix-pushed bumps fixAttempts (cap 5). Recurrence and
13
+ # fix budgets therefore advance on real rounds, never polls.
14
+ # status the workflow's terminal transition; the reducer only ever
15
+ # recommends through `decision`.
16
+ # Every subcommand takes the slot lock, validates the state, writes atomically
17
+ # and prints {ok:true,...}. Exit: 0 · 2 usage · 3 state_malformed · 75 locked.
18
+ set -euo pipefail
19
+
20
+ PB_SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
21
+ # shellcheck source=lib/common.sh
22
+ . "$PB_SCRIPT_DIR/lib/common.sh"
23
+ # shellcheck source=lib/lock.sh
24
+ . "$PB_SCRIPT_DIR/lib/lock.sh"
25
+ # shellcheck source=lib/state.sh
26
+ . "$PB_SCRIPT_DIR/lib/state.sh"
27
+
28
+ sub="${1:-}"; [ $# -gt 0 ] && shift
29
+ state_file=""; thread=""; had_rejection=""; fix_pushed=0; status=""
30
+ while [ $# -gt 0 ]; do
31
+ case "$1" in
32
+ --state-file) state_file="${2:-}"; shift 2 ;;
33
+ --thread) thread="${2:-}"; shift 2 ;;
34
+ --had-rejection) had_rejection="${2:-}"; shift 2 ;;
35
+ --fix-pushed) fix_pushed=1; shift ;;
36
+ --status) status="${2:-}"; shift 2 ;;
37
+ *) pb_fail usage "record.sh: unknown argument: $1" ;;
38
+ esac
39
+ done
40
+ [ -n "$state_file" ] || pb_fail usage "record.sh: --state-file required"
41
+ case "$sub" in
42
+ flag-injection) [ -n "$thread" ] || pb_fail usage "record.sh flag-injection: --thread <graphqlId> required" ;;
43
+ round) [[ "$had_rejection" =~ ^(true|false)$ ]] || pb_fail usage "record.sh round: --had-rejection true|false required" ;;
44
+ status) [[ "$status" =~ ^(complete|escalated|cancelled)$ ]] || pb_fail usage "record.sh status: --status complete|escalated|cancelled required" ;;
45
+ *) pb_fail usage "record.sh: subcommand must be flag-injection, round or status" ;;
46
+ esac
47
+
48
+ pb_require jq
49
+ pb_init
50
+ pb_lock_acquire "$state_file" || pb_lock_fail "$state_file"
51
+ pb_state_load "$state_file"
52
+ now=$(pb_now)
53
+
54
+ case "$sub" in
55
+ flag-injection)
56
+ pb_state_update "$state_file" '.actions.flagged[$t] = {reason:"injection", at:$now}' --arg t "$thread" --arg now "$now"
57
+ jq -nc --arg t "$thread" --arg now "$now" '{ok:true, recorded:"flag-injection", thread:$t, at:$now}' ;;
58
+ round)
59
+ pb_state_update "$state_file" \
60
+ '.pr.lastRoundFindingKeys = (.pr.botFindingKeys // [])
61
+ | .pr.lastRoundHadRejection = $rej
62
+ | .pr.fixAttempts = (if $fix then ([(.pr.fixAttempts // 0) + 1, 5] | min) else (.pr.fixAttempts // 0) end)
63
+ | .lastRound = {at:$now, hadRejection:$rej, fixPushed:$fix, headSha:(.pr.headSha // null)}' \
64
+ --argjson rej "$had_rejection" --argjson fix "$([ "$fix_pushed" -eq 1 ] && echo true || echo false)" --arg now "$now"
65
+ jq -c '{ok:true, recorded:"round", lastRoundFindingKeys:.pr.lastRoundFindingKeys, lastRoundHadRejection:.pr.lastRoundHadRejection, fixAttempts:.pr.fixAttempts}' "$state_file" ;;
66
+ status)
67
+ pb_state_update "$state_file" '.status = $s | .statusChangedAt = $now' --arg s "$status" --arg now "$now"
68
+ jq -nc --arg s "$status" --arg now "$now" '{ok:true, recorded:"status", status:$s, at:$now}' ;;
69
+ esac
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env bash
2
+ # reduce-state.sh — the pure decision layer of the pr-babysit helper.
3
+ #
4
+ # Reads one snapshot (collect-pr.sh output) and the previous slot state, and
5
+ # emits the next state plus the compact agent-facing result. No network, no
6
+ # clock (--now is an input), no side effects beyond the two output files, so
7
+ # every decision rule below is testable against captured real snapshots.
8
+ #
9
+ # Usage: reduce-state.sh --snapshot <path> --state <path> --now <iso8601>
10
+ # [--state-out <path>] [--result-out <path>]
11
+ # [--state-path <path>] [--snapshot-path <path>]
12
+ # --state previous state file; absent file = first tick
13
+ # --state-out where to write the next state (skipped when omitted)
14
+ # --result-out where to write the result (stdout when omitted)
15
+ # --state-path / --snapshot-path the paths reported inside the result
16
+ #
17
+ # The rules encoded here are the workflow's (workflows/babysit.md): the Step 1
18
+ # actionable filter and resolution audit, the Step 4 recurrence gate and the
19
+ # Step 6 stop conditions. Reason and error codes are closed sets — see the spec
20
+ # docs/toolu/specs/2026-09-19-pr-babysit-helper-design.md.
21
+ # Exit: 0 result produced · 2 usage · 3 state_malformed | slot_mismatch | invalid_json.
22
+ set -euo pipefail
23
+
24
+ PB_SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
25
+ # shellcheck source=lib/common.sh
26
+ . "$PB_SCRIPT_DIR/lib/common.sh"
27
+ # shellcheck source=lib/normalize.sh
28
+ . "$PB_SCRIPT_DIR/lib/normalize.sh"
29
+
30
+ snapshot=""; state=""; now=""; state_out=""; result_out=""; state_path=""; snapshot_path=""
31
+ while [ $# -gt 0 ]; do
32
+ case "$1" in
33
+ --snapshot) snapshot="${2:-}"; shift 2 ;;
34
+ --state) state="${2:-}"; shift 2 ;;
35
+ --now) now="${2:-}"; shift 2 ;;
36
+ --state-out) state_out="${2:-}"; shift 2 ;;
37
+ --result-out) result_out="${2:-}"; shift 2 ;;
38
+ --state-path) state_path="${2:-}"; shift 2 ;;
39
+ --snapshot-path) snapshot_path="${2:-}"; shift 2 ;;
40
+ *) pb_fail usage "reduce-state.sh: unknown argument: $1" ;;
41
+ esac
42
+ done
43
+ [ -n "$snapshot" ] || pb_fail usage "reduce-state.sh: --snapshot <path> required"
44
+ [ -n "$state" ] || pb_fail usage "reduce-state.sh: --state <path> required"
45
+ [[ "$now" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]] || pb_fail usage "reduce-state.sh: --now must be an ISO-8601 UTC timestamp (YYYY-MM-DDTHH:MM:SSZ)"
46
+ [ -n "$state_path" ] || state_path="$state"
47
+ [ -n "$snapshot_path" ] || snapshot_path="$snapshot"
48
+ pb_require jq
49
+
50
+ # --- validate inputs (fail closed, never write) ------------------------------
51
+ [ -f "$snapshot" ] || pb_fail invalid_json "reduce-state.sh: snapshot not found: $snapshot" '{"source":"snapshot"}'
52
+ pb_json_valid "$snapshot" || pb_fail invalid_json "reduce-state.sh: snapshot is not valid JSON: $snapshot" '{"source":"snapshot"}'
53
+ jq -e '.version == 1 and (.repo | type == "string") and (.number | type == "number") and (.head.sha | type == "string") and (.pr | type == "object") and (.threads | type == "array")' "$snapshot" >/dev/null 2>&1 \
54
+ || pb_fail invalid_json "reduce-state.sh: snapshot is not a version-1 pr-babysit snapshot" '{"source":"snapshot"}'
55
+
56
+ prev_json="null"
57
+ if [ -e "$state" ]; then
58
+ pb_json_valid "$state" || pb_fail state_malformed "reduce-state.sh: state file is not valid JSON: $state" '{"source":"state"}'
59
+ jq -e '.version == 2' "$state" >/dev/null 2>&1 \
60
+ || pb_fail state_malformed "reduce-state.sh: state file is not version 2: $state" "$(jq -c '{source:"state", version: (.version // null)}' "$state")"
61
+ if ! jq -e --slurpfile s "$snapshot" '.repo == $s[0].repo and .number == $s[0].number' "$state" >/dev/null 2>&1; then
62
+ pb_fail slot_mismatch "reduce-state.sh: state belongs to $(jq -r '"\(.repo)#\(.number)"' "$state"), snapshot is $(jq -r '"\(.repo)#\(.number)"' "$snapshot")" \
63
+ "$(jq -c --slurpfile s "$snapshot" '{source:"state", state:{repo, number}, snapshot:{repo:$s[0].repo, number:$s[0].number}}' "$state")"
64
+ fi
65
+ prev_json=$(cat "$state")
66
+ fi
67
+
68
+ # --- the reducer -------------------------------------------------------------
69
+ pb_jq_reduce() {
70
+ pb_jq_defs
71
+ cat <<'EOF'
72
+ # ---- injection heuristics (advisory only): the agent keeps the decision.
73
+ def injection_pattern:
74
+ . as $b
75
+ | [ "ignore (all |any |the )?(previous|prior|above|earlier) (instructions|prompts?|rules)",
76
+ "disregard (all |any |the |your )?(previous|prior|system|above) ",
77
+ "you are (now )?(an? )?(ai|assistant|llm|language model|claude|codex|copilot)",
78
+ "(^|\\n)\\s*(system|assistant)\\s*:",
79
+ "<(system|instructions?)>",
80
+ "(run|execute) (the following|this|these) (command|shell|script)" ]
81
+ | map(. as $p | select($b | test($p; "i"))) | first // null;
82
+ # author_class: ci_reviewer (by exact login) | bot (GraphQL Bot / REST type) | human.
83
+ def author_class: if (.author | is_ci_reviewer) then "ci_reviewer" elif .authorType == "Bot" then "bot" else "human" end;
84
+ # ---- inputs
85
+ $snap as $snap | $prev as $prev
86
+ | ($snap.pr) as $pr
87
+ | ($snap.head.sha) as $head
88
+ | ($pr.author) as $author
89
+ | (($snap.repo | ascii_downcase | gsub("/"; "-")) + "-" + ($snap.number | tostring)) as $slot
90
+ | ($snap.repo + "#" + ($snap.number | tostring)) as $key
91
+ | (if $prev == null then {replied:{}, resolved:{}, flagged:{}} else ($prev.actions // {replied:{}, resolved:{}, flagged:{}}) end) as $actions
92
+ | ($actions.flagged | keys) as $flagged
93
+ | ($actions.replied | keys) as $repliedKeys
94
+ # ---- CI
95
+ | ($pr.statusCheckRollup // []) as $rollup
96
+ | ($rollup | ci_status) as $ciStatus
97
+ | ($rollup | map({name: check_name, status: check_state, url: check_url})) as $checks
98
+ # ---- verdict
99
+ | ($snap.bot.verdict // {}) as $v
100
+ | ($snap.bot.comment) as $botComment
101
+ | ($v.state // "absent") as $botState
102
+ | ($v.verdict // "none") as $botVerdict
103
+ | (($v.findings // []) | map(.key)) as $keys
104
+ | (($v.findings // []) | length) as $findingsCount
105
+ | ($botState == "absent" or $botState == "unknown" or ($v.is_review_comment == false)) as $degraded
106
+ | (if $botState == "absent" then "review_absent" elif $degraded then "review_unknown_format" else null end) as $degradedReason
107
+ | ($prev != null and $botComment != null and ($prev.pr.botCommentId // null) == $botComment.id and ($prev.pr.botCommentUpdatedAt // null) == $botComment.updatedAt) as $sameRun
108
+ # ---- thread classification
109
+ | ($snap.threads | map(
110
+ . as $t
111
+ | ($t.comments | last) as $lastComment
112
+ | ($t.comments | map(select(.author != $author)) | last) as $lastNonAuthor
113
+ | ($flagged | index($t.id) != null) as $isFlagged
114
+ | (if $lastNonAuthor == null then "none" else ($lastNonAuthor | author_class) end) as $class
115
+ | ($t.isResolved == false) as $open
116
+ | ($open and ($isFlagged | not) and $lastComment != null and $lastComment.author != $author and ($class == "human" or $class == "ci_reviewer")) as $answerable
117
+ | (if $t.isOutdated then ($answerable and $class == "human") else $answerable end) as $actionable
118
+ | ($open and ($t.isOutdated | not) and ($isFlagged | not)) as $audited
119
+ | { id: $t.id, path: $t.path, line: $t.line, isOutdated: $t.isOutdated, isResolved: $t.isResolved,
120
+ rootCommentId: ($t.comments | first | .databaseId // null),
121
+ inReplyTo: ($lastNonAuthor.databaseId // null),
122
+ authorClass: $class,
123
+ lastCommentAuthor: ($lastComment.author // null),
124
+ lastCommentAt: ($lastComment.createdAt // null),
125
+ injectionSuspect: ((($lastNonAuthor.body // "") | injection_pattern) != null),
126
+ injectionPattern: (($lastNonAuthor.body // "") | injection_pattern),
127
+ comments: $t.comments,
128
+ flags: {actionable: $actionable, audited: $audited, flagged: $isFlagged,
129
+ skippedOutdated: ($open and $t.isOutdated and $class == "ci_reviewer"),
130
+ replied: ($repliedKeys | index("thread:" + $t.id + "@" + (($lastNonAuthor.databaseId // 0) | tostring)) != null)} }
131
+ )) as $threads
132
+ | ($threads | map(select(.flags.actionable)) | map(del(.flags, .isResolved))) as $actionable
133
+ | ($threads | map(select(.flags.audited))) as $audited
134
+ | ($audited | map(select(.flags.actionable | not)) | map({id, path, line, repliedAt: .lastCommentAt, lastCommentAuthor})) as $staleUnresolved
135
+ | ($threads | map(select(.flags.skippedOutdated)) | map(.id)) as $skippedOutdated
136
+ | ($threads | map(select(.flags.flagged)) | map(.id)) as $flaggedInjection
137
+ | ($audited | length) as $unresolved
138
+ # ---- conversation and review-level comments
139
+ | (($snap.comments // []) | map(select(.author != $author and .authorType != "Bot"))
140
+ | map(. as $c | select(($snap.comments | map(select(.author == $author and .createdAt > $c.createdAt)) | length) == 0))
141
+ | map(. as $c | select(($repliedKeys | index("conversation:" + ($c.id | tostring))) == null))
142
+ | map({id, author, body, createdAt, url})) as $convActionable
143
+ | (($snap.reviews // []) | map(select(.author != $author and .authorType != "Bot" and .state != "APPROVED" and ((.body // "") | length) > 0))
144
+ | map(. as $r | select(($repliedKeys | index("review:" + ($r.id | tostring))) == null))
145
+ | map({id, author, state, body, submittedAt, url})) as $reviewActionable
146
+ # ---- recurrence (Step 4 gate) — only a NEW verdict run can recur
147
+ | (if $prev == null then [] else ($prev.pr.lastRoundFindingKeys // []) end) as $lastRoundKeys
148
+ | (if $prev == null then false else ($prev.pr.lastRoundHadRejection // false) end) as $lastRoundHadRejection
149
+ | (if $prev == null then 0 else ($prev.pr.recurrenceStreak // 0) end) as $prevStreak
150
+ | (if $prev == null then 0 else ($prev.pr.fixAttempts // 0) end) as $fixAttempts
151
+ | (if $sameRun or $prev == null then [] else ($keys | map(select(. as $k | $lastRoundKeys | index($k) != null))) end) as $recurringKeys
152
+ | (if $sameRun then $prevStreak elif ($recurringKeys | length) > 0 then ($prevStreak + 1) else 0 end) as $streak
153
+ # ---- change detection + backoff
154
+ | ({ciStatus: $ciStatus, reviewDecision: $pr.reviewDecision, mergeable: $pr.mergeable, unresolvedThreads: $unresolved,
155
+ headSha: $head, botVerdict: $botVerdict, botState: $botState, botFindingKeys: $keys}) as $cmp
156
+ | ($prev == null or ($prev.pr | {ciStatus, reviewDecision, mergeable, unresolvedThreads, headSha, botVerdict, botState, botFindingKeys}) != $cmp) as $changed
157
+ | (if $changed then 0 else (($prev.idleStreak // 0) + 1) end) as $idleStreak
158
+ | (if $idleStreak >= 9 then 15 elif $idleStreak >= 6 then 12 elif $idleStreak >= 3 then 6 elif $ciStatus == "fail" then 1 else 3 end) as $intervalMinutes
159
+ | (if $idleStreak >= 6 then 60 elif $idleStreak >= 3 then 30 else 15 end) as $waitSeconds
160
+ # ---- decision (Step 6)
161
+ | ($prev != null and $botState == "provider_error" and ($prev.pr.botState // "") == "provider_error" and ($prev.pr.headSha // "") == $head) as $providerErrorRepeated
162
+ | ([ (if $pr.state == "MERGED" then {code:"pr_merged", detail:"PR is merged"} else empty end),
163
+ (if $pr.state == "CLOSED" then {code:"pr_closed", detail:"PR is closed"} else empty end),
164
+ (if $pr.mergeable == "CONFLICTING" then {code:"merge_conflict", detail:"mergeable is CONFLICTING"} else empty end),
165
+ (if $fixAttempts >= 5 then {code:"fix_attempts_exhausted", detail:"\($fixAttempts) fix attempts recorded"} else empty end),
166
+ (if ($recurringKeys | length) > 0 and $lastRoundHadRejection then {code:"recurrence_after_rejection", detail:"\($recurringKeys | length) finding key(s) recurred after a Won't-fix round"} else empty end),
167
+ (if ($recurringKeys | length) > 0 and $streak >= 2 then {code:"recurrence_streak", detail:"finding keys recurred on \($streak) consecutive rounds"} else empty end),
168
+ (if $providerErrorRepeated then {code:"provider_error_repeated", detail:"review provider error twice on head \($head[0:8])"} else empty end)
169
+ ]) as $escalations
170
+ | ([ (if $ciStatus == "pass" then {code:"ci_pass", detail:"\($checks | length) check(s) passed"}
171
+ elif $ciStatus == "fail" then {code:"ci_failed", detail:($checks | map(select(.status == "fail")) | map(.name) | join(", "))}
172
+ else {code:"ci_pending", detail:(if ($checks | length) == 0 then "no checks reported yet" else ($checks | map(select(.status == "pending")) | map(.name) | join(", ")) end)} end),
173
+ (if $unresolved == 0 then {code:"threads_clear", detail:"no unresolved review threads"} else empty end),
174
+ (if ($actionable | length) > 0 then {code:"threads_unresolved", detail:"\($actionable | length) actionable thread(s)"} else empty end),
175
+ (if ($staleUnresolved | length) > 0 then {code:"threads_stale_unresolved", detail:"\($staleUnresolved | length) replied-but-unresolved thread(s)"} else empty end),
176
+ (if $botState == "in_progress" then {code:"review_in_progress", detail:"review bot still running"}
177
+ elif $botState == "provider_error" and ($providerErrorRepeated | not) then {code:"provider_error", detail:"review bot reported a provider error; rerun the review job once"}
178
+ elif $botState == "complete" and $botVerdict == "approved" and $findingsCount == 0 then {code:"review_approved", detail:"bot verdict approved with zero findings"}
179
+ elif $botState == "complete" then {code:"review_changes", detail:"bot verdict \($botVerdict) with \($findingsCount) finding(s)"}
180
+ elif $degraded then {code:$degradedReason, detail:"bot verdict cannot be read"} else empty end),
181
+ (if $degraded then {code:"manual_verify", detail:"verify review findings manually: \($botComment.url // "no bot comment")"} else empty end),
182
+ (if $pr.mergeable == "UNKNOWN" and $pr.state == "OPEN" then {code:"mergeable_unknown", detail:"GitHub has not computed mergeability yet"} else empty end),
183
+ (if $changed then empty else {code:"unchanged", detail:"nothing changed since the last tick"} end)
184
+ ]) as $signals
185
+ | (($escalations | length) > 0) as $escalate
186
+ | ($pr.state == "OPEN" and $ciStatus == "pass" and $unresolved == 0 and $pr.mergeable != "UNKNOWN"
187
+ and ( ($botState == "complete" and $botVerdict == "approved" and $findingsCount == 0) or $degraded )) as $successReady
188
+ | (if $escalate then "escalate" elif $successReady then "success" else "keep_going" end) as $decision
189
+ | ($escalations + $signals) as $reasons
190
+ # ---- outputs
191
+ | {
192
+ state: {
193
+ version: 2, slot: $slot, repo: $snap.repo, number: $snap.number,
194
+ cronName: ("pr-babysit:" + $slot),
195
+ lastUpdate: $now,
196
+ totalTicks: ((if $prev == null then 0 else ($prev.totalTicks // 0) end) + 1),
197
+ idleStreak: $idleStreak, currentInterval: $intervalMinutes, waitSeconds: $waitSeconds,
198
+ status: (if $prev == null then "active" else ($prev.status // "active") end),
199
+ worktree: (if $prev == null then null else ($prev.worktree // null) end),
200
+ pr: {
201
+ key: $key, ciStatus: $ciStatus, reviewDecision: $pr.reviewDecision, mergeable: $pr.mergeable,
202
+ unresolvedThreads: $unresolved, headSha: $head, fixAttempts: $fixAttempts,
203
+ botVerdict: $botVerdict, botState: $botState,
204
+ botCommentId: ($botComment.id // null), botCommentUpdatedAt: ($botComment.updatedAt // null),
205
+ botFindingKeys: $keys, lastRoundFindingKeys: $lastRoundKeys, lastRoundHadRejection: $lastRoundHadRejection,
206
+ recurrenceStreak: $streak, unresolvedAfterClearance: $unresolved, lastError: null
207
+ },
208
+ actions: $actions,
209
+ lastGoodSnapshot: $snapshotPath
210
+ },
211
+ result: {
212
+ version: 1, slot: $slot, changed: $changed, decision: $decision, reasons: $reasons,
213
+ pr: {number: $pr.number, url: $pr.url, head: $head, branch: $pr.headRefName, base: $pr.baseRefName, author: $author,
214
+ state: $pr.state, mergeable: $pr.mergeable, reviewDecision: $pr.reviewDecision},
215
+ ci: {status: $ciStatus, checks: $checks},
216
+ verdict: {state: $botState, verdict: $botVerdict, findingsCount: $findingsCount, findingKeys: $keys,
217
+ mustFix: ($v.must_fix // []), commentUrl: ($botComment.url // null), commentId: ($botComment.id // null),
218
+ degraded: $degraded, degradedReason: $degradedReason, sameRunAsLastTick: $sameRun},
219
+ threads: {total: ($snap.threads | length), unresolved: $unresolved,
220
+ actionable: $actionable, staleUnresolved: $staleUnresolved,
221
+ skippedOutdated: $skippedOutdated, flaggedInjection: $flaggedInjection},
222
+ conversation: {actionable: $convActionable},
223
+ reviews: {actionable: $reviewActionable},
224
+ recurrence: {streak: $streak, lastRoundHadRejection: $lastRoundHadRejection, recurringKeys: $recurringKeys, fixAttempts: $fixAttempts},
225
+ backoff: {idleStreak: $idleStreak, intervalMinutes: $intervalMinutes, waitSeconds: $waitSeconds},
226
+ errors: [], snapshotPath: $snapshotPath, statePath: $statePath
227
+ }
228
+ }
229
+ EOF
230
+ }
231
+
232
+ combined=$(jq -n \
233
+ --slurpfile snapfile "$snapshot" --argjson prev "$prev_json" \
234
+ --arg now "$now" --arg statePath "$state_path" --arg snapshotPath "$snapshot_path" \
235
+ '$snapfile[0] as $snap | $prev as $prev | $now as $now | $statePath as $statePath | $snapshotPath as $snapshotPath | '"$(pb_jq_reduce)") \
236
+ || pb_fail invalid_json "reduce-state.sh: reducer failed on $snapshot" '{"source":"reduce"}'
237
+
238
+ if [ -n "$state_out" ]; then
239
+ pb_atomic_write_json "$state_out" jq -c '.state' <<<"$combined" || pb_fail invalid_json "reduce-state.sh: could not write $state_out" '{"source":"state_out"}'
240
+ fi
241
+ if [ -n "$result_out" ]; then
242
+ pb_atomic_write_json "$result_out" jq -c '.result' <<<"$combined" || pb_fail invalid_json "reduce-state.sh: could not write $result_out" '{"source":"result_out"}'
243
+ else
244
+ jq -c '.result' <<<"$combined"
245
+ fi