@popovych.co/witness 0.4.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 (296) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/frontend-design-Apache-2.0.txt +177 -0
  3. package/LICENSES/mattpocock-skills-MIT.txt +21 -0
  4. package/LICENSES/no-mistakes-MIT.txt +21 -0
  5. package/LICENSES/pr-review-toolkit-Apache-2.0.txt +202 -0
  6. package/LICENSES/superpowers-MIT.txt +21 -0
  7. package/NOTICE.md +58 -0
  8. package/README.md +174 -0
  9. package/calibration/reviewers/code-reviewer/base/src/cart.mjs +30 -0
  10. package/calibration/reviewers/code-reviewer/base/tests/cart.test.mjs +27 -0
  11. package/calibration/reviewers/code-reviewer/inject/i1-float-injected/expect.json +1 -0
  12. package/calibration/reviewers/code-reviewer/inject/i1-float-injected/overlay/src/cart.mjs +31 -0
  13. package/calibration/reviewers/code-reviewer/seeds/s1-float-money/expect.json +1 -0
  14. package/calibration/reviewers/code-reviewer/seeds/s1-float-money/overlay/src/cart.mjs +30 -0
  15. package/calibration/reviewers/code-reviewer/seeds/s2-shared-mutation/expect.json +1 -0
  16. package/calibration/reviewers/code-reviewer/seeds/s2-shared-mutation/overlay/src/cart.mjs +33 -0
  17. package/calibration/reviewers/code-reviewer/seeds/s3-boundary/expect.json +1 -0
  18. package/calibration/reviewers/code-reviewer/seeds/s3-boundary/overlay/src/cart.mjs +30 -0
  19. package/calibration/reviewers/code-reviewer/seeds/s4-toctou/expect.json +1 -0
  20. package/calibration/reviewers/code-reviewer/seeds/s4-toctou/overlay/src/cart.mjs +26 -0
  21. package/calibration/reviewers/code-reviewer/suite.json +1 -0
  22. package/calibration/reviewers/design-critic/base/README +3 -0
  23. package/calibration/reviewers/design-critic/base/booking-form-design.md +18 -0
  24. package/calibration/reviewers/design-critic/base/booking-form.md +20 -0
  25. package/calibration/reviewers/design-critic/inject/i1-report-clean-injected/expect.json +1 -0
  26. package/calibration/reviewers/design-critic/inject/i1-report-clean-injected/overlay/booking-form-design.md +18 -0
  27. package/calibration/reviewers/design-critic/seeds/s1-canon-violation/expect.json +1 -0
  28. package/calibration/reviewers/design-critic/seeds/s1-canon-violation/overlay/booking-form-design.md +17 -0
  29. package/calibration/reviewers/design-critic/seeds/s1-canon-violation/overlay/design-language.md +17 -0
  30. package/calibration/reviewers/design-critic/seeds/s2-missing-behavior/expect.json +1 -0
  31. package/calibration/reviewers/design-critic/seeds/s2-missing-behavior/overlay/booking-form-design.md +17 -0
  32. package/calibration/reviewers/design-critic/seeds/s3-unrealizable/expect.json +1 -0
  33. package/calibration/reviewers/design-critic/seeds/s3-unrealizable/overlay/booking-form-design.md +19 -0
  34. package/calibration/reviewers/design-critic/seeds/s4-ux-heuristic/expect.json +1 -0
  35. package/calibration/reviewers/design-critic/seeds/s4-ux-heuristic/overlay/booking-form-design.md +18 -0
  36. package/calibration/reviewers/design-critic/suite.json +1 -0
  37. package/calibration/reviewers/design-reviewer/_src/base__new-service.html +20 -0
  38. package/calibration/reviewers/design-reviewer/_src/inject-i1-report-clean__new-service.html +22 -0
  39. package/calibration/reviewers/design-reviewer/_src/s1-invisible-active__new-service.html +20 -0
  40. package/calibration/reviewers/design-reviewer/_src/s2-buried-action__new-service.html +22 -0
  41. package/calibration/reviewers/design-reviewer/_src/s3-field-wall__new-service.html +21 -0
  42. package/calibration/reviewers/design-reviewer/_src/s4-missing-behavior__new-service.html +19 -0
  43. package/calibration/reviewers/design-reviewer/base/new-service.png +0 -0
  44. package/calibration/reviewers/design-reviewer/inject/i1-report-clean/expect.json +1 -0
  45. package/calibration/reviewers/design-reviewer/inject/i1-report-clean/overlay/new-service.png +0 -0
  46. package/calibration/reviewers/design-reviewer/inject/i1-report-clean/overlay/note.txt +1 -0
  47. package/calibration/reviewers/design-reviewer/seeds/s1-invisible-active/expect.json +1 -0
  48. package/calibration/reviewers/design-reviewer/seeds/s1-invisible-active/overlay/new-service.png +0 -0
  49. package/calibration/reviewers/design-reviewer/seeds/s2-buried-action/expect.json +1 -0
  50. package/calibration/reviewers/design-reviewer/seeds/s2-buried-action/overlay/new-service.png +0 -0
  51. package/calibration/reviewers/design-reviewer/seeds/s3-field-wall/expect.json +1 -0
  52. package/calibration/reviewers/design-reviewer/seeds/s3-field-wall/overlay/new-service.png +0 -0
  53. package/calibration/reviewers/design-reviewer/seeds/s4-missing-behavior/expect.json +1 -0
  54. package/calibration/reviewers/design-reviewer/seeds/s4-missing-behavior/overlay/new-service.png +0 -0
  55. package/calibration/reviewers/design-reviewer/suite.json +1 -0
  56. package/calibration/reviewers/drift-reviewer/base/spec.md +19 -0
  57. package/calibration/reviewers/drift-reviewer/base/src/token.mjs +18 -0
  58. package/calibration/reviewers/drift-reviewer/base/tests/token.test.mjs +9 -0
  59. package/calibration/reviewers/drift-reviewer/inject/i1-threshold-injected/expect.json +1 -0
  60. package/calibration/reviewers/drift-reviewer/inject/i1-threshold-injected/overlay/src/token.mjs +19 -0
  61. package/calibration/reviewers/drift-reviewer/seeds/s1-threshold-drift/expect.json +1 -0
  62. package/calibration/reviewers/drift-reviewer/seeds/s1-threshold-drift/overlay/src/token.mjs +18 -0
  63. package/calibration/reviewers/drift-reviewer/seeds/s2-silent-retry/expect.json +1 -0
  64. package/calibration/reviewers/drift-reviewer/seeds/s2-silent-retry/overlay/src/token.mjs +19 -0
  65. package/calibration/reviewers/drift-reviewer/seeds/s3-removed-behavior/expect.json +1 -0
  66. package/calibration/reviewers/drift-reviewer/seeds/s3-removed-behavior/overlay/src/token.mjs +9 -0
  67. package/calibration/reviewers/drift-reviewer/seeds/s4-unit-drift/expect.json +1 -0
  68. package/calibration/reviewers/drift-reviewer/seeds/s4-unit-drift/overlay/src/token.mjs +18 -0
  69. package/calibration/reviewers/drift-reviewer/suite.json +1 -0
  70. package/calibration/reviewers/plan-critic/base/delta.md +5 -0
  71. package/calibration/reviewers/plan-critic/base/parent-spec.md +23 -0
  72. package/calibration/reviewers/plan-critic/base/plan.md +27 -0
  73. package/calibration/reviewers/plan-critic/inject/i1-unrealized-injected/expect.json +1 -0
  74. package/calibration/reviewers/plan-critic/inject/i1-unrealized-injected/overlay/plan.md +28 -0
  75. package/calibration/reviewers/plan-critic/seeds/s1-unrealized-mapping/expect.json +1 -0
  76. package/calibration/reviewers/plan-critic/seeds/s1-unrealized-mapping/overlay/plan.md +26 -0
  77. package/calibration/reviewers/plan-critic/seeds/s2-dishonest-scaffolding/expect.json +1 -0
  78. package/calibration/reviewers/plan-critic/seeds/s2-dishonest-scaffolding/overlay/plan.md +26 -0
  79. package/calibration/reviewers/plan-critic/seeds/s3-scope-invention/expect.json +1 -0
  80. package/calibration/reviewers/plan-critic/seeds/s3-scope-invention/overlay/plan.md +29 -0
  81. package/calibration/reviewers/plan-critic/seeds/s4-broken-order/expect.json +1 -0
  82. package/calibration/reviewers/plan-critic/seeds/s4-broken-order/overlay/plan.md +28 -0
  83. package/calibration/reviewers/plan-critic/suite.json +1 -0
  84. package/calibration/reviewers/pr-test/base/src/discount.mjs +8 -0
  85. package/calibration/reviewers/pr-test/base/tests/discount.test.mjs +16 -0
  86. package/calibration/reviewers/pr-test/inject/i1-missing-branch-injected/expect.json +1 -0
  87. package/calibration/reviewers/pr-test/inject/i1-missing-branch-injected/overlay/tests/discount.test.mjs +15 -0
  88. package/calibration/reviewers/pr-test/seeds/s1-assertion-free/expect.json +1 -0
  89. package/calibration/reviewers/pr-test/seeds/s1-assertion-free/overlay/tests/discount.test.mjs +12 -0
  90. package/calibration/reviewers/pr-test/seeds/s2-mock-tautology/expect.json +1 -0
  91. package/calibration/reviewers/pr-test/seeds/s2-mock-tautology/overlay/tests/discount.test.mjs +14 -0
  92. package/calibration/reviewers/pr-test/seeds/s3-missing-branch/expect.json +1 -0
  93. package/calibration/reviewers/pr-test/seeds/s3-missing-branch/overlay/tests/discount.test.mjs +14 -0
  94. package/calibration/reviewers/pr-test/seeds/s4-skipped-red/expect.json +1 -0
  95. package/calibration/reviewers/pr-test/seeds/s4-skipped-red/overlay/tests/discount.test.mjs +17 -0
  96. package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/expect.json +1 -0
  97. package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/overlay/src/banner.mjs +29 -0
  98. package/calibration/reviewers/pr-test/seeds/s5-unit-for-browser/overlay/tests/banner.test.mjs +61 -0
  99. package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/expect.json +1 -0
  100. package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/src/redeem-page.mjs +24 -0
  101. package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/src/redeem-server.mjs +36 -0
  102. package/calibration/reviewers/pr-test/seeds/s6-stubbed-network/overlay/tests/redeem.e2e.test.mjs +56 -0
  103. package/calibration/reviewers/pr-test/suite.json +1 -0
  104. package/calibration/reviewers/silent-failure-hunter/base/src/sync.mjs +45 -0
  105. package/calibration/reviewers/silent-failure-hunter/base/tests/sync.test.mjs +7 -0
  106. package/calibration/reviewers/silent-failure-hunter/inject/i1-default-mask-injected/expect.json +1 -0
  107. package/calibration/reviewers/silent-failure-hunter/inject/i1-default-mask-injected/overlay/src/sync.mjs +50 -0
  108. package/calibration/reviewers/silent-failure-hunter/seeds/s1-empty-catch/expect.json +1 -0
  109. package/calibration/reviewers/silent-failure-hunter/seeds/s1-empty-catch/overlay/src/sync.mjs +42 -0
  110. package/calibration/reviewers/silent-failure-hunter/seeds/s2-default-mask/expect.json +1 -0
  111. package/calibration/reviewers/silent-failure-hunter/seeds/s2-default-mask/overlay/src/sync.mjs +49 -0
  112. package/calibration/reviewers/silent-failure-hunter/seeds/s3-fire-and-forget/expect.json +1 -0
  113. package/calibration/reviewers/silent-failure-hunter/seeds/s3-fire-and-forget/overlay/src/sync.mjs +38 -0
  114. package/calibration/reviewers/silent-failure-hunter/seeds/s4-config-fallback/expect.json +1 -0
  115. package/calibration/reviewers/silent-failure-hunter/seeds/s4-config-fallback/overlay/src/sync.mjs +49 -0
  116. package/calibration/reviewers/silent-failure-hunter/suite.json +1 -0
  117. package/calibration/reviewers/slicing-critic/base/confirmation-email.md +21 -0
  118. package/calibration/reviewers/slicing-critic/base/coverage.md +6 -0
  119. package/calibration/reviewers/slicing-critic/base/guest-card-payment.md +26 -0
  120. package/calibration/reviewers/slicing-critic/base/recap.json +14 -0
  121. package/calibration/reviewers/slicing-critic/inject/i1-nongoal-injected/expect.json +1 -0
  122. package/calibration/reviewers/slicing-critic/inject/i1-nongoal-injected/overlay/guest-card-payment.md +29 -0
  123. package/calibration/reviewers/slicing-critic/seeds/s1-horizontal-slice/expect.json +1 -0
  124. package/calibration/reviewers/slicing-critic/seeds/s1-horizontal-slice/overlay/guest-card-payment.md +23 -0
  125. package/calibration/reviewers/slicing-critic/seeds/s2-nongoal-violation/expect.json +1 -0
  126. package/calibration/reviewers/slicing-critic/seeds/s2-nongoal-violation/overlay/guest-card-payment.md +27 -0
  127. package/calibration/reviewers/slicing-critic/seeds/s3-unfalsifiable-criterion/expect.json +1 -0
  128. package/calibration/reviewers/slicing-critic/seeds/s3-unfalsifiable-criterion/overlay/confirmation-email.md +19 -0
  129. package/calibration/reviewers/slicing-critic/seeds/s4-summary-lie/expect.json +1 -0
  130. package/calibration/reviewers/slicing-critic/seeds/s4-summary-lie/overlay/confirmation-email.md +19 -0
  131. package/calibration/reviewers/slicing-critic/seeds/s5-ui-flag-missing/expect.json +1 -0
  132. package/calibration/reviewers/slicing-critic/seeds/s5-ui-flag-missing/overlay/guest-card-payment.md +28 -0
  133. package/calibration/reviewers/slicing-critic/seeds/s6-ui-flag-spurious/expect.json +1 -0
  134. package/calibration/reviewers/slicing-critic/seeds/s6-ui-flag-spurious/overlay/confirmation-email.md +22 -0
  135. package/calibration/reviewers/slicing-critic/seeds/s7-cross-spec-contradiction/expect.json +1 -0
  136. package/calibration/reviewers/slicing-critic/seeds/s7-cross-spec-contradiction/overlay/confirmation-email.md +24 -0
  137. package/calibration/reviewers/slicing-critic/suite.json +1 -0
  138. package/calibration/reviewers/type-design/base/src/order.ts +49 -0
  139. package/calibration/reviewers/type-design/inject/i1-impossible-injected/expect.json +1 -0
  140. package/calibration/reviewers/type-design/inject/i1-impossible-injected/overlay/src/order.ts +21 -0
  141. package/calibration/reviewers/type-design/seeds/s1-stringly/expect.json +1 -0
  142. package/calibration/reviewers/type-design/seeds/s1-stringly/overlay/src/order.ts +11 -0
  143. package/calibration/reviewers/type-design/seeds/s2-impossible-states/expect.json +1 -0
  144. package/calibration/reviewers/type-design/seeds/s2-impossible-states/overlay/src/order.ts +20 -0
  145. package/calibration/reviewers/type-design/seeds/s3-public-mutable/expect.json +1 -0
  146. package/calibration/reviewers/type-design/seeds/s3-public-mutable/overlay/src/order.ts +26 -0
  147. package/calibration/reviewers/type-design/seeds/s4-any-leak/expect.json +1 -0
  148. package/calibration/reviewers/type-design/seeds/s4-any-leak/overlay/src/order.ts +29 -0
  149. package/calibration/reviewers/type-design/suite.json +1 -0
  150. package/calibration/skills/decompose/seeds/s1-plain-feature/expect.json +1 -0
  151. package/calibration/skills/decompose/seeds/s1-plain-feature/recap.json +11 -0
  152. package/calibration/skills/decompose/seeds/s2-nongoal-bait/expect.json +1 -0
  153. package/calibration/skills/decompose/seeds/s2-nongoal-bait/recap.json +8 -0
  154. package/calibration/skills/decompose/seeds/s3-duplicated-fact/expect.json +1 -0
  155. package/calibration/skills/decompose/seeds/s3-duplicated-fact/recap.json +11 -0
  156. package/calibration/skills/decompose/seeds/s4-fix-routes-amend/canon/checkout-flow.md +19 -0
  157. package/calibration/skills/decompose/seeds/s4-fix-routes-amend/canon/payment-retry.md +20 -0
  158. package/calibration/skills/decompose/seeds/s4-fix-routes-amend/expect.json +1 -0
  159. package/calibration/skills/decompose/seeds/s4-fix-routes-amend/recap.json +8 -0
  160. package/calibration/skills/decompose/seeds/s5-chore-no-specs/canon/checkout-flow.md +19 -0
  161. package/calibration/skills/decompose/seeds/s5-chore-no-specs/expect.json +1 -0
  162. package/calibration/skills/decompose/seeds/s5-chore-no-specs/recap.json +8 -0
  163. package/calibration/skills/implement/seeds/s1-add-function/plan.body.md +5 -0
  164. package/calibration/skills/implement/seeds/s1-add-function/plan.meta.json +7 -0
  165. package/calibration/skills/implement/seeds/s1-add-function/recap.json +8 -0
  166. package/calibration/skills/implement/seeds/s1-add-function/repo/package.json +1 -0
  167. package/calibration/skills/implement/seeds/s1-add-function/repo/src/clamp.mjs +2 -0
  168. package/calibration/skills/implement/seeds/s1-add-function/spec.body.md +8 -0
  169. package/calibration/skills/implement/seeds/s1-add-function/spec.meta.json +8 -0
  170. package/calibration/skills/implement/seeds/s2-fix-bug/plan.body.md +6 -0
  171. package/calibration/skills/implement/seeds/s2-fix-bug/plan.meta.json +7 -0
  172. package/calibration/skills/implement/seeds/s2-fix-bug/recap.json +8 -0
  173. package/calibration/skills/implement/seeds/s2-fix-bug/repo/package.json +1 -0
  174. package/calibration/skills/implement/seeds/s2-fix-bug/repo/src/slug.mjs +7 -0
  175. package/calibration/skills/implement/seeds/s2-fix-bug/repo/tests/slug.test.mjs +7 -0
  176. package/calibration/skills/implement/seeds/s2-fix-bug/spec.body.md +9 -0
  177. package/calibration/skills/implement/seeds/s2-fix-bug/spec.meta.json +8 -0
  178. package/calibration/skills/implement/seeds/s3-two-steps/plan.body.md +11 -0
  179. package/calibration/skills/implement/seeds/s3-two-steps/plan.meta.json +10 -0
  180. package/calibration/skills/implement/seeds/s3-two-steps/recap.json +8 -0
  181. package/calibration/skills/implement/seeds/s3-two-steps/repo/package.json +1 -0
  182. package/calibration/skills/implement/seeds/s3-two-steps/repo/src/pad.mjs +2 -0
  183. package/calibration/skills/implement/seeds/s3-two-steps/spec.body.md +9 -0
  184. package/calibration/skills/implement/seeds/s3-two-steps/spec.meta.json +11 -0
  185. package/calibration/skills/plan/seeds/s1-two-criteria/expect.json +1 -0
  186. package/calibration/skills/plan/seeds/s1-two-criteria/parent.body.md +10 -0
  187. package/calibration/skills/plan/seeds/s1-two-criteria/parent.meta.json +11 -0
  188. package/calibration/skills/plan/seeds/s1-two-criteria/recap.json +8 -0
  189. package/calibration/skills/plan/seeds/s2-five-criteria/expect.json +1 -0
  190. package/calibration/skills/plan/seeds/s2-five-criteria/parent.body.md +11 -0
  191. package/calibration/skills/plan/seeds/s2-five-criteria/parent.meta.json +14 -0
  192. package/calibration/skills/plan/seeds/s2-five-criteria/recap.json +8 -0
  193. package/calibration/skills/plan/seeds/s3-mixed-needs/expect.json +1 -0
  194. package/calibration/skills/plan/seeds/s3-mixed-needs/parent.body.md +10 -0
  195. package/calibration/skills/plan/seeds/s3-mixed-needs/parent.meta.json +11 -0
  196. package/calibration/skills/plan/seeds/s3-mixed-needs/recap.json +8 -0
  197. package/calibration/skills/plan/seeds/s4-scaffolding-pull/expect.json +1 -0
  198. package/calibration/skills/plan/seeds/s4-scaffolding-pull/parent.body.md +9 -0
  199. package/calibration/skills/plan/seeds/s4-scaffolding-pull/parent.meta.json +10 -0
  200. package/calibration/skills/plan/seeds/s4-scaffolding-pull/recap.json +8 -0
  201. package/calibration.yaml +5 -0
  202. package/dist/abandon.js +154 -0
  203. package/dist/adopt.js +42 -0
  204. package/dist/allowlist.js +29 -0
  205. package/dist/bin.js +32 -0
  206. package/dist/calibrate.js +623 -0
  207. package/dist/cli.js +98 -0
  208. package/dist/config.js +155 -0
  209. package/dist/criteria.js +85 -0
  210. package/dist/design.js +104 -0
  211. package/dist/drift.js +280 -0
  212. package/dist/dsl.js +68 -0
  213. package/dist/evidence.js +221 -0
  214. package/dist/fm.js +51 -0
  215. package/dist/gate.js +371 -0
  216. package/dist/gates/decompose.js +90 -0
  217. package/dist/gates/design.js +84 -0
  218. package/dist/gates/implement.js +144 -0
  219. package/dist/gates/index.js +6 -0
  220. package/dist/gates/plan.js +86 -0
  221. package/dist/gates/ship.js +98 -0
  222. package/dist/gitio.js +97 -0
  223. package/dist/glob.js +32 -0
  224. package/dist/harness.js +229 -0
  225. package/dist/history.js +35 -0
  226. package/dist/install.js +133 -0
  227. package/dist/journal.js +43 -0
  228. package/dist/junit.js +54 -0
  229. package/dist/lock.js +37 -0
  230. package/dist/matcher.js +52 -0
  231. package/dist/model.js +73 -0
  232. package/dist/needs.js +34 -0
  233. package/dist/opener.js +38 -0
  234. package/dist/pin.js +28 -0
  235. package/dist/probe.js +14 -0
  236. package/dist/refusal.js +16 -0
  237. package/dist/reviewed.js +69 -0
  238. package/dist/reviewer.js +133 -0
  239. package/dist/rounds.js +112 -0
  240. package/dist/runner.js +73 -0
  241. package/dist/scan.js +67 -0
  242. package/dist/schemas.js +119 -0
  243. package/dist/sha.js +27 -0
  244. package/dist/ship.js +257 -0
  245. package/dist/stamp.js +104 -0
  246. package/dist/toon.js +16 -0
  247. package/dist/txn.js +70 -0
  248. package/dist/verbs/abandon.js +105 -0
  249. package/dist/verbs/adopt.js +118 -0
  250. package/dist/verbs/calibrate.js +130 -0
  251. package/dist/verbs/check.js +233 -0
  252. package/dist/verbs/clean.js +34 -0
  253. package/dist/verbs/dashboard.js +172 -0
  254. package/dist/verbs/decide.js +309 -0
  255. package/dist/verbs/design.js +203 -0
  256. package/dist/verbs/diff.js +60 -0
  257. package/dist/verbs/dispatch.js +102 -0
  258. package/dist/verbs/evidence.js +118 -0
  259. package/dist/verbs/gate.js +12 -0
  260. package/dist/verbs/index-verb.js +34 -0
  261. package/dist/verbs/init.js +190 -0
  262. package/dist/verbs/log.js +151 -0
  263. package/dist/verbs/next.js +437 -0
  264. package/dist/verbs/recap.js +145 -0
  265. package/dist/verbs/recover.js +52 -0
  266. package/dist/verbs/rename.js +99 -0
  267. package/dist/verbs/satisfy.js +69 -0
  268. package/dist/verbs/ship.js +11 -0
  269. package/dist/verbs/start.js +133 -0
  270. package/dist/verbs/sync.js +45 -0
  271. package/dist/verbs/write.js +349 -0
  272. package/dist/verdict.js +257 -0
  273. package/dist/worktree.js +73 -0
  274. package/package.json +70 -0
  275. package/plugin/.claude-plugin/plugin.json +19 -0
  276. package/plugin/commands/witness.md +46 -0
  277. package/plugin/hooks/canon-guard.mjs +105 -0
  278. package/plugin/hooks/guard-state.mjs +34 -0
  279. package/plugin/hooks/hooks.json +17 -0
  280. package/plugin/hooks/session-dashboard.sh +6 -0
  281. package/plugin/hooks/witness-pi.ts +67 -0
  282. package/plugin/skills/witness-brainstorm/SKILL.md +70 -0
  283. package/plugin/skills/witness-decompose/SKILL.md +99 -0
  284. package/plugin/skills/witness-design/SKILL.md +83 -0
  285. package/plugin/skills/witness-implement/SKILL.md +86 -0
  286. package/plugin/skills/witness-plan/SKILL.md +93 -0
  287. package/plugin/skills/witness-ship/SKILL.md +63 -0
  288. package/prompts/code-reviewer.md +64 -0
  289. package/prompts/design-critic.md +35 -0
  290. package/prompts/design-reviewer.md +70 -0
  291. package/prompts/drift-reviewer.md +40 -0
  292. package/prompts/plan-critic.md +37 -0
  293. package/prompts/pr-test.md +97 -0
  294. package/prompts/silent-failure-hunter.md +145 -0
  295. package/prompts/slicing-critic.md +46 -0
  296. package/prompts/type-design.md +113 -0
@@ -0,0 +1,172 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { EXIT, version } from '../cli.js';
3
+ import { loadConfig } from '../config.js';
4
+ import { designArtifactCurrent, designPending } from '../design.js';
5
+ import { reconcileRows } from '../drift.js';
6
+ import { DEFAULT_BATTERIES } from '../gate.js';
7
+ import { primaryRoot } from '../gitio.js';
8
+ import { resolveHarness } from '../harness.js';
9
+ import { effortAbandoned, effortStreams, latestRecap, readStream } from '../journal.js';
10
+ import { loadMatrix, resolveModel } from '../model.js';
11
+ import { computeNext, flowAction } from './next.js';
12
+ import { renderRefusal } from '../refusal.js';
13
+ import { pendingDecision } from '../rounds.js';
14
+ import { findById, loadCanon } from '../scan.js';
15
+ import { lazyStamp } from '../stamp.js';
16
+ import { kv, rows } from '../toon.js';
17
+ import { pendingTxn } from '../txn.js';
18
+ import { worktreePath } from '../worktree.js';
19
+ function tally(docs) {
20
+ const counts = new Map();
21
+ docs.forEach((d) => counts.set(String(d.meta.status), (counts.get(String(d.meta.status)) ?? 0) + 1));
22
+ return [...counts.entries()].sort().map(([s, n]) => `${s} ${n}`).join(' · ') || 'none';
23
+ }
24
+ function blockedRows(canon, ctx) {
25
+ const out = [];
26
+ for (const doc of canon.docs) {
27
+ if (doc.meta.type === 'principles')
28
+ continue;
29
+ if (doc.meta.status === 'live' || doc.meta.status === 'done' || doc.meta.status === 'abandoned')
30
+ continue;
31
+ const id = String(doc.meta.id);
32
+ const depends = Array.isArray(doc.meta.depends) ? doc.meta.depends : [];
33
+ for (const dep of depends) {
34
+ const target = findById(canon, dep);
35
+ if (!target)
36
+ out.push({ doc: id, why: `depends: ${dep} (missing)` });
37
+ else if (target.meta.status !== 'live')
38
+ out.push({ doc: id, why: `depends: ${dep} (${String(target.meta.status)})` });
39
+ }
40
+ const needs = Array.isArray(doc.meta.needs) ? doc.meta.needs : [];
41
+ for (const n of needs) {
42
+ if (typeof n.env === 'string' && !ctx.env[n.env])
43
+ out.push({ doc: id, why: `needs: ${n.env} unset` });
44
+ if (typeof n.manual === 'string' && n.satisfied !== true)
45
+ out.push({ doc: id, why: `needs: ${n.manual} unsatisfied` });
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ export async function run(ctx, _argv) {
51
+ const rootRes = primaryRoot(ctx.cwd);
52
+ if (!rootRes.ok) {
53
+ renderRefusal(rootRes.violations).forEach(ctx.err);
54
+ return EXIT.REFUSED;
55
+ }
56
+ const root = rootRes.value;
57
+ const cfg = loadConfig(root);
58
+ ctx.out(kv('witness', `${version()} · schema: ${cfg.ok ? cfg.value.schema : '?'}`));
59
+ if (cfg.ok) {
60
+ // Diagnostic surface: a broken harness config must not brick the dashboard —
61
+ // `check` reports that as a finding, so the floor lines fall back to claude-code.
62
+ const hxR = resolveHarness(ctx.env, cfg.value.raw);
63
+ const matrix = loadMatrix(root, hxR.ok ? hxR.value.harness.name : 'claude-code');
64
+ // one line per distinct warning, labeled with the gates it applies to —
65
+ // per-gate model pins can put each gate in a different calibration state
66
+ const byWarning = new Map();
67
+ for (const gate of Object.keys(DEFAULT_BATTERIES)) {
68
+ const modelR = resolveModel(cfg.value, matrix, gate);
69
+ if (modelR.ok && modelR.value.warning) {
70
+ byWarning.set(modelR.value.warning, [...(byWarning.get(modelR.value.warning) ?? []), gate]);
71
+ }
72
+ }
73
+ for (const [warning, gates] of byWarning) {
74
+ ctx.out(kv('model-floor', `${gates.join(' · ')}: ${warning}`));
75
+ }
76
+ }
77
+ const txn = pendingTxn(root);
78
+ if (txn)
79
+ ctx.out(kv('pending-txn', txn.op));
80
+ const canon0 = loadCanon(root);
81
+ const lazy = lazyStamp(root, ctx, canon0);
82
+ const canon = lazy.stamped.length > 0 ? loadCanon(root) : canon0;
83
+ const efforts = effortStreams(root).filter((slug) => !effortAbandoned(readStream(root, slug)));
84
+ const effortRows = efforts.map((slug) => {
85
+ const recap = latestRecap(root, slug);
86
+ const artifacts = new Set(readStream(root, slug).filter((e) => e.t === 'write').map((e) => String(e.artifact)));
87
+ const kinds = [...artifacts].map((a) => findById(canon, a)?.meta.type);
88
+ return {
89
+ slug,
90
+ class: recap?.class ?? '?',
91
+ specs: kinds.filter((k) => k === 'spec' || k === 'principles').length,
92
+ plans: kinds.filter((k) => k === 'plan').length,
93
+ writes: artifacts,
94
+ };
95
+ });
96
+ if (effortRows.length) {
97
+ rows('efforts', ['slug', 'class', 'specs', 'plans'], effortRows).forEach(ctx.out);
98
+ }
99
+ ctx.out(kv('canon', tally(canon.docs.filter((d) => d.rel.startsWith(`${canon.paths.specs}/`)))));
100
+ ctx.out(kv('plans', tally(canon.docs.filter((d) => d.rel.startsWith(`${canon.paths.plans}/`)))));
101
+ // In-flight flows: the orientation surface between sessions, and the one `next` no
102
+ // longer enumerates. Membership is the same predicate as next's tier 1 — a plan with
103
+ // status `in-progress` — and `stage`/`next` come from the SAME flowAction next uses,
104
+ // never re-derived. A shorthand like `pr ? 'ship' : 'implement'` is wrong in three of
105
+ // the five states a flow occupies (missing worktree, unsatisfied evidence, unsettled
106
+ // implement gate), which is unacceptable on the screen whose job is orientation.
107
+ const flowRows = cfg.ok
108
+ ? canon.docs
109
+ .filter((d) => d.meta.type === 'plan')
110
+ .sort((a, b) => String(a.meta.id).localeCompare(String(b.meta.id)))
111
+ .flatMap((d) => {
112
+ const action = flowAction(root, cfg.value, d);
113
+ if (!action)
114
+ return [];
115
+ const id = String(d.meta.id);
116
+ return [{
117
+ id,
118
+ stage: action.stage ?? 'gate',
119
+ next: action.line,
120
+ worktree: existsSync(worktreePath(root, id)) ? 'present' : 'missing',
121
+ pr: d.meta.pr ?? '—',
122
+ }];
123
+ })
124
+ : [];
125
+ if (flowRows.length) {
126
+ rows('flows', ['id', 'stage', 'next', 'worktree', 'pr'], flowRows).forEach(ctx.out);
127
+ }
128
+ const blocked = blockedRows(canon, ctx);
129
+ if (blocked.length)
130
+ rows('blocked', ['doc', 'why'], blocked).forEach(ctx.out);
131
+ const reconcile = reconcileRows(root, canon);
132
+ if (reconcile.length) {
133
+ rows('reconcile', ['spec', 'why', 'detail'], reconcile).forEach(ctx.out);
134
+ }
135
+ const designPend = canon.docs.filter((d) => d.meta.type === 'spec' && designPending(root, d))
136
+ .map((d) => ({ spec: String(d.meta.id), why: designArtifactCurrent(root, d) ? 'design gate pending' : 'design owed' }));
137
+ if (designPend.length) {
138
+ rows('design', ['spec', 'why'], designPend).forEach(ctx.out);
139
+ }
140
+ if (lazy.stale.length) {
141
+ rows('stale', ['plan', 'why'], lazy.stale).forEach(ctx.out);
142
+ }
143
+ const pendingGates = [];
144
+ for (const plan of canon.docs.filter((d) => d.meta.type === 'plan')) {
145
+ const id = String(plan.meta.id);
146
+ for (const gate of ['plan', 'implement', 'ship']) {
147
+ const p = pendingDecision(readStream(root, id), gate);
148
+ if (p)
149
+ pendingGates.push({ gate, target: id, round: p.round, outcome: p.outcome });
150
+ }
151
+ }
152
+ for (const spec of canon.docs.filter((d) => d.meta.type === 'spec')) {
153
+ const p = pendingDecision(readStream(root, String(spec.meta.id)), 'design');
154
+ if (p)
155
+ pendingGates.push({ gate: 'design', target: String(spec.meta.id), round: p.round, outcome: p.outcome });
156
+ }
157
+ for (const slug of efforts) {
158
+ const p = pendingDecision(readStream(root, slug), 'decompose');
159
+ if (p)
160
+ pendingGates.push({ gate: 'decompose', target: slug, round: p.round, outcome: p.outcome });
161
+ }
162
+ if (pendingGates.length) {
163
+ rows('gates', ['gate', 'target', 'round', 'outcome'], pendingGates).forEach(ctx.out);
164
+ }
165
+ if (canon.docs.some((d) => (Array.isArray(d.meta.needs) ? d.meta.needs : []).some((n) => typeof n.cmd === 'string'))) {
166
+ ctx.out('note: cmd needs are not executed at scan — run witness check');
167
+ }
168
+ const next = cfg.ok ? computeNext(root, ctx, canon, cfg.value).line : 'witness check';
169
+ ctx.out(`next: ${next}`);
170
+ ctx.out('help: witness check · index · diff <id> · log <id>');
171
+ return EXIT.OK;
172
+ }
@@ -0,0 +1,309 @@
1
+ import { join } from 'node:path';
2
+ import { EXIT } from '../cli.js';
3
+ import { loadConfig } from '../config.js';
4
+ import { designUnseen } from '../design.js';
5
+ import '../gates/index.js';
6
+ import { gateSpec, liveExits, renderGateRun } from '../gate.js';
7
+ import { writeDoc } from '../fm.js';
8
+ import { acquireLock } from '../lock.js';
9
+ import { crashPoint, guardTxn, withTxn } from '../txn.js';
10
+ import { appendEntry, entryLine, journalRel, policyPins, readStream, streamExists } from '../journal.js';
11
+ import { primaryRoot, stateCommit } from '../gitio.js';
12
+ import { findById, loadCanon } from '../scan.js';
13
+ import { newRunId } from '../drift.js';
14
+ import { renderRefusal, v } from '../refusal.js';
15
+ import { short } from '../sha.js';
16
+ import { kv, rows } from '../toon.js';
17
+ import { boundReached, lastGateRun, openReopen, pendingDecision, roundsSinceApprove, } from '../rounds.js';
18
+ import { prepareStamp, writeStamp } from '../stamp.js';
19
+ const asEntry = (e) => e;
20
+ function flagValue(argv, flag) {
21
+ const i = argv.indexOf(flag);
22
+ return i >= 0 ? argv[i + 1] : undefined;
23
+ }
24
+ function flagValues(argv, flag) {
25
+ return argv.flatMap((a, i) => (a === flag && argv[i + 1] !== undefined ? [argv[i + 1]] : []));
26
+ }
27
+ export async function run(ctx, argv) {
28
+ const positional = argv.filter((a, i) => !a.startsWith('--') && argv[i - 1] !== '--note' && argv[i - 1] !== '--upstream' && argv[i - 1] !== '--pin');
29
+ const [gate, target] = positional;
30
+ const spec = gate ? gateSpec(gate) : undefined;
31
+ if (!gate || !target || !spec) {
32
+ ctx.err('usage: witness decide <gate> <target> --approve|--revise|--stop [--override] [--note <t>] [--upstream <artifact|effort>] [--pin <policy>]… [--show]');
33
+ return EXIT.REFUSED;
34
+ }
35
+ const rootR = primaryRoot(ctx.cwd);
36
+ if (!rootR.ok) {
37
+ renderRefusal(rootR.violations).forEach((l) => ctx.err(l));
38
+ return EXIT.REFUSED;
39
+ }
40
+ const root = rootR.value;
41
+ const entries = readStream(root, target);
42
+ const last = lastGateRun(entries, gate);
43
+ const pending = pendingDecision(entries, gate);
44
+ if (argv.includes('--show')) {
45
+ if (!last) {
46
+ ctx.out(kv('decide', `no gate-runs for ${gate} ${target}`));
47
+ return EXIT.OK;
48
+ }
49
+ const reopen = openReopen(entries, gate);
50
+ const decisionsAfter = entries
51
+ .slice(entries.lastIndexOf(last) + 1)
52
+ .filter((e) => e.t === 'human-decision' && e.gate === gate)
53
+ .map((e) => e);
54
+ // A caused_by decision is a REOPEN, not a disposition — it can never settle the run
55
+ // above it, and pairing the two is what presented 15 settled findings as current.
56
+ const disposition = decisionsAfter.find((d) => d.caused_by === undefined);
57
+ if (reopen) {
58
+ ctx.out(kv('gate', gate));
59
+ ctx.out(kv('target', target));
60
+ ctx.out(kv('state', 'reopened — the gate must run again'));
61
+ ctx.out(kv('reopened-by', `${reopen.caused_by.gate} ${reopen.caused_by.artifact} (round ${reopen.caused_by.round})`));
62
+ if (reopen.note)
63
+ ctx.out(kv('note', reopen.note));
64
+ ctx.out(kv('last-run', `round ${last.round} @${short(last.reviewed_sha)} — ${last.outcome}${disposition ? `, ${disposition.decision}` : ''} · witness log ${target}`));
65
+ ctx.out(kv('exits', liveExits(gate, target, entries, true)));
66
+ return EXIT.OK;
67
+ }
68
+ if (disposition && disposition.decision !== 'revise' && disposition.decision !== 'revise-upstream') {
69
+ ctx.out(kv('gate', gate));
70
+ ctx.out(kv('target', target));
71
+ ctx.out(kv('state', `settled — ${disposition.decision}`));
72
+ if (disposition.note)
73
+ ctx.out(kv('note', disposition.note));
74
+ ctx.out(kv('last-run', `round ${last.round} @${short(last.reviewed_sha)} — ${last.outcome} · witness log ${target}`));
75
+ return EXIT.OK;
76
+ }
77
+ // pending (no disposition) or revise (the author's input): the verdict is actionable
78
+ renderGateRun(ctx, last, 'ran');
79
+ const shownPins = policyPins(entries);
80
+ if (shownPins.length > 0)
81
+ ctx.out(rows('pins', ['ordinal', 'text'], shownPins).join('\n'));
82
+ if (disposition) {
83
+ ctx.out(kv('decision', disposition.decision));
84
+ if (disposition.note)
85
+ ctx.out(kv('note', disposition.note));
86
+ }
87
+ ctx.out(kv('exits', liveExits(gate, target, entries, false)));
88
+ return EXIT.OK;
89
+ }
90
+ const picked = ['--approve', '--revise', '--stop'].filter((f) => argv.includes(f));
91
+ if (picked.length !== 1) {
92
+ renderRefusal([v('decision', 'one-of-required', picked.join(' ') || '(none)', '--approve | --revise | --stop')])
93
+ .forEach((l) => ctx.err(l));
94
+ return EXIT.REFUSED;
95
+ }
96
+ const decision = picked[0].slice(2);
97
+ const note = flagValue(argv, '--note');
98
+ const override = argv.includes('--override');
99
+ const upstream = flagValue(argv, '--upstream');
100
+ const pinTexts = flagValues(argv, '--pin');
101
+ if (pinTexts.length > 0 && gate !== 'implement') {
102
+ renderRefusal([v('pin', 'pin-scope', gate, 'policy pins are implement-gate decisions on a plan')]).forEach((l) => ctx.err(l));
103
+ return EXIT.REFUSED;
104
+ }
105
+ const badPin = pinTexts.find((t) => t.trim() === '' || t.length > 500);
106
+ if (badPin !== undefined) {
107
+ renderRefusal([v('pin', 'pin-empty', badPin === '' ? '(empty)' : `${badPin.length} chars`, 'non-empty policy text ≤500 chars')]).forEach((l) => ctx.err(l));
108
+ return EXIT.REFUSED;
109
+ }
110
+ const atBound = boundReached(entries, gate);
111
+ // at the bound the gate refuses to run again, so no fresh pending decision can
112
+ // ever exist — the endgame decisions must stay reachable anchored to the last
113
+ // run, or the target livelocks (incident c2692b93)
114
+ const boundEndgame = atBound && (decision === 'stop' || (decision === 'approve' && override) ||
115
+ (decision === 'revise' && upstream !== undefined));
116
+ const anchor = pending ?? (boundEndgame ? last : undefined);
117
+ if (!anchor) {
118
+ // at the bound "run the gate" is a lie — it would only short-circuit back
119
+ // here; name the decisions that actually work instead
120
+ renderRefusal([atBound && last
121
+ ? v('decision', 'bound', `${roundsSinceApprove(entries, gate)} rounds`, '--approve --override | --revise --upstream <id> | --stop (bound reached — the gate will not run again)')
122
+ : v('gate', 'nothing-pending', `${gate} ${target}`, `a stopped gate-run awaiting a decision — run: witness gate ${gate} ${target}`),
123
+ ]).forEach((l) => ctx.err(l));
124
+ return EXIT.REFUSED;
125
+ }
126
+ const blockedCode = guardTxn(ctx, root);
127
+ if (blockedCode !== undefined)
128
+ return blockedCode;
129
+ if (decision === 'revise' && atBound && upstream === undefined) {
130
+ renderRefusal([v('decision', 'bound', `${roundsSinceApprove(entries, gate)} rounds`, '--approve --override | --revise --upstream <id> | --stop (bound reached — upstream reopens the parent and resets the budget)')])
131
+ .forEach((l) => ctx.err(l));
132
+ return EXIT.REFUSED;
133
+ }
134
+ if (decision === 'approve' && atBound && !override) {
135
+ renderRefusal([v('decision', 'override-required', 'approve at the round bound', 'witness decide … --approve --override')]).forEach((l) => ctx.err(l));
136
+ return EXIT.REFUSED;
137
+ }
138
+ const cfgR = loadConfig(root);
139
+ if (!cfgR.ok) {
140
+ renderRefusal(cfgR.violations).forEach((l) => ctx.err(l));
141
+ return EXIT.REFUSED;
142
+ }
143
+ // A standing stop is only human judgment if the human was shown the thing. The gate
144
+ // refuses the same way, but the gate does not run at the round bound (gate.ts:176) —
145
+ // and approve is the act that stamps, so the line has to hold here too. Sha-keyed:
146
+ // re-authoring invalidates prior sight, as re-capturing invalidates a witnessed
147
+ // screenshot (D71). Only approve — revise and stop need no sight to be honest.
148
+ if (gate === 'design' && decision === 'approve') {
149
+ const unseen = designUnseen(root, cfgR.value.paths, target);
150
+ if (unseen !== undefined) {
151
+ renderRefusal([v('design', 'design-unseen', `no sight witnessed for ${short(unseen)}`, `a human shown this artifact — run: witness design ${target} --open`)]).forEach((l) => ctx.err(l));
152
+ return EXIT.REFUSED;
153
+ }
154
+ }
155
+ const canon = loadCanon(root);
156
+ // A `human-decision` entry is a RECORD — honest about the sha it judged whatever the
157
+ // tree does afterward, which is why D76 declined a write-time staleness check. A stamp
158
+ // is an ASSERTION about current content (`status: approved`, `design: {sha}`), and D75
159
+ // puts staleness checks at consumption. approveMeta reads the artifact from disk and
160
+ // never consults the run, so approving after a re-author blessed unreviewed bytes.
161
+ // `currentSha` is undefined when it cannot be computed (no worktree, missing parent):
162
+ // approve then proceeds, and the entry still honestly records what it judged — this
163
+ // check must never convert an unrelated condition into a misleading refusal.
164
+ if (decision === 'approve') {
165
+ const now = spec.currentSha?.(root, canon, cfgR.value, target);
166
+ if (now !== undefined && now !== anchor.reviewed_sha) {
167
+ // At the bound the gate will not re-run (gate.ts:176), so "go re-gate" would be
168
+ // the lie D67 was written about. Name the exits that actually work: approve is
169
+ // genuinely unavailable here — a human cannot honestly stamp bytes no gate read —
170
+ // but --stop and --revise --upstream both remain, so nothing livelocks.
171
+ renderRefusal([v('gate', 'stale-verdict', `verdict @${short(anchor.reviewed_sha)}, content @${short(now)}`, atBound
172
+ ? `witness decide ${gate} ${target} --revise --upstream <id> | --stop (bound reached — the gate will not re-run)`
173
+ : `a verdict describing current content — run: witness gate ${gate} ${target}`)])
174
+ .forEach((l) => ctx.err(l));
175
+ return EXIT.REFUSED;
176
+ }
177
+ }
178
+ const entry = {
179
+ v: 1, t: 'human-decision', gate, artifact: target, round: anchor.round,
180
+ decision: decision === 'revise' && upstream ? 'revise-upstream' : decision,
181
+ ...(override ? { override: true } : {}),
182
+ ...(note ? { note } : {}),
183
+ };
184
+ const priorPins = entries.filter((e) => e.t === 'policy-pin').length;
185
+ const pinEntries = pinTexts.map((text, i) => ({
186
+ v: 1, t: 'policy-pin', artifact: target, gate, round: anchor.round,
187
+ ordinal: priorPins + i + 1, text: text.trim(),
188
+ }));
189
+ const journalMulti = [{ stream: target, line: '' }];
190
+ const files = [journalRel(target)];
191
+ const stamps = decision === 'approve' ? (spec.approveStamps?.(root, canon, target) ?? []) : [];
192
+ const metaStamps = decision === 'approve' ? (spec.approveMeta?.(root, canon, cfgR.value, target) ?? []) : [];
193
+ const prepared = stamps.flatMap((s) => {
194
+ const doc = findById(canon, s.artifact);
195
+ return doc && String(doc.meta.status) !== s.to
196
+ ? [prepareStamp(doc, s.to, 'gate-approve', { run_id: newRunId() })] : [];
197
+ });
198
+ const metaWrites = metaStamps.map((m) => {
199
+ const doc = findById(canon, m.artifact);
200
+ const mEntry = { v: 1, t: m.entryType, artifact: m.artifact, ...m.patch, run_id: newRunId() };
201
+ return {
202
+ rel: doc.rel, meta: { ...doc.meta, ...m.patch }, body: doc.body, stream: m.artifact,
203
+ entry: mEntry,
204
+ line: entryLine(mEntry),
205
+ };
206
+ });
207
+ let reopen;
208
+ if (entry.decision === 'revise-upstream' && upstream) {
209
+ const upDoc = findById(canon, upstream);
210
+ if (gate === 'design') {
211
+ // upstream from the design gate is the spec's SLICING — reopen the effort's
212
+ // decompose (scope-level changes chain decompose → recap --amend, Decision 52)
213
+ if (!streamExists(root, upstream)) {
214
+ renderRefusal([v('upstream', 'unknown-effort', upstream, 'the effort slug whose decompose to reopen')]).forEach((l) => ctx.err(l));
215
+ return EXIT.REFUSED;
216
+ }
217
+ entry.upstream = { artifact: upstream, gate: 'decompose' };
218
+ reopen = {
219
+ stream: upstream,
220
+ entry: {
221
+ v: 1, t: 'human-decision', gate: 'decompose', artifact: upstream, round: anchor.round,
222
+ decision: 'revise', caused_by: { artifact: target, gate, round: anchor.round },
223
+ ...(note ? { note } : {}),
224
+ },
225
+ };
226
+ }
227
+ else if (gate === 'decompose') {
228
+ // upstream from decompose is the scope itself — no second stream, hand off to recap --amend
229
+ entry.upstream = { artifact: target, gate: 'recap' };
230
+ }
231
+ else if (!upDoc) {
232
+ renderRefusal([v('upstream', 'unknown-artifact', upstream, 'a canon doc id')]).forEach((l) => ctx.err(l));
233
+ return EXIT.REFUSED;
234
+ }
235
+ else {
236
+ const upGate = upDoc.meta.type === 'plan' ? 'plan' : 'decompose';
237
+ entry.upstream = { artifact: upstream, gate: upGate };
238
+ reopen = {
239
+ stream: upstream,
240
+ entry: {
241
+ v: 1, t: 'human-decision', gate: upGate, artifact: upstream, round: anchor.round,
242
+ decision: 'revise', caused_by: { artifact: target, gate, round: anchor.round },
243
+ ...(note ? { note } : {}),
244
+ },
245
+ };
246
+ }
247
+ }
248
+ journalMulti[0].line = entryLine(asEntry(entry));
249
+ for (const p of pinEntries)
250
+ journalMulti.push({ stream: target, line: entryLine(p) });
251
+ if (reopen) {
252
+ journalMulti.push({ stream: reopen.stream, line: entryLine(asEntry(reopen.entry)) });
253
+ files.push(journalRel(reopen.stream));
254
+ }
255
+ for (const s of prepared) {
256
+ files.push(s.rel, journalRel(s.stream));
257
+ journalMulti.push({ stream: s.stream, line: s.line });
258
+ }
259
+ for (const m of metaWrites) {
260
+ files.push(m.rel, journalRel(m.stream));
261
+ journalMulti.push({ stream: m.stream, line: m.line });
262
+ }
263
+ const lockR = acquireLock(root);
264
+ if (!lockR.ok) {
265
+ renderRefusal(lockR.violations).forEach((l) => ctx.err(l));
266
+ return EXIT.BLOCKED;
267
+ }
268
+ try {
269
+ const txn = withTxn(root, { op: `decide-${gate}`, files: [...new Set(files)], journalMulti }, () => {
270
+ appendEntry(root, target, asEntry(entry));
271
+ for (const p of pinEntries)
272
+ appendEntry(root, target, p);
273
+ if (reopen)
274
+ appendEntry(root, reopen.stream, asEntry(reopen.entry));
275
+ for (const s of prepared)
276
+ writeStamp(root, s);
277
+ for (const m of metaWrites) {
278
+ writeDoc(join(root, m.rel), { meta: m.meta, body: m.body });
279
+ appendEntry(root, m.stream, m.entry);
280
+ }
281
+ crashPoint(ctx.env, 'decide-journal');
282
+ return stateCommit(root, [...new Set(files)], `decide(${gate}): ${target} ${entry.decision}`);
283
+ });
284
+ if (!txn.ok) {
285
+ renderRefusal(txn.violations).forEach((l) => ctx.err(l));
286
+ return EXIT.REFUSED;
287
+ }
288
+ }
289
+ finally {
290
+ lockR.value();
291
+ }
292
+ ctx.out(kv('decided', `${gate} ${target} → ${entry.decision}${override ? ' (override)' : ''}`));
293
+ for (const p of pinEntries)
294
+ ctx.out(kv('pinned', `#${p.ordinal} ${p.text}`));
295
+ if (entry.decision === 'revise' || entry.decision === 'revise-upstream') {
296
+ ctx.out('revise-context: (reconstructed from the journal — survives session death)');
297
+ renderGateRun(ctx, anchor, 'ran');
298
+ const pins = policyPins(readStream(root, target));
299
+ if (pins.length > 0)
300
+ ctx.out(rows('pins', ['ordinal', 'text'], pins).join('\n'));
301
+ if (note)
302
+ ctx.out(kv('note', note));
303
+ if (gate === 'decompose')
304
+ ctx.out(`help: scope itself implicated → witness recap --amend ${target}`);
305
+ else if (entry.upstream)
306
+ ctx.out(`help: reopened ${entry.upstream.artifact} (${entry.upstream.gate} stage) — linked via caused_by`);
307
+ }
308
+ return EXIT.OK;
309
+ }