@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,749 @@
1
+ #!/usr/bin/env bash
2
+ # Plan-ledger checker lib + CLI: run each plan step's `check`, stamp mechanical
3
+ # status + content-addressed diff_sha into the per-branch ledger, and report
4
+ # fresh-green/next. The SCRIPT sets status (exit-code truth) — the agent cannot
5
+ # claim green. Sourceable (functions only) and runnable (guarded `main`).
6
+ # jq-only. Parse/IO errors fail closed (exit 2).
7
+ #
8
+ # Run: bash plan-ledger.sh run <doc.md> [--step <id>] [--activity <label>] [--force] [--verify]
9
+ # bash plan-ledger.sh status | preflight [<doc.md>] | path | root | --self-test
10
+ # Source: . "${BASH_SOURCE%/*}/plan-ledger.sh" (defines pl_* helpers, no run)
11
+
12
+ # pipefail so the `git diff | git hash-object` pipe surfaces failures instead of
13
+ # silently yielding the empty-blob sha. NOT -euo: this file sources libs and runs
14
+ # user `check` commands whose non-zero exits are expected signal, not fatal.
15
+ set -o pipefail
16
+
17
+ _toolu_lib="${TOOLU_LIB_DIR:-${BASH_SOURCE%/*}}"
18
+ # shellcheck source=plan-ledger-parse.sh
19
+ . "$_toolu_lib/plan-ledger-parse.sh"
20
+ # shellcheck source=detect.sh
21
+ . "$_toolu_lib/detect.sh"
22
+ # shellcheck source=plan-ledger-preflight.sh
23
+ . "$_toolu_lib/plan-ledger-preflight.sh"
24
+ # shellcheck source=diff-sha.sh
25
+ . "$_toolu_lib/diff-sha.sh"
26
+ # shellcheck source=telemetry.sh
27
+ . "$_toolu_lib/telemetry.sh"
28
+
29
+ # pl_diff_sha BASE
30
+ # Print the content-addressed diff hash of BASE...HEAD (matches push-review.sh:88).
31
+ # Empty stdout + non-zero on git failure so callers can fail closed. Both
32
+ # callers below run with cwd already at the project root (pl_cmd_run cd's
33
+ # there; pl_cmd_status assumes it like every other bare git call in this file),
34
+ # so "." is the equivalent repo root.
35
+ pl_diff_sha() {
36
+ local base="$1"
37
+ toolu_diff_sha . "$base"
38
+ }
39
+
40
+ # pl_ledger_path
41
+ # Print the ledger path for the current branch:
42
+ # <project_root>/<host-state>/tmp/plan-ledger/<branch_slug>.json
43
+ # Non-zero if the project root can't be resolved (not a git repo).
44
+ pl_ledger_path() {
45
+ local root branch slug
46
+ root=$(detect_project_root)
47
+ [ -n "$root" ] || return 1
48
+ branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
49
+ slug=$(branch_slug "$branch")
50
+ printf '%s/%s.json\n' "$(toolu_project_state_dir plan-ledger "$root")" "$slug"
51
+ }
52
+
53
+ # pl_evidence COMBINED_OUTPUT
54
+ # JSON-encode the last 10 lines of a check's combined stdout+stderr, capped to
55
+ # ~2000 bytes, via `jq -Rs` (handles null bytes / invalid UTF-8 safely). Prints a
56
+ # JSON string (quoted) on stdout.
57
+ pl_evidence() {
58
+ printf '%s' "$1" \
59
+ | tail -n 10 \
60
+ | head -c 2000 \
61
+ | jq -Rs .
62
+ }
63
+
64
+ # pl_scope_sha BASE PATHS_JSON
65
+ # Print the content hash of BASE...HEAD restricted to PATHS_JSON (a JSON array
66
+ # of pathspecs), or nothing when the array is empty.
67
+ #
68
+ # This is what lets a step stay green through a change it does not depend on. A
69
+ # branch-wide hash says "something moved"; this says "something YOU read moved".
70
+ pl_scope_sha() {
71
+ local base="$1" paths_json="$2"
72
+ local -a paths=()
73
+ local p
74
+ while IFS= read -r p; do
75
+ [ -n "$p" ] && paths+=("$p")
76
+ done < <(jq -r '.[]?' <<< "$paths_json" 2>/dev/null)
77
+ [ "${#paths[@]}" -gt 0 ] || return 0
78
+
79
+ # Capture the diff separately rather than piping straight into hash-object.
80
+ # A failed `git diff` writes nothing, and hashing nothing yields the
81
+ # well-known empty-blob sha — indistinguishable from "these paths are
82
+ # genuinely unchanged". Stored, that sha would match on every later run and
83
+ # pin the step green forever. Returning non-zero instead drops the step back
84
+ # to the branch-wide rule, which re-runs it. (push-review.sh guards the same
85
+ # empty-blob hazard on its own diff.)
86
+ local diff_out
87
+ diff_out=$(git diff --no-color "${base}...HEAD" -- "${paths[@]}" 2>/dev/null) || return 1
88
+
89
+ # The declaration is part of the identity: hashing only the diff would let a
90
+ # step keep a green after its `paths` were edited, as long as the new set
91
+ # happened to produce identical content — widening a scope must invalidate it.
92
+ { printf '%s\0' "${paths[@]}"; printf '%s' "$diff_out"; } | git hash-object --stdin
93
+ }
94
+
95
+ # pl_scope_map STEPS_JSON BASE
96
+ # Print {step_id: scope_sha} for every step declaring `paths`. Steps without
97
+ # paths are absent from the map and fall back to the branch-wide hash.
98
+ pl_scope_map() {
99
+ local steps="$1" base="$2" id paths sha out="{}"
100
+ while IFS=$'\t' read -r id paths; do
101
+ [ -n "$id" ] || continue
102
+ sha=$(pl_scope_sha "$base" "$paths") || sha=""
103
+ if [ -z "$sha" ]; then
104
+ # Falling back to the branch-wide rule is the safe direction — it re-runs
105
+ # the step more often, never less — but a `paths` declaration that quietly
106
+ # does nothing looks identical to one that works. Say so.
107
+ printf 'plan-ledger: step %s declares paths that could not be hashed; judging it on the whole branch diff\n' \
108
+ "$id" >&2
109
+ continue
110
+ fi
111
+ out=$(jq -c --arg id "$id" --arg sha "$sha" '. + {($id): $sha}' <<< "$out") || return 1
112
+ done < <(jq -r '.[] | select((.paths // []) | length > 0) | "\(.id)\t\(.paths | tojson)"' <<< "$steps" 2>/dev/null)
113
+ printf '%s' "$out"
114
+ }
115
+
116
+ # pl_recompute LEDGER_JSON CURRENT_DIFF_SHA
117
+ # Recompute summary{total,green,red,pending,running,stale,fresh_green} and next
118
+ # against CURRENT_DIFF_SHA (a step is fresh-green iff status==green AND diff_sha
119
+ # matches; a running step is never fresh; next = first non-fresh-green step id,
120
+ # null when all fresh-green). Print the updated ledger json on stdout.
121
+ pl_recompute() {
122
+ local ledger="$1" cur="$2" scope_map="${3:-}" verify="${4:-}"
123
+ [ -n "$scope_map" ] || scope_map='{}'
124
+ jq --arg cur "$cur" --argjson scope "$scope_map" --arg verify "$verify" '
125
+ # A step is fresh when it is green and nothing it depends on has moved.
126
+ # "Depends on" is the branch diff by default; a step that declared `paths`
127
+ # is judged on the hash of just those, so an unrelated edit no longer
128
+ # re-runs it. `--verify` ignores scope entirely and judges every step on
129
+ # the branch hash — the pre-push contract, where a narrow or stale `paths`
130
+ # declaration must not be able to hold a green.
131
+ def scoped_sha: $scope[.id] // null;
132
+ def is_fresh:
133
+ (.status == "green")
134
+ and (if ($verify != "1") and (scoped_sha != null)
135
+ then (.scope_sha != null) and (.scope_sha == scoped_sha)
136
+ else (.diff_sha == $cur) end);
137
+ .summary = {
138
+ total: (.steps | length),
139
+ green: ([.steps[] | select(.status == "green")] | length),
140
+ red: ([.steps[] | select(.status == "red")] | length),
141
+ pending: ([.steps[] | select(.status == "pending")]| length),
142
+ running: ([.steps[] | select(.status == "running")]| length),
143
+ stale: ([.steps[] | select(.status == "green" and (is_fresh | not))] | length),
144
+ fresh_green: ([.steps[] | select(is_fresh)] | length),
145
+ retried: ([.steps[] | select((.retries // []) | length > 0)] | length)
146
+ }
147
+ | .next = (first(.steps[] | select(is_fresh | not) | .id) // null)
148
+ ' <<< "$ledger"
149
+ }
150
+
151
+ # pl_summary_line LEDGER_JSON SLUG
152
+ # Print the single-line, parseable summary:
153
+ # plan-ledger <slug>: <fresh_green>/<total> fresh-green, next=<id|none>
154
+ # When the next step declares a model tier, ` model=<alias>` is appended so the
155
+ # executor knows which model to delegate it to without re-reading the plan doc.
156
+ # The suffix is omitted entirely for steps with no declared tier, keeping the
157
+ # line byte-identical for plans that never opted in.
158
+ pl_summary_line() {
159
+ local ledger="$1" slug="$2"
160
+ jq -r --arg slug "$slug" '
161
+ (.next) as $n
162
+ | ([.steps[] | select(.id == $n) | .model] | map(select(. != null)) | first) as $m
163
+ | "plan-ledger " + $slug + ": "
164
+ + (.summary.fresh_green | tostring) + "/" + (.summary.total | tostring)
165
+ + " fresh-green, next=" + ($n // "none")
166
+ + (if $m == null then "" else " model=" + $m end)
167
+ ' <<< "$ledger"
168
+ }
169
+
170
+ # pl_all_fresh LEDGER_JSON -> return 0 iff every step is fresh-green.
171
+ pl_all_fresh() {
172
+ local ledger="$1"
173
+ [ "$(jq -r '.next == null' <<< "$ledger")" = "true" ]
174
+ }
175
+
176
+ # pl_now -> UTC ISO-8601 timestamp.
177
+ pl_now() { date -u +%Y-%m-%dT%H:%M:%SZ; }
178
+
179
+ # pl_build_step_entry STEPS_JSON ID STATUS EXIT_CODE DIFF_SHA EVIDENCE_JSON \
180
+ # [STARTED_AT] [ACTIVITY] [PRIOR_ENTRY_JSON]
181
+ # Print a single ledger step object merging the doc fields (id/title/check plus
182
+ # the authored ac_refs/depends_on/input/model from STEPS_JSON) with the run results.
183
+ # EVIDENCE_JSON is an already-JSON-encoded string. STARTED_AT/ACTIVITY are
184
+ # optional; an empty arg becomes JSON null. started_at is the ISO-8601 time the
185
+ # step entered `running`; activity is an optional short label.
186
+ #
187
+ # PRIOR_ENTRY_JSON is the step's prior on-disk ledger entry (or "" / "null" when
188
+ # none). retries[] is the chronological list of reds preceding this state: if the
189
+ # prior entry was red, it is archived as a retry record (attempt, exit_code,
190
+ # diff_sha, evidence_tail, at=prior.last_run) appended after the prior's own
191
+ # retries; otherwise the prior retries (or []) carry forward unchanged. Only reds
192
+ # are archived. All additive — version stays 1.
193
+ pl_build_step_entry() {
194
+ local steps="$1" id="$2" status="$3" code="$4" sha="$5" evidence="$6"
195
+ local started_at="${7:-}" activity="${8:-}" prior="${9:-}"
196
+ [ -n "$prior" ] || prior="null"
197
+ jq -n \
198
+ --argjson steps "$steps" \
199
+ --arg id "$id" \
200
+ --arg status "$status" \
201
+ --argjson code "$code" \
202
+ --arg sha "$sha" \
203
+ --arg now "$(pl_now)" \
204
+ --argjson evidence "$evidence" \
205
+ --arg started_at "$started_at" \
206
+ --arg activity "$activity" \
207
+ --argjson prior "$prior" '
208
+ ($steps[] | select(.id == $id)) as $s
209
+ | ($prior.retries // []) as $prior_retries
210
+ | (if ($prior.status // null) == "red"
211
+ then $prior_retries + [{
212
+ attempt: (($prior_retries | length) + 1),
213
+ exit_code: $prior.exit_code,
214
+ diff_sha: $prior.diff_sha,
215
+ evidence_tail: $prior.evidence_tail,
216
+ at: $prior.last_run }]
217
+ else $prior_retries end) as $retries
218
+ | { id: $s.id, title: $s.title, check: $s.check,
219
+ status: $status,
220
+ started_at: (if $started_at == "" then null else $started_at end),
221
+ activity: (if $activity == "" then null else $activity end),
222
+ exit_code: $code, diff_sha: $sha,
223
+ last_run: $now, evidence_tail: $evidence,
224
+ ac_refs: ($s.ac_refs // []),
225
+ depends_on: ($s.depends_on // []),
226
+ input: ($s.input // null),
227
+ model: ($s.model // null),
228
+ retries: $retries }
229
+ '
230
+ }
231
+
232
+ # pl_cmd_run DOC [--step ID] [--activity LABEL]
233
+ # Parse DOC's steps; cd to project root; run checks (all, or only --step ID
234
+ # preserving other entries from an existing ledger); recompute and write the
235
+ # ledger; print the summary line. Exit 0 iff all fresh-green, else 1; parse/IO
236
+ # error -> exit 2 (writes nothing).
237
+ #
238
+ # When --step is given, the step is written `running` + started_at (+ activity)
239
+ # in a first atomic write BEFORE its check runs, then rewritten green|red after
240
+ # (two writes) so a watcher sees the in-flight state. --activity sets an optional
241
+ # short label; it only applies with --step.
242
+ # Seconds a single step's check may run before it is killed and marked red.
243
+ # A check that waits on something that never comes (a prompt, a lock, a network
244
+ # read) would otherwise hang the whole run with no way to tell from outside.
245
+ # 0 disables the bound.
246
+ PL_STEP_TIMEOUT="${PLAN_LEDGER_STEP_TIMEOUT:-1800}"
247
+
248
+ # pl_progress MESSAGE — a line to stderr, so stdout stays the summary contract.
249
+ # A full run can take minutes; silence for that long is indistinguishable from
250
+ # a hang, which is exactly how it was being read.
251
+ pl_progress() {
252
+ printf 'plan-ledger: %s\n' "$1" >&2
253
+ }
254
+
255
+ # pl_run_check CHECK OUTFILE -> exit code of CHECK.
256
+ #
257
+ # stdin is /dev/null: a check that reads stdin (a `git commit` without -m, any
258
+ # prompt) would otherwise inherit the caller's terminal and block forever.
259
+ # `timeout` bounds the rest; 124 is its kill signal and reads as red.
260
+ pl_run_check() {
261
+ local check="$1" outfile="$2"
262
+ if [ "${PL_STEP_TIMEOUT:-0}" != "0" ] && command -v timeout >/dev/null 2>&1; then
263
+ timeout "$PL_STEP_TIMEOUT" bash -c "$check" >"$outfile" 2>&1 </dev/null
264
+ return $?
265
+ fi
266
+ bash -c "$check" >"$outfile" 2>&1 </dev/null
267
+ }
268
+
269
+ pl_cmd_run() {
270
+ local doc="$1"; shift
271
+ local only_step="" activity="" force="" verify="" steps base cur ledger_file root
272
+ while [ "$#" -gt 0 ]; do
273
+ case "$1" in
274
+ --step)
275
+ only_step="${2:-}"
276
+ [ -n "$only_step" ] || { echo "plan-ledger: --step requires an id" >&2; return 2; }
277
+ shift 2
278
+ ;;
279
+ --activity)
280
+ activity="${2:-}"
281
+ [ -n "$activity" ] || { echo "plan-ledger: --activity requires a label" >&2; return 2; }
282
+ shift 2
283
+ ;;
284
+ --force)
285
+ force=1
286
+ shift
287
+ ;;
288
+ --verify)
289
+ # Judge every step on the branch diff, ignoring per-step `paths`. This
290
+ # is the pre-push contract: a narrow or stale scope declaration must not
291
+ # be able to hold a green through a change it failed to account for.
292
+ verify=1
293
+ shift
294
+ ;;
295
+ *)
296
+ echo "plan-ledger: unknown run flag: $1" >&2; return 2
297
+ ;;
298
+ esac
299
+ done
300
+ [ -z "$activity" ] || [ -n "$only_step" ] || { echo "plan-ledger: --activity requires --step" >&2; return 2; }
301
+
302
+ # Parse first — on failure write NOTHING (crit8).
303
+ steps=$(pl_parse_steps "$doc") || return 2
304
+
305
+ base="${PUSH_REVIEW_BASE:-$(detect_base_branch)}"
306
+ root=$(detect_project_root)
307
+ [ -n "$root" ] || { echo "plan-ledger: not in a git repo" >&2; return 2; }
308
+ ledger_file=$(pl_ledger_path) || { echo "plan-ledger: cannot resolve ledger path" >&2; return 2; }
309
+
310
+ # cd to project root so checks run there (and diff_sha is repo-relative).
311
+ cd "$root" || { echo "plan-ledger: cannot cd to $root" >&2; return 2; }
312
+
313
+ cur=$(pl_diff_sha "$base") || { echo "plan-ledger: git diff ${base}...HEAD failed" >&2; return 2; }
314
+
315
+ local branch slug
316
+ branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 2
317
+ slug=$(branch_slug "$branch")
318
+
319
+ # Prior on-disk entries, indexed by id. Read for BOTH full and --step runs: a
320
+ # --step run preserves the other steps' entries verbatim, and EVERY run passes
321
+ # each step's prior entry to the serializer so a prior red is archived into
322
+ # retries[] (and prior retries carry forward). Captured once here from the
323
+ # ORIGINAL ledger, before any write below — so the --step running pre-write
324
+ # cannot overwrite the prior we archive from. Fail-closed: a present-but-corrupt
325
+ # ledger returns 2 (never silently dropped); an absent ledger is the empty map.
326
+ local existing="{}"
327
+ local prior
328
+ if prior=$(pl_read_ledger "$ledger_file" 2>/dev/null); then
329
+ existing=$(jq '[.steps[] | {key: .id, value: .}] | from_entries' <<< "$prior") \
330
+ || { echo "plan-ledger: corrupt prior ledger at $ledger_file" >&2; return 2; }
331
+ elif [ -s "$ledger_file" ]; then
332
+ # File exists and is non-empty but pl_read_ledger refused it (unparseable
333
+ # json): corrupt, not absent. Fail closed rather than silently archiving
334
+ # nothing and clobbering it with a fresh run.
335
+ echo "plan-ledger: corrupt prior ledger at $ledger_file" >&2; return 2
336
+ fi
337
+
338
+ # Write #1 (only with --step): mark the target step `running` + started_at
339
+ # (+ activity) and persist BEFORE running its check, so a watcher sees the
340
+ # in-flight state. Other steps keep their prior entry (or seed pending). The
341
+ # green|red rewrite (write #2) happens after the check, below.
342
+ local started_at=""
343
+ if [ -n "$only_step" ]; then
344
+ started_at=$(pl_now)
345
+ local pre_steps
346
+ pre_steps=$(jq -n \
347
+ --argjson ex "$existing" --argjson steps "$steps" \
348
+ --arg only "$only_step" --arg now "$started_at" --arg act "$activity" '
349
+ [ $steps[] | .id as $id
350
+ | ($ex[$id]) as $p
351
+ | ($steps[] | select(.id==$id)) as $s
352
+ | if $id == $only
353
+ then { id: $s.id, title: $s.title, check: $s.check,
354
+ status: "running",
355
+ started_at: $now,
356
+ activity: (if $act == "" then null else $act end),
357
+ exit_code: null, diff_sha: null,
358
+ last_run: $now, evidence_tail: null,
359
+ ac_refs: ($s.ac_refs // []),
360
+ depends_on: ($s.depends_on // []),
361
+ input: ($s.input // null),
362
+ model: ($s.model // null),
363
+ retries: ($p.retries // []) }
364
+ elif $p != null
365
+ then $p
366
+ | .ac_refs = ($s.ac_refs // [])
367
+ | .depends_on = ($s.depends_on // [])
368
+ | .input = ($s.input // null)
369
+ | .model = ($s.model // null)
370
+ else { id: $s.id, title: $s.title, check: $s.check,
371
+ status: "pending", started_at: null, activity: null,
372
+ exit_code: null, diff_sha: null,
373
+ last_run: null, evidence_tail: null,
374
+ ac_refs: ($s.ac_refs // []),
375
+ depends_on: ($s.depends_on // []),
376
+ input: ($s.input // null),
377
+ model: ($s.model // null),
378
+ retries: [] }
379
+ end
380
+ ]') || { echo "plan-ledger: failed to assemble running pre-write" >&2; return 2; }
381
+ local pre_ledger
382
+ pre_ledger=$(jq -n \
383
+ --arg branch "$branch" --arg base "$base" --arg doc "$doc" \
384
+ --arg now "$started_at" --argjson steps "$pre_steps" '
385
+ { version: 1, branch: $branch, base_branch: $base, plan_doc: $doc,
386
+ updated_at: $now, summary: {}, next: null, steps: $steps }') \
387
+ || { echo "plan-ledger: failed to assemble running pre-ledger" >&2; return 2; }
388
+ pre_ledger=$(pl_recompute "$pre_ledger" "$cur" "$scope_map" "$verify") \
389
+ || { echo "plan-ledger: failed to recompute running pre-ledger" >&2; return 2; }
390
+ pl_write_ledger "$ledger_file" "$pre_ledger" \
391
+ || { echo "plan-ledger: running pre-write failed" >&2; return 2; }
392
+ fi
393
+
394
+ # Build the steps array.
395
+ # Per-step scope hashes for this run. Empty for steps that declare no
396
+ # `paths`, which keeps them on the branch-wide rule they have always used.
397
+ local scope_map
398
+ scope_map=$(pl_scope_map "$steps" "$base") || scope_map='{}'
399
+ [ -n "$scope_map" ] || scope_map='{}'
400
+
401
+ local out_steps id check status code evidence tmpout new_entry
402
+ local _pl_index=0 _pl_total
403
+ _pl_total=$(jq 'length' <<< "$steps")
404
+ out_steps="[]"
405
+ while IFS= read -r id; do
406
+ [ -n "$id" ] || continue
407
+ check=$(jq -r --arg id "$id" '.[] | select(.id==$id) | .check' <<< "$steps") \
408
+ || { echo "plan-ledger: failed to read check for step $id" >&2; return 2; }
409
+ if [ -n "$only_step" ] && [ "$id" != "$only_step" ]; then
410
+ # Carry the prior entry forward, but RE-DERIVE the authored fields
411
+ # (ac_refs/depends_on/input/model) from the current parsed step $s — they live in
412
+ # the plan doc and must reflect any edit since the last run (spec: authored
413
+ # fields are re-derived every run), while all engine state (status,
414
+ # exit_code, diff_sha, last_run, evidence_tail, retries) is preserved. No
415
+ # prior entry -> seed a pending step with the current authored fields.
416
+ new_entry=$(jq -n --argjson ex "$existing" --argjson steps "$steps" --arg id "$id" '
417
+ ($ex[$id]) as $p
418
+ | ($steps[] | select(.id==$id)) as $s
419
+ | if $p != null
420
+ then $p
421
+ | .scope_sha = (.scope_sha // null)
422
+ | .ac_refs = ($s.ac_refs // [])
423
+ | .depends_on = ($s.depends_on // [])
424
+ | .input = ($s.input // null)
425
+ | .model = ($s.model // null)
426
+ | .title = $s.title
427
+ | .check = $s.check
428
+ | .status = (.status // "pending")
429
+ | .started_at = (.started_at // null)
430
+ | .activity = (.activity // null)
431
+ | .exit_code = (.exit_code // null)
432
+ | .diff_sha = (.diff_sha // null)
433
+ | .last_run = (.last_run // null)
434
+ | .evidence_tail = (.evidence_tail // null)
435
+ | .retries = (.retries // [])
436
+ else { id: $s.id, title: $s.title, check: $s.check,
437
+ status: "pending", started_at: null, activity: null,
438
+ exit_code: null, diff_sha: null,
439
+ last_run: null, evidence_tail: null,
440
+ ac_refs: ($s.ac_refs // []),
441
+ depends_on: ($s.depends_on // []),
442
+ input: ($s.input // null),
443
+ model: ($s.model // null),
444
+ retries: [] }
445
+ end
446
+ ') || { echo "plan-ledger: failed to assemble entry for step $id" >&2; return 2; }
447
+ else
448
+ # Prior on-disk entry for this id (or "null"): the builder archives it into
449
+ # retries[] iff it was red, and carries its prior retries forward.
450
+ local prior_entry
451
+ prior_entry=$(jq -cn --argjson ex "$existing" --arg id "$id" '$ex[$id] // null') \
452
+ || { echo "plan-ledger: failed to read prior entry for step $id" >&2; return 2; }
453
+
454
+ _pl_index=$((_pl_index + 1))
455
+
456
+ # A step already green AT THIS diff sha has nothing new to prove: re-running
457
+ # it burns the same minutes to reach the same answer. A full run after one
458
+ # edited step used to re-execute every check, which is what made "run the
459
+ # whole plan" feel like a hang. An explicit --step always runs (you asked
460
+ # for that one); --force re-runs everything.
461
+ # Fresh means "nothing this step depends on moved". For a step with
462
+ # `paths` that is its own scope hash; otherwise the branch hash, as before.
463
+ # `--verify` forces the branch hash for everything.
464
+ local _pl_scope_now _pl_prior_key _pl_now_key
465
+ _pl_scope_now=$(jq -r --arg id "$id" '.[$id] // ""' <<< "$scope_map")
466
+ if [ -z "$verify" ] && [ -n "$_pl_scope_now" ]; then
467
+ _pl_prior_key=$(jq -r '.scope_sha // ""' <<< "$prior_entry")
468
+ _pl_now_key="$_pl_scope_now"
469
+ else
470
+ _pl_prior_key=$(jq -r '.diff_sha // ""' <<< "$prior_entry")
471
+ _pl_now_key="$cur"
472
+ fi
473
+
474
+ if [ -z "$force" ] && [ -z "$only_step" ] \
475
+ && [ "$(jq -r '.status // ""' <<< "$prior_entry")" = "green" ] \
476
+ && [ -n "$_pl_now_key" ] && [ "$_pl_prior_key" = "$_pl_now_key" ]; then
477
+ pl_progress "[$_pl_index/$_pl_total] $id: fresh-green, skipped (--force re-runs)"
478
+ new_entry=$(jq -n --argjson p "$prior_entry" --argjson steps "$steps" --arg id "$id" '
479
+ ($steps[] | select(.id==$id)) as $s
480
+ | $p
481
+ | .scope_sha = (.scope_sha // null)
482
+ | .ac_refs = ($s.ac_refs // [])
483
+ | .depends_on = ($s.depends_on // [])
484
+ | .input = ($s.input // null)
485
+ | .model = ($s.model // null)
486
+ | .title = $s.title
487
+ | .check = $s.check
488
+ | .status = (.status // "pending")
489
+ | .started_at = (.started_at // null)
490
+ | .activity = (.activity // null)
491
+ | .exit_code = (.exit_code // null)
492
+ | .diff_sha = (.diff_sha // null)
493
+ | .last_run = (.last_run // null)
494
+ | .evidence_tail = (.evidence_tail // null)
495
+ | .retries = (.retries // [])
496
+ ') || { echo "plan-ledger: failed to carry forward step $id" >&2; return 2; }
497
+ out_steps=$(jq --argjson e "$new_entry" '. + [$e]' <<< "$out_steps") \
498
+ || { echo "plan-ledger: failed to append step $id" >&2; return 2; }
499
+ continue
500
+ fi
501
+
502
+ tmpout="$ledger_file.run.$$.$id"
503
+ mkdir -p "$(dirname "$ledger_file")" 2>/dev/null || true
504
+ local _pl_t0 _pl_t1 _pl_duration
505
+ pl_progress "[$_pl_index/$_pl_total] $id: running check"
506
+ _pl_t0=$(date +%s)
507
+ pl_run_check "$check" "$tmpout"
508
+ code=$?
509
+ _pl_t1=$(date +%s)
510
+ _pl_duration=$((_pl_t1 - _pl_t0))
511
+ [ "$code" -eq 0 ] && status="green" || status="red"
512
+ evidence=$(pl_evidence "$(cat "$tmpout")")
513
+ if [ "$code" -eq 124 ]; then
514
+ # Name the timeout in the evidence, or the ledger just shows a red step
515
+ # with whatever partial output the check managed before it was killed.
516
+ evidence=$(pl_evidence "timed out after ${PL_STEP_TIMEOUT}s (PLAN_LEDGER_STEP_TIMEOUT)
517
+ $evidence")
518
+ fi
519
+ rm -f "$tmpout"
520
+ pl_progress "[$_pl_index/$_pl_total] $id: $status (${_pl_duration}s)"
521
+ new_entry=$(pl_build_step_entry "$steps" "$id" "$status" "$code" "$cur" "$evidence" "" "" "$prior_entry") \
522
+ || { echo "plan-ledger: failed to build entry for step $id" >&2; return 2; }
523
+ # Record what the step's declared scope hashed to when it ran, so the next
524
+ # run can tell whether anything it depends on has moved since.
525
+ new_entry=$(jq -c --arg sha "$_pl_scope_now" \
526
+ '.scope_sha = (if $sha == "" then null else $sha end)' <<< "$new_entry") \
527
+ || { echo "plan-ledger: failed to stamp scope for step $id" >&2; return 2; }
528
+ # step_run telemetry: this branch is the ONE code path both the full-run
529
+ # (every id lands here) and --step run (only the targeted id lands here)
530
+ # share, so instrumenting it covers both per the spec. attempt =
531
+ # archived-retry count + 1, read off the entry pl_build_step_entry just
532
+ # archived, so it reflects the SAME retry history just computed.
533
+ local _pl_attempt
534
+ _pl_attempt=$(jq -r '(.retries | length) + 1' <<< "$new_entry")
535
+ telemetry_append "$root" "step_run" \
536
+ "$(jq -cn --arg id "$id" --arg status "$status" --argjson code "$code" \
537
+ --argjson duration "$_pl_duration" --argjson attempt "$_pl_attempt" \
538
+ '{step_id: $id, status: $status, exit_code: $code, duration_s: $duration, attempt: $attempt}')"
539
+ fi
540
+ out_steps=$(jq --argjson e "$new_entry" '. + [$e]' <<< "$out_steps") \
541
+ || { echo "plan-ledger: failed to append step $id" >&2; return 2; }
542
+ done < <(jq -r '.[].id' <<< "$steps")
543
+
544
+ # Assemble the full ledger, then recompute summary/next against current sha.
545
+ local ledger
546
+ ledger=$(jq -n \
547
+ --arg branch "$branch" \
548
+ --arg base "$base" \
549
+ --arg doc "$doc" \
550
+ --arg now "$(pl_now)" \
551
+ --argjson steps "$out_steps" '
552
+ { version: 1, branch: $branch, base_branch: $base, plan_doc: $doc,
553
+ updated_at: $now,
554
+ summary: {}, next: null, steps: $steps }
555
+ ') || { echo "plan-ledger: failed to assemble ledger" >&2; return 2; }
556
+ ledger=$(pl_recompute "$ledger" "$cur" "$scope_map" "$verify") \
557
+ || { echo "plan-ledger: failed to recompute summary" >&2; return 2; }
558
+
559
+ # verified_sha is the branch hash at which a --verify run last found every
560
+ # step green. The push gate reads it rather than per-step freshness, so a
561
+ # scoped green can speed up iteration without ever standing in for the
562
+ # full check before a push.
563
+ local prior_verified
564
+ local prior_json="${prior:-}"
565
+ [ -n "$prior_json" ] || prior_json='{}'
566
+ prior_verified=$(jq -r '.verified_sha // ""' <<< "$prior_json" 2>/dev/null) || prior_verified=""
567
+ # Read freshness off the summary pl_recompute just wrote rather than
568
+ # restating the rule here. The two were equivalent, but only by an argument
569
+ # about what --verify does to the skip logic — and an argument is not a
570
+ # guarantee once someone edits is_fresh. summary.fresh_green is computed by
571
+ # that same definition, so the stamp cannot drift from it.
572
+ if [ -n "$verify" ] && [ -z "$only_step" ] \
573
+ && [ "$(jq -r '.summary.fresh_green == .summary.total' <<< "$ledger")" = "true" ]; then
574
+ ledger=$(jq -c --arg sha "$cur" '.verified_sha = $sha' <<< "$ledger") || return 2
575
+ else
576
+ ledger=$(jq -c --arg sha "$prior_verified" \
577
+ '.verified_sha = (if $sha == "" then null else $sha end)' <<< "$ledger") || return 2
578
+ fi
579
+
580
+ pl_write_ledger "$ledger_file" "$ledger" || { echo "plan-ledger: ledger write failed" >&2; return 2; }
581
+
582
+ pl_summary_line "$ledger" "$slug"
583
+ pl_all_fresh "$ledger" && return 0 || return 1
584
+ }
585
+
586
+ # pl_ac_coverage_lines LEDGER_JSON CUR SPEC_DOC
587
+ # Print a human-readable AC-coverage report to stdout (REPORT-ONLY — the caller
588
+ # must never let it affect exit codes or the gate). For each AC id declared in
589
+ # SPEC_DOC (via pl_parse_acs), list the ledger steps whose ac_refs name it and
590
+ # whether at least one of those covering steps is fresh-green (status==green AND
591
+ # diff_sha==CUR); an AC with no fresh-green covering step is flagged UNCOVERED.
592
+ # Spec-less (SPEC_DOC empty / "none" / missing / no AC ids) -> print nothing and
593
+ # return 0: coverage is skipped, never a blocker. Never writes, never errors out.
594
+ pl_ac_coverage_lines() {
595
+ local ledger="$1" cur="$2" spec="$3" acs
596
+ case "$(printf '%s' "$spec" | tr '[:upper:]' '[:lower:]')" in
597
+ ""|none) return 0 ;;
598
+ esac
599
+ acs=$(pl_parse_acs "$spec")
600
+ [ -n "$acs" ] || return 0
601
+ printf 'AC coverage (report-only):\n'
602
+ # For each AC id, jq finds covering steps and whether any is fresh-green.
603
+ local ac line
604
+ while IFS= read -r ac; do
605
+ [ -n "$ac" ] || continue
606
+ line=$(jq -rn --argjson l "$ledger" --arg cur "$cur" --arg ac "$ac" '
607
+ [ $l.steps[] | select((.ac_refs // []) | index($ac)) ] as $cov
608
+ | ($cov | map(.id)) as $ids
609
+ | ($cov | any(.status == "green" and .diff_sha == $cur)) as $fresh
610
+ | if ($ids | length) == 0
611
+ then " " + $ac + ": UNCOVERED (no step references it)"
612
+ elif $fresh
613
+ then " " + $ac + ": covered by " + ($ids | join(", "))
614
+ else " " + $ac + ": UNCOVERED (" + ($ids | join(", ")) + " not fresh-green)"
615
+ end
616
+ ') || { echo "plan-ledger: failed to compute AC coverage for $ac" >&2; return 0; }
617
+ printf '%s\n' "$line"
618
+ done <<< "$acs"
619
+ return 0
620
+ }
621
+
622
+ # pl_cmd_status
623
+ # Read the current branch's ledger (absent -> exit 2), recompute summary/next vs
624
+ # the current diff_sha WITHOUT running checks, write the refreshed ledger, print
625
+ # the summary line, then the AC-coverage report (report-only). Exit 0 iff all
626
+ # fresh-green, else 1 — AC coverage NEVER changes the exit code.
627
+ pl_cmd_status() {
628
+ local base cur ledger_file ledger slug branch
629
+ base="${PUSH_REVIEW_BASE:-$(detect_base_branch)}"
630
+ ledger_file=$(pl_ledger_path) || { echo "plan-ledger: cannot resolve ledger path" >&2; return 2; }
631
+ ledger=$(pl_read_ledger "$ledger_file") || { echo "plan-ledger: no ledger at $ledger_file" >&2; return 2; }
632
+ cur=$(pl_diff_sha "$base") || { echo "plan-ledger: git diff ${base}...HEAD failed" >&2; return 2; }
633
+
634
+ branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 2
635
+ slug=$(branch_slug "$branch")
636
+
637
+ # Self-heal orphaned `running` steps (crash between the two run writes) back to
638
+ # pending before recomputing, so a stale running can't wedge status/push-gate.
639
+ ledger=$(pl_heal_orphans "$ledger") \
640
+ || { echo "plan-ledger: failed to heal orphaned running steps" >&2; return 2; }
641
+ ledger=$(pl_recompute "$ledger" "$cur") \
642
+ || { echo "plan-ledger: failed to recompute summary" >&2; return 2; }
643
+ pl_write_ledger "$ledger_file" "$ledger" || { echo "plan-ledger: ledger write failed" >&2; return 2; }
644
+
645
+ pl_summary_line "$ledger" "$slug"
646
+
647
+ # AC-coverage report (report-only): resolve the plan's **Spec:** doc relative to
648
+ # the project root, parse its AC ids, and report which are covered by a
649
+ # fresh-green step. REPORT-ONLY — coverage must never change the status exit
650
+ # code or wedge the gate (spec Non-Goal). A coverage failure is reported to
651
+ # stderr (not silently dropped), then status proceeds with its real exit code.
652
+ local plan_doc spec_field spec_path root
653
+ plan_doc=$(jq -r '.plan_doc // ""' <<< "$ledger") \
654
+ || { echo "plan-ledger: could not read plan_doc for AC coverage (skipping report)" >&2; plan_doc=""; }
655
+ if [ -n "$plan_doc" ]; then
656
+ root=$(detect_project_root)
657
+ [ -f "$plan_doc" ] || { [ -n "$root" ] && [ -f "$root/$plan_doc" ] && plan_doc="$root/$plan_doc"; }
658
+ if [ -f "$plan_doc" ]; then
659
+ spec_field=$(pl_doc_field "$plan_doc" Spec)
660
+ spec_path="$spec_field"
661
+ case "$(printf '%s' "$spec_field" | tr '[:upper:]' '[:lower:]')" in
662
+ ""|none) spec_path="$spec_field" ;;
663
+ *) [ -f "$spec_path" ] || { [ -n "$root" ] && [ -f "$root/$spec_field" ] && spec_path="$root/$spec_field"; } ;;
664
+ esac
665
+ # pl_ac_coverage_lines is itself report-only: it returns 0 even on an
666
+ # internal failure (after emitting a stderr diagnostic), so status's exit
667
+ # code below is decided solely by fresh-green state.
668
+ pl_ac_coverage_lines "$ledger" "$cur" "$spec_path"
669
+ fi
670
+ fi
671
+
672
+ pl_all_fresh "$ledger" && return 0 || return 1
673
+ }
674
+
675
+ # pl_self_test
676
+ # Parse a tiny inline fixture doc and assert pl_parse_steps yields the expected
677
+ # two-step array. Minimal but real. Exit 0/1.
678
+ pl_self_test() {
679
+ local dir doc out
680
+ dir=$(mktemp -d) || return 1
681
+ doc="$dir/selftest-plan.md"
682
+ cat > "$doc" <<'EOF'
683
+ # Self-test Plan
684
+
685
+ ## Steps (machine-readable)
686
+
687
+ ```json
688
+ [
689
+ { "id": "s1", "title": "ok", "check": "true" },
690
+ { "id": "s2", "title": "fail", "check": "false" }
691
+ ]
692
+ ```
693
+ EOF
694
+ if ! out=$(pl_parse_steps "$doc"); then
695
+ rm -rf "$dir"; echo "plan-ledger --self-test: parse failed" >&2; return 1
696
+ fi
697
+ rm -rf "$dir"
698
+ if [ "$(jq -r 'length' <<< "$out")" != "2" ] \
699
+ || [ "$(jq -r '.[0].id' <<< "$out")" != "s1" ] \
700
+ || [ "$(jq -r '.[1].check' <<< "$out")" != "false" ]; then
701
+ echo "plan-ledger --self-test: unexpected parse result" >&2; return 1
702
+ fi
703
+ echo "plan-ledger --self-test: ok"
704
+ return 0
705
+ }
706
+
707
+ # main "$@"
708
+ # CLI dispatch. Requires jq + git. Unknown command -> exit 2.
709
+ main() {
710
+ command -v jq >/dev/null 2>&1 || { echo "plan-ledger: jq is required" >&2; exit 2; }
711
+ command -v git >/dev/null 2>&1 || { echo "plan-ledger: git is required" >&2; exit 2; }
712
+
713
+ local cmd="${1:-}"; shift || true
714
+ case "$cmd" in
715
+ run)
716
+ [ -n "${1:-}" ] || { echo "plan-ledger: run requires a plan doc path" >&2; exit 2; }
717
+ pl_cmd_run "$@"; exit $?
718
+ ;;
719
+ status)
720
+ pl_cmd_status; exit $?
721
+ ;;
722
+ preflight)
723
+ pl_cmd_preflight "${1:-}"; exit $?
724
+ ;;
725
+ path)
726
+ pl_ledger_path || { echo "plan-ledger: cannot resolve ledger path" >&2; exit 2; }
727
+ exit 0
728
+ ;;
729
+ root)
730
+ local root
731
+ root=$(detect_project_root)
732
+ [ -n "$root" ] || { echo "plan-ledger: not in a git repo" >&2; exit 2; }
733
+ printf '%s\n' "$root"
734
+ exit 0
735
+ ;;
736
+ --self-test)
737
+ pl_self_test; exit $?
738
+ ;;
739
+ *)
740
+ echo "plan-ledger: usage: run <doc> [--step <id>] [--activity <label>] | status | preflight [<doc>] | path | root | --self-test" >&2
741
+ exit 2
742
+ ;;
743
+ esac
744
+ }
745
+
746
+ # Guarded main: run only when executed directly, not when sourced.
747
+ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
748
+ main "$@"
749
+ fi