@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,125 @@
1
+ #!/usr/bin/env bash
2
+ # One-time host permission allowlist for a repo.
3
+ #
4
+ # toolu's gates are the layer that decides what should stop you; the host's
5
+ # permission prompts are a second, unrelated layer that stops you again for
6
+ # routine work — every git and gh invocation, every shell command. Once the
7
+ # gates ask properly (see gate-mode.sh), the second layer is mostly noise.
8
+ #
9
+ # So the first toolu session in a repo writes an allowlist into that repo's
10
+ # .claude/settings.local.json, and records a sentinel so it never does it
11
+ # again. Deleting a rule by hand therefore sticks: this is a one-time
12
+ # convenience, not a policy that reasserts itself every session.
13
+ #
14
+ # Deliberate choices:
15
+ # * settings.local.json, not settings.json — the local file is not meant to
16
+ # be committed, so a teammate never inherits this decision.
17
+ # * the sentinel lives under the git-ignored state root, NOT in
18
+ # toolu.config.json, which IS committed — a marker there would ride into
19
+ # the repo and every clone would think it had already been done.
20
+ # * entries are merged as a union. Nothing the user already wrote is
21
+ # removed or reordered.
22
+ # * a settings file we cannot parse is left byte-identical. Losing someone's
23
+ # permission rules to a clumsy rewrite is far worse than a missing
24
+ # convenience.
25
+ #
26
+ # Claude Code only: Codex has no equivalent permission file.
27
+ #
28
+ # Public API:
29
+ # toolu_permissions_autowrite ROOT 0 if written, 1 if skipped
30
+
31
+ _TOOLU_PERMISSIONS_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
32
+ # shellcheck source=config.sh
33
+ . "$_TOOLU_PERMISSIONS_LIB_DIR/config.sh"
34
+
35
+ # Blanket shell access plus the two edit tools. Bash(*) subsumes Bash(git:*)
36
+ # and Bash(gh:*), so they are not listed separately.
37
+ TOOLU_PERMISSIONS_DEFAULT='["Bash(*)","Edit","Write"]'
38
+ TOOLU_PERMISSIONS_SENTINEL=".permissions-written"
39
+
40
+ _toolu_permissions_warn() {
41
+ printf 'toolu-permissions: %s\n' "$1" >&2
42
+ }
43
+
44
+ # _toolu_permissions_entries -> the JSON array of rules to add.
45
+ # A configured permissions.allow replaces the default outright (a user who
46
+ # names their own set means that set, not that set plus ours).
47
+ _toolu_permissions_entries() {
48
+ local configured
49
+ toolu_load_config
50
+ if [ "$_TOOLU_HAS_JQ" = "1" ]; then
51
+ configured=$(jq -c '
52
+ if ((.permissions?.allow? | type) == "array")
53
+ and ((.permissions.allow | map(select(type == "string")) | length) > 0)
54
+ then (.permissions.allow | map(select(type == "string")))
55
+ else empty end' <<< "$TOOLU_CFG_JSON" 2>/dev/null)
56
+ [ -n "$configured" ] && { printf '%s' "$configured"; return 0; }
57
+ fi
58
+ printf '%s' "$TOOLU_PERMISSIONS_DEFAULT"
59
+ }
60
+
61
+ # toolu_permissions_autowrite ROOT
62
+ toolu_permissions_autowrite() {
63
+ local root="${1:-}" settings_file sentinel state_root entries existing merged tmp added
64
+ [ -n "$root" ] || root=$(toolu_project_root)
65
+ [ -n "$root" ] || return 1
66
+ command -v jq >/dev/null 2>&1 || return 1
67
+
68
+ # Claude-only.
69
+ [ "$(toolu_host)" = claude ] || return 1
70
+
71
+ # Explicit opt-out.
72
+ toolu_flag_false permissions autoAllow && return 1
73
+
74
+ # A directory that is not a repository has no project-local settings to own.
75
+ git -C "$root" rev-parse --git-dir >/dev/null 2>&1 || return 1
76
+
77
+ state_root=$(toolu_project_state_root "$root")
78
+ [ -n "$state_root" ] || return 1
79
+ sentinel="$state_root/$TOOLU_PERMISSIONS_SENTINEL"
80
+ [ -f "$sentinel" ] && return 1
81
+
82
+ settings_file="$root/$(toolu_project_dirname)/settings.local.json"
83
+ existing='{}'
84
+ if [ -f "$settings_file" ]; then
85
+ if ! existing=$(jq -e . "$settings_file" 2>/dev/null); then
86
+ _toolu_permissions_warn "malformed JSON in $settings_file; leaving it untouched"
87
+ return 1
88
+ fi
89
+ fi
90
+
91
+ entries=$(_toolu_permissions_entries)
92
+
93
+ # Union, order-preserving: existing rules first, then whatever is new.
94
+ merged=$(jq --argjson add "$entries" '
95
+ ((.permissions.allow // []) | map(select(type == "string"))) as $have
96
+ | ($add | map(select(. as $x | $have | index($x) | not))) as $new
97
+ | .permissions = ((.permissions // {}) + {allow: ($have + $new)})
98
+ ' <<< "$existing" 2>/dev/null) || {
99
+ _toolu_permissions_warn "could not merge permissions into $settings_file"
100
+ return 1
101
+ }
102
+
103
+ added=$(jq -r --argjson add "$entries" '
104
+ ((.permissions.allow // []) | map(select(type == "string"))) as $have
105
+ | ($add | map(select(. as $x | $have | index($x) | not)) | join(", "))
106
+ ' <<< "$existing" 2>/dev/null)
107
+
108
+ mkdir -p "$(dirname "$settings_file")" 2>/dev/null || return 1
109
+ tmp=$(mktemp "${settings_file}.XXXXXX" 2>/dev/null) || return 1
110
+ if ! printf '%s\n' "$merged" > "$tmp" 2>/dev/null || ! mv -f "$tmp" "$settings_file" 2>/dev/null; then
111
+ rm -f "$tmp" 2>/dev/null
112
+ _toolu_permissions_warn "could not write $settings_file"
113
+ return 1
114
+ fi
115
+
116
+ # The sentinel is written only after the settings write actually landed, so a
117
+ # failed write is retried next session rather than silently skipped forever.
118
+ mkdir -p "$state_root" 2>/dev/null && : > "$sentinel" 2>/dev/null
119
+
120
+ if [ -n "$added" ]; then
121
+ printf 'toolu wrote %s to %s (one time only; delete a rule and it stays deleted). Add that file to .gitignore if it is not there already.\n' \
122
+ "$added" "$settings_file"
123
+ fi
124
+ return 0
125
+ }
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env bash
2
+ # Plan-ledger parse + I/O lib: extract the machine-readable steps block from a
3
+ # plan doc and read/write the per-branch ledger json atomically. Pure functions,
4
+ # jq-only, no command execution and no `set` (this is a sourced library).
5
+ # Source via: . "${BASH_SOURCE%/*}/plan-ledger-parse.sh"
6
+
7
+ # pl_parse_steps PLAN_DOC_PATH
8
+ # Extract the FIRST ```json fenced block under the `## Steps (machine-readable)`
9
+ # heading and print the JSON array to stdout. Validates with jq: must be a
10
+ # non-empty array whose every element has non-empty string id/title/check, and
11
+ # whose optional `model` (the tier that should execute the step) is one of
12
+ # haiku|sonnet|opus|fable|inherit.
13
+ # On missing heading/block, empty array, malformed json, or an unroutable
14
+ # model: error to stderr, non-zero return, nothing on stdout. Never writes.
15
+ pl_parse_steps() {
16
+ local doc="$1" block
17
+ if [ ! -f "$doc" ]; then
18
+ echo "plan-ledger-parse: plan doc not found: $doc" >&2
19
+ return 1
20
+ fi
21
+ # awk state machine: once past the marker heading, capture the lines between
22
+ # the next ```json fence and its closing ``` fence. `in_steps` arms after the
23
+ # heading; `in_block` arms inside the first json fence; we stop at its close.
24
+ block=$(awk '
25
+ /^## Steps \(machine-readable\)[[:space:]]*$/ { in_steps = 1; next }
26
+ in_steps && !in_block && /^```json[[:space:]]*$/ { in_block = 1; next }
27
+ in_block && /^```[[:space:]]*$/ { exit }
28
+ in_block { print }
29
+ ' "$doc")
30
+ if [ -z "$block" ]; then
31
+ echo "plan-ledger-parse: no '## Steps (machine-readable)' json block in $doc" >&2
32
+ return 1
33
+ fi
34
+ # Validate: parseable, non-empty array, every step has non-empty id/title/check.
35
+ if ! jq -e '
36
+ type == "array"
37
+ and length > 0
38
+ and all(.[];
39
+ (.id | type == "string" and length > 0) and
40
+ (.title | type == "string" and length > 0) and
41
+ (.check | type == "string" and length > 0))
42
+ ' <<< "$block" >/dev/null 2>&1; then
43
+ echo "plan-ledger-parse: steps block in $doc is not a non-empty array of {id,title,check} strings" >&2
44
+ return 1
45
+ fi
46
+ # `model` is the tier that should execute the step. Optional, but when present
47
+ # it must be a routable alias — a typo here would send `execution` to delegate
48
+ # on a model the Agent tool rejects, failing the step for a reason that has
49
+ # nothing to do with the step. Reject at parse time instead.
50
+ # The alias list mirrors TOOLU_MODEL_ALIASES in hooks/lib/config.sh; this lib
51
+ # stays jq-only rather than sourcing the config loader, so parity is enforced
52
+ # by __tests__/plan-ledger-model.bats.
53
+ local bad_models
54
+ if ! bad_models=$(jq -r '
55
+ [ .[] | select(.model != null)
56
+ | select((.model | type) != "string"
57
+ or ((.model) as $m
58
+ | ["haiku","sonnet","opus","fable","inherit"] | index($m) | not))
59
+ | "\(.id)=\(.model|tostring)" ] | join(", ")
60
+ ' <<< "$block" 2>/dev/null); then
61
+ # Fail closed: an unvalidated model would reach the executor as a delegation
62
+ # target, so an internal validator failure is an error, not a pass.
63
+ echo "plan-ledger-parse: could not validate step models in $doc" >&2
64
+ return 1
65
+ fi
66
+ if [ -n "$bad_models" ]; then
67
+ echo "plan-ledger-parse: invalid step model in $doc ($bad_models); allowed: haiku sonnet opus fable inherit" >&2
68
+ return 1
69
+ fi
70
+ # `paths` narrows what a step's freshness depends on. A step that declares it
71
+ # is re-run only when those paths change, instead of on every change anywhere
72
+ # in the branch diff. Wrong contents here would silently keep a step green
73
+ # after its real inputs moved, so the shape is checked rather than trusted:
74
+ # an array of non-empty strings, or absent.
75
+ local bad_paths
76
+ if ! bad_paths=$(jq -r '
77
+ [ .[] | select(.paths != null)
78
+ | select((.paths | type) != "array"
79
+ or ([.paths[] | select((type) != "string" or (. == ""))] | length) > 0)
80
+ | .id ] | join(", ")
81
+ ' <<< "$block" 2>/dev/null); then
82
+ echo "plan-ledger-parse: could not validate step paths in $doc" >&2
83
+ return 2
84
+ fi
85
+ if [ -n "$bad_paths" ]; then
86
+ echo "plan-ledger-parse: invalid step paths in $doc ($bad_paths); expected an array of non-empty strings" >&2
87
+ return 2
88
+ fi
89
+
90
+ # Emit the normalized array (jq-compacted) on stdout. Backfill the optional
91
+ # authored fields so downstream serialization always sees them: ac_refs,
92
+ # depends_on and paths default to [], input and model to null. Legacy
93
+ # {id,title,check} steps thus gain the defaults; authored values are preserved.
94
+ # Permissive `// default` also coerces a present-but-null field to its default.
95
+ jq -c 'map(
96
+ .ac_refs = (.ac_refs // []) |
97
+ .depends_on = (.depends_on // []) |
98
+ .paths = (.paths // []) |
99
+ .input = (.input // null) |
100
+ .model = (.model // null)
101
+ )' <<< "$block"
102
+ }
103
+
104
+ # pl_doc_field DOC FIELD
105
+ # Print the trimmed value of an inline-bold `**<FIELD>:**` from a packed doc
106
+ # header line, or empty string if the field (or doc) is absent. Never errors:
107
+ # a missing field is a valid "spec-less / unstamped" signal, not a failure.
108
+ #
109
+ # Header lines pack fields together, e.g.:
110
+ # **Date:** 2026-06-17 **Status:** Draft **Spec:** path.md **Topic:** …
111
+ # Extraction (first matching line wins): strip up to and including `**FIELD:**`,
112
+ # then cut from the next `**Word:**` bold key (preceded by whitespace) or EOL,
113
+ # then trim surrounding whitespace. A trailing (last-on-line) field has no
114
+ # following bold key, so it runs to EOL. The boundary requires ≥1 space before
115
+ # the next `**…:**` so a value that happens to contain `**` mid-word is not
116
+ # truncated, and both the 3-space house style and a 1-space packing are handled.
117
+ pl_doc_field() {
118
+ local doc="$1" field="$2"
119
+ [ -n "$doc" ] && [ -f "$doc" ] || return 0
120
+ [ -n "$field" ] || return 0
121
+ # First line containing the bold key, then sed the value out of it.
122
+ grep -m1 -F "**${field}:**" "$doc" 2>/dev/null \
123
+ | sed -E "s/.*\\*\\*${field}:\\*\\*[[:space:]]*//; s/[[:space:]]+\\*\\*[^*]+:\\*\\*.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//"
124
+ }
125
+
126
+ # pl_parse_acs SPEC_DOC
127
+ # Print the newline-separated list of acceptance-criterion ids (`AC-<n>`) declared
128
+ # under the spec's `## Acceptance criteria` heading, in document order, deduped.
129
+ # Ids come from the stable `**AC-<n>:**` bold prefix. A missing doc, a doc with no
130
+ # such heading, or a heading with no AC lines all yield an empty list and a zero
131
+ # return — "no ACs" is a valid state, never an error. Never writes.
132
+ pl_parse_acs() {
133
+ local doc="$1"
134
+ [ -n "$doc" ] && [ -f "$doc" ] || return 0
135
+ # awk state machine: arm inside the `## Acceptance criteria` section, disarm at
136
+ # the next `## ` heading, and emit the id from each `**AC-<n>:**` bold prefix.
137
+ # `seen[]` dedups while preserving first-seen order.
138
+ awk '
139
+ /^## Acceptance criteria[[:space:]]*$/ { in_ac = 1; next }
140
+ in_ac && /^## / { in_ac = 0 }
141
+ in_ac {
142
+ if (match($0, /\*\*AC-[0-9]+:\*\*/)) {
143
+ id = substr($0, RSTART + 2, RLENGTH - 5) # strip leading ** and trailing :**
144
+ if (!(id in seen)) { seen[id] = 1; print id }
145
+ }
146
+ }
147
+ ' "$doc"
148
+ }
149
+
150
+ # pl_check_ac_refs PLAN_DOC [SPEC_DOC]
151
+ # Print each dangling ac_ref — an `ac_refs` id in the plan's steps block that is
152
+ # NOT declared in the spec — one per line, and return non-zero iff any exist.
153
+ # When SPEC_DOC is omitted, empty, or the literal "none" (case-insensitive), the
154
+ # plan is spec-less: coverage is skipped, nothing is printed, return 0. A spec
155
+ # present but lacking an `## Acceptance criteria` section declares zero ids, so
156
+ # every non-empty ac_ref is dangling. Parse failure of the plan -> non-zero.
157
+ pl_check_ac_refs() {
158
+ local plan="$1" spec="${2:-}" steps refs acs dangling
159
+ # Spec-less: nothing to resolve against -> pass with no dangling refs.
160
+ case "$(printf '%s' "$spec" | tr '[:upper:]' '[:lower:]')" in
161
+ ""|none) return 0 ;;
162
+ esac
163
+ steps=$(pl_parse_steps "$plan") || return 1
164
+ # All referenced ids across every step, deduped (order irrelevant for set diff).
165
+ refs=$(jq -r '[.[].ac_refs[]?] | unique[]' <<< "$steps") || return 1
166
+ # No refs at all -> nothing can dangle.
167
+ [ -n "$refs" ] || return 0
168
+ acs=$(pl_parse_acs "$spec")
169
+ # Set difference: refs not present in the spec's declared ids. comm needs both
170
+ # operands sorted; refs is already unique. Empty `acs` -> all refs dangle.
171
+ dangling=$(comm -23 <(printf '%s\n' "$refs" | sort) <(printf '%s\n' "$acs" | sort)) || return 1
172
+ if [ -n "$dangling" ]; then
173
+ printf '%s\n' "$dangling"
174
+ return 1
175
+ fi
176
+ return 0
177
+ }
178
+
179
+ # pl_read_ledger STATE_FILE
180
+ # Print the ledger json to stdout. Empty or absent file -> non-zero, no output.
181
+ pl_read_ledger() {
182
+ local state_file="$1" content
183
+ if [ ! -s "$state_file" ]; then
184
+ return 1
185
+ fi
186
+ content=$(cat "$state_file" 2>/dev/null) || return 1
187
+ if ! jq -e . <<< "$content" >/dev/null 2>&1; then
188
+ return 1
189
+ fi
190
+ printf '%s\n' "$content"
191
+ }
192
+
193
+ # pl_write_ledger STATE_FILE JSON_STRING
194
+ # Validate JSON_STRING with jq, then write it atomically (temp + mv) to
195
+ # STATE_FILE, creating the parent dir if needed. Invalid json -> non-zero,
196
+ # no write and no leftover temp file.
197
+ pl_write_ledger() {
198
+ local state_file="$1" json="$2" dir tmp
199
+ if ! jq -e . <<< "$json" >/dev/null 2>&1; then
200
+ echo "plan-ledger-parse: refusing to write invalid ledger json to $state_file" >&2
201
+ return 1
202
+ fi
203
+ dir=$(dirname "$state_file")
204
+ if ! mkdir -p "$dir"; then
205
+ echo "plan-ledger-parse: cannot create ledger dir: $dir" >&2
206
+ return 1
207
+ fi
208
+ tmp="$state_file.tmp.$$"
209
+ if ! jq . <<< "$json" > "$tmp" 2>/dev/null; then
210
+ rm -f "$tmp"
211
+ echo "plan-ledger-parse: failed to stage ledger to $tmp" >&2
212
+ return 1
213
+ fi
214
+ if ! mv "$tmp" "$state_file"; then
215
+ rm -f "$tmp"
216
+ echo "plan-ledger-parse: atomic mv failed for $state_file" >&2
217
+ return 1
218
+ fi
219
+ }
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ # Plan-ledger preflight precondition gate. Sourced by plan-ledger.sh (keeps that
3
+ # file under its 500-line size budget). Pure-ish: reads the plan + spec docs and
4
+ # the ledger; runs no checks and writes nothing. jq-only via the parse lib.
5
+ #
6
+ # Depends on (already sourced by plan-ledger.sh before this file): pl_doc_field,
7
+ # pl_ledger_path, pl_read_ledger (plan-ledger-parse.sh) + detect_project_root
8
+ # (detect.sh).
9
+ #
10
+ # Source via: . "${BASH_SOURCE%/*}/plan-ledger-preflight.sh"
11
+
12
+ # pl_status_is_approved STATUS -> return 0 iff STATUS == "approved" (case-insensitive).
13
+ pl_status_is_approved() {
14
+ local s
15
+ s=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
16
+ [ "$s" = "approved" ]
17
+ }
18
+
19
+ # Default age (seconds) after which a `running` step is considered orphaned (a
20
+ # crash between the two writes wedged it). Overridable for tests.
21
+ : "${PL_STUCK_THRESHOLD:=300}"
22
+
23
+ # pl_heal_orphans LEDGER_JSON
24
+ # Reset any `running` step whose started_at is older than PL_STUCK_THRESHOLD
25
+ # seconds (or has no/empty started_at) back to `pending`, clearing started_at +
26
+ # activity. A still-fresh running step is left as-is. Prints the (possibly
27
+ # unchanged) ledger json on stdout. Pure: no I/O, no recompute.
28
+ #
29
+ # Comparison is lexical on the fixed ISO-8601 UTC format (`YYYY-MM-DDTHH:MM:SSZ`
30
+ # sorts chronologically). The cutoff = now - threshold is computed in bash via
31
+ # epoch math + reformat — portable across BSD (`date -r`) and GNU (`date -d @`),
32
+ # and immune to jq mktime's local-timezone interpretation.
33
+ pl_heal_orphans() {
34
+ local ledger="$1" now_epoch cutoff_epoch cutoff
35
+ now_epoch=$(date -u +%s)
36
+ cutoff_epoch=$((now_epoch - PL_STUCK_THRESHOLD))
37
+ cutoff=$(date -u -r "$cutoff_epoch" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
38
+ || date -u -d "@$cutoff_epoch" +%Y-%m-%dT%H:%M:%SZ) \
39
+ || return 1
40
+ jq --arg cutoff "$cutoff" '
41
+ .steps |= map(
42
+ if .status == "running"
43
+ then
44
+ (.started_at // "") as $st
45
+ | if ($st == "") or ($st < $cutoff)
46
+ then .status = "pending" | .started_at = null | .activity = null
47
+ else . end
48
+ else . end
49
+ )
50
+ ' <<< "$ledger"
51
+ }
52
+
53
+ # pl_cmd_preflight [PLAN_DOC]
54
+ # Resolve the plan doc (arg, else the current branch ledger's plan_doc). Read its
55
+ # **Status:** and **Spec:** via pl_doc_field. Pass (exit 0, silent) only when the
56
+ # plan is Approved AND (spec-less OR the declared spec is Approved).
57
+ #
58
+ # Exit codes:
59
+ # 0 preconditions met.
60
+ # 1 preconditions unmet (plan not approved / declared spec unreadable / spec
61
+ # not approved / header-less plan with empty Status). One blocker line each.
62
+ # 2 plan doc missing / unreadable, or cannot resolve a plan to check.
63
+ pl_cmd_preflight() {
64
+ local plan="${1:-}" root ledger ledger_file
65
+
66
+ root=$(detect_project_root)
67
+
68
+ # Read the current branch's ledger if present, to (a) resolve the plan_doc when
69
+ # no arg was given and (b) self-heal orphaned `running` steps so a wedged step
70
+ # can't outlive a crash. Healing rewrites the ledger; absence is not an error.
71
+ if ledger_file=$(pl_ledger_path 2>/dev/null) && ledger=$(pl_read_ledger "$ledger_file" 2>/dev/null); then
72
+ local healed
73
+ if healed=$(pl_heal_orphans "$ledger" 2>/dev/null) && [ "$healed" != "$ledger" ]; then
74
+ pl_write_ledger "$ledger_file" "$healed" 2>/dev/null || true
75
+ fi
76
+ if [ -z "$plan" ]; then
77
+ plan=$(jq -r '.plan_doc // ""' <<< "$ledger" 2>/dev/null)
78
+ fi
79
+ fi
80
+ if [ -z "$plan" ]; then
81
+ echo "preflight: no plan doc given and no ledger plan_doc to resolve" >&2
82
+ return 2
83
+ fi
84
+
85
+ # A relative plan path is resolved against repo root (matching how it's run).
86
+ local plan_abs="$plan"
87
+ case "$plan" in
88
+ /*) : ;;
89
+ *) [ -n "$root" ] && plan_abs="$root/$plan" ;;
90
+ esac
91
+ if [ ! -r "$plan_abs" ]; then
92
+ echo "preflight: plan doc not found or unreadable: $plan" >&2
93
+ return 2
94
+ fi
95
+
96
+ # Plan status. A header-less/malformed plan yields an empty Status -> blocker
97
+ # (exit 1, not 2 — the doc exists, it just isn't stamped).
98
+ local plan_status
99
+ plan_status=$(pl_doc_field "$plan_abs" Status)
100
+ if [ -z "$plan_status" ]; then
101
+ echo "preflight: plan has no **Status:** header ($plan) — run /toolu:plan-review to stamp it" >&2
102
+ return 1
103
+ fi
104
+ if ! pl_status_is_approved "$plan_status"; then
105
+ echo "preflight: plan not approved (Status: $plan_status) — run /toolu:plan-review" >&2
106
+ return 1
107
+ fi
108
+
109
+ # Spec gate. A missing **Spec:** line OR "none" is spec-less -> skip.
110
+ local spec
111
+ spec=$(pl_doc_field "$plan_abs" Spec)
112
+ local spec_lc
113
+ spec_lc=$(printf '%s' "$spec" | tr '[:upper:]' '[:lower:]')
114
+ if [ -z "$spec" ] || [ "$spec_lc" = "none" ]; then
115
+ return 0
116
+ fi
117
+
118
+ # Declared spec: resolve relative to repo root, must be readable.
119
+ local spec_abs="$spec"
120
+ case "$spec" in
121
+ /*) : ;;
122
+ *) [ -n "$root" ] && spec_abs="$root/$spec" ;;
123
+ esac
124
+ if [ ! -r "$spec_abs" ]; then
125
+ echo "preflight: declared spec not found or unreadable: $spec" >&2
126
+ return 1
127
+ fi
128
+
129
+ local spec_status
130
+ spec_status=$(pl_doc_field "$spec_abs" Status)
131
+ if ! pl_status_is_approved "$spec_status"; then
132
+ echo "preflight: spec $spec not approved (Status: ${spec_status:-none}) — run /toolu:spec-review" >&2
133
+ return 1
134
+ fi
135
+
136
+ return 0
137
+ }