@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,623 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { parse, stringify } from 'yaml';
7
+ import { main } from './cli.js';
8
+ import { loadConfig } from './config.js';
9
+ import { changedFiles, diffBase, evidenceForDiff } from './evidence.js';
10
+ import { readDoc, splitDoc, writeDoc } from './fm.js';
11
+ import { git, stateCommit } from './gitio.js';
12
+ import { ok, refuse, v } from './refusal.js';
13
+ import { PROMPT_NAMES, invokeReviewer, parseVerdictText, resolvePrompt } from './reviewer.js';
14
+ import { findById, loadCanon } from './scan.js';
15
+ import { anchorMenu, parseVerdict, verdictViolations } from './verdict.js';
16
+ import { createWorktree } from './worktree.js';
17
+ export function calibrationDir() {
18
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'calibration');
19
+ }
20
+ function seedList(group) {
21
+ return (dir) => {
22
+ const g = join(dir, group);
23
+ if (!existsSync(g))
24
+ return [];
25
+ return readdirSync(g)
26
+ .filter((n) => statSync(join(g, n)).isDirectory())
27
+ .sort()
28
+ .map((id) => ({
29
+ id,
30
+ overlay: join(g, id, 'overlay'),
31
+ defect: JSON.parse(readFileSync(join(g, id, 'expect.json'), 'utf8')).defect,
32
+ }));
33
+ };
34
+ }
35
+ export function loadReviewerSuite(reviewer) {
36
+ const dir = join(calibrationDir(), 'reviewers', reviewer);
37
+ if (!existsSync(join(dir, 'suite.json'))) {
38
+ return refuse([v('reviewer', 'unknown-suite', reviewer, `one of: ${PROMPT_NAMES.join(' ')}`)]);
39
+ }
40
+ const kind = JSON.parse(readFileSync(join(dir, 'suite.json'), 'utf8')).kind;
41
+ return ok({ reviewer, kind, dir, seeds: seedList('seeds')(dir), injects: seedList('inject')(dir) });
42
+ }
43
+ function walkRel(root, prefix = '') {
44
+ return readdirSync(root)
45
+ .sort()
46
+ .flatMap((name) => {
47
+ const p = join(root, name);
48
+ const rel = prefix === '' ? name : `${prefix}/${name}`;
49
+ return statSync(p).isDirectory() ? walkRel(p, rel) : [rel];
50
+ });
51
+ }
52
+ export function materialize(suite, overlay) {
53
+ const dir = mkdtempSync(join(tmpdir(), `cal-${suite.reviewer}-`));
54
+ cpSync(join(suite.dir, 'base'), dir, { recursive: true });
55
+ if (overlay !== undefined)
56
+ cpSync(overlay, dir, { recursive: true, force: true });
57
+ return { dir, files: walkRel(dir) };
58
+ }
59
+ export function composeReviewed(suite, dir, files) {
60
+ if (suite.kind === 'tree')
61
+ return { reviewed: { kind: 'tree', root: dir, files }, context: '' };
62
+ if (suite.kind === 'screens') {
63
+ const captures = files.filter((f) => f.endsWith('.png')).map((f) => ({ name: f.split('/').pop(), path: join(dir, f) }));
64
+ return { reviewed: { kind: 'screens', captures }, context: '' };
65
+ }
66
+ const docs = files
67
+ .filter((f) => f.endsWith('.md'))
68
+ .map((f) => {
69
+ const body = readFileSync(join(dir, f), 'utf8');
70
+ const filenameId = f.replace(/\.md$/, '').split('/').pop();
71
+ // Prefer the doc's own frontmatter id (matches production: gates/decompose.ts
72
+ // and gates/plan.ts both derive Reviewed.docs[].id from CanonDoc.meta.id, never
73
+ // the filename). Fixture files that carry a canon-doc id in frontmatter must
74
+ // resolve the same way calibration measures, or the model's natural anchors
75
+ // (which follow the id it reads, not the file it came from) never resolve.
76
+ const parsed = splitDoc(body);
77
+ const metaId = parsed.ok ? parsed.value.meta.id : undefined;
78
+ const id = typeof metaId === 'string' && metaId !== '' ? metaId : filenameId;
79
+ return { id, body };
80
+ });
81
+ const context = files
82
+ .filter((f) => f.endsWith('.json'))
83
+ .map((f) => `### ${f}\n\n${readFileSync(join(dir, f), 'utf8')}`)
84
+ .join('\n\n');
85
+ return { reviewed: { kind: 'docs', docs }, context };
86
+ }
87
+ export function renderReviewed(reviewed, context) {
88
+ const parts = [];
89
+ if (context !== '')
90
+ parts.push(`## Calibration context\n\n${context}`);
91
+ parts.push('## Reviewed content');
92
+ if (reviewed.kind === 'screens') {
93
+ parts.push('Read each PNG at its absolute path with the Read tool before judging:');
94
+ for (const c of reviewed.captures)
95
+ parts.push(`- ${c.name} — ${c.path}`);
96
+ return parts.join('\n\n');
97
+ }
98
+ if (reviewed.kind === 'docs') {
99
+ for (const d of reviewed.docs)
100
+ parts.push(`### ${d.id}\n\n${d.body}`);
101
+ }
102
+ else {
103
+ for (const f of reviewed.files) {
104
+ parts.push(`### ${f}\n\n\`\`\`\n${readFileSync(join(reviewed.root, f), 'utf8')}\`\`\``);
105
+ }
106
+ }
107
+ return parts.join('\n\n');
108
+ }
109
+ export function distribute(samples, buckets) {
110
+ const out = new Array(buckets).fill(0);
111
+ for (let i = 0; i < samples; i += 1)
112
+ out[i % buckets] += 1;
113
+ return out;
114
+ }
115
+ export const threshold = (samples) => Math.ceil(0.9 * samples);
116
+ export const injectSamples = (samples) => Math.max(2, Math.ceil(samples / 5));
117
+ export function runSample(ctx, harness, model, lens, suite, overlay) {
118
+ const { dir, files } = materialize(suite, overlay);
119
+ const { reviewed, context } = composeReviewed(suite, dir, files);
120
+ // same anchor-menu injection as the gate path — calibration must measure the
121
+ // prompt condition production reviewers actually see
122
+ const menu = anchorMenu(reviewed);
123
+ const prompt = `${lens}\n\n${menu ? `${menu}\n\n` : ''}${renderReviewed(reviewed, context)}\n`;
124
+ const invoked = invokeReviewer(ctx, harness, { cwd: dir, prompt, model });
125
+ if (!invoked.ok)
126
+ return invoked; // invocation-layer failure aborts the run
127
+ const raw = parseVerdictText(invoked.value.text);
128
+ if (!raw.ok)
129
+ return ok({ valid: false, blocking: 0, why: 'verdict-unparseable' });
130
+ const verdict = parseVerdict(raw.value);
131
+ if (!verdict.ok)
132
+ return ok({ valid: false, blocking: 0, why: 'verdict-shape' });
133
+ const violations = verdictViolations(verdict.value, reviewed);
134
+ if (violations.length > 0)
135
+ return ok({ valid: false, blocking: 0, why: `malformed: ${violations[0].rule}` });
136
+ const blocking = verdict.value.findings.filter((f) => f.blocking).length;
137
+ return ok({ valid: true, blocking, why: blocking > 0 ? 'blocking findings' : 'clean' });
138
+ }
139
+ function side() {
140
+ return { ok: 0, total: 0 };
141
+ }
142
+ export async function runReviewerSuite(ctx, harness, model, reviewer, samples) {
143
+ const suiteR = loadReviewerSuite(reviewer);
144
+ if (!suiteR.ok)
145
+ return suiteR;
146
+ const suite = suiteR.value;
147
+ const lensR = resolvePrompt(reviewer);
148
+ if (!lensR.ok)
149
+ return lensR;
150
+ const lens = lensR.value.contents;
151
+ const catches = side();
152
+ const clean = side();
153
+ const inject = side();
154
+ // Deterministic call order (fake-reviewer scripting relies on it):
155
+ // defect seeds (sorted, round-robin) → injects (sorted, injectSamples each) → clean runs.
156
+ const perSeed = distribute(samples, suite.seeds.length);
157
+ for (const [i, seed] of suite.seeds.entries()) {
158
+ for (let n = 0; n < perSeed[i]; n += 1) {
159
+ const s = runSample(ctx, harness, model, lens, suite, seed.overlay);
160
+ if (!s.ok)
161
+ return s;
162
+ catches.total += 1;
163
+ if (s.value.valid && s.value.blocking > 0)
164
+ catches.ok += 1;
165
+ }
166
+ }
167
+ for (const seed of suite.injects) {
168
+ for (let n = 0; n < injectSamples(samples); n += 1) {
169
+ const s = runSample(ctx, harness, model, lens, suite, seed.overlay);
170
+ if (!s.ok)
171
+ return s;
172
+ inject.total += 1;
173
+ if (s.value.valid && s.value.blocking > 0)
174
+ inject.ok += 1;
175
+ }
176
+ }
177
+ for (let n = 0; n < samples; n += 1) {
178
+ const s = runSample(ctx, harness, model, lens, suite);
179
+ if (!s.ok)
180
+ return s;
181
+ clean.total += 1;
182
+ if (s.value.valid && s.value.blocking === 0)
183
+ clean.ok += 1;
184
+ }
185
+ const pass = catches.ok >= threshold(catches.total) && clean.ok >= threshold(clean.total) && inject.ok === inject.total;
186
+ return ok({ reviewer, catches, clean, inject, pass });
187
+ }
188
+ export async function runReviewerSuites(ctx, harness, model, samples, only) {
189
+ const names = [...PROMPT_NAMES].sort().filter((n) => only === undefined || n === only);
190
+ if (names.length === 0)
191
+ return refuse([v('--only', 'unknown-reviewer', only ?? '', PROMPT_NAMES.join(' '))]);
192
+ const scores = [];
193
+ for (const name of names) {
194
+ const r = await runReviewerSuite(ctx, harness, model, name, samples);
195
+ if (!r.ok)
196
+ return r;
197
+ scores.push(r.value);
198
+ }
199
+ return ok(scores);
200
+ }
201
+ export const reportPass = (r) => r.reviewers.every((x) => x.pass) && r.skills.every((x) => x.pass);
202
+ export function aggregate(r) {
203
+ let okCount = 0;
204
+ let total = 0;
205
+ for (const s of r.reviewers) {
206
+ okCount += s.catches.ok + s.clean.ok + s.inject.ok;
207
+ total += s.catches.total + s.clean.total + s.inject.total;
208
+ }
209
+ for (const s of r.skills) {
210
+ okCount += s.ok;
211
+ total += s.total;
212
+ }
213
+ return total === 0 ? 0 : okCount / total;
214
+ }
215
+ export const localOverlayPath = (root) => join(root, '.witness', 'calibration.local.yaml');
216
+ export function addToLocalOverlay(root, model, harness) {
217
+ const path = localOverlayPath(root);
218
+ const current = existsSync(path)
219
+ ? parse(readFileSync(path, 'utf8'))
220
+ : {};
221
+ const matrices = current.matrices ?? {};
222
+ const models = matrices[harness]?.models ?? [];
223
+ if (!models.includes(model))
224
+ models.push(model);
225
+ matrices[harness] = { models };
226
+ mkdirSync(dirname(path), { recursive: true });
227
+ // legacy top-level `models:` (claude-code measurements) is preserved verbatim
228
+ writeFileSync(path, stringify({ ...current, matrices }));
229
+ }
230
+ // --- Skill-suite half (stage-skill calibration contracts) ---
231
+ export const SKILL_NAMES = ['decompose', 'plan', 'implement'];
232
+ export function loadSkillSeeds(skill) {
233
+ const dir = join(calibrationDir(), 'skills', skill, 'seeds');
234
+ return readdirSync(dir)
235
+ .filter((n) => statSync(join(dir, n)).isDirectory())
236
+ .sort()
237
+ .map((id) => {
238
+ const seedDir = join(dir, id);
239
+ const expectPath = join(seedDir, 'expect.json');
240
+ const expect = existsSync(expectPath) ? JSON.parse(readFileSync(expectPath, 'utf8')) : {};
241
+ return { id, dir: seedDir, expect };
242
+ });
243
+ }
244
+ export function skillMdPath(stage) {
245
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'plugin', 'skills', `witness-${stage}`, 'SKILL.md');
246
+ }
247
+ export const NONINTERACTIVE_OVERRIDE = `## Calibration override
248
+
249
+ You are running non-interactively for calibration. Ignore every instruction
250
+ above that says to run commands, ask questions, invoke skills, or use tools —
251
+ you cannot. Author the artifact(s) you would hand to \`witness write\` and
252
+ output ONLY this JSON object, no prose before or after it:
253
+
254
+ {"artifacts": [{"id": "<artifact-id>", "meta": { ...the manifest JSON... }, "body": "<the markdown body>"}]}
255
+
256
+ An empty set is \`{"artifacts": []}\`. The meta object is exactly what you would
257
+ put in the --meta file; the body string is exactly what you would put in the
258
+ --body file.`;
259
+ export function artifactsEnvelope(raw) {
260
+ const bad = () => refuse([v('envelope', 'shape', JSON.stringify(raw).slice(0, 200), '{ "artifacts": [{id, meta, body}] }')]);
261
+ if (typeof raw !== 'object' || raw === null)
262
+ return bad();
263
+ const artifacts = raw.artifacts;
264
+ if (!Array.isArray(artifacts))
265
+ return bad();
266
+ const out = [];
267
+ for (const a of artifacts) {
268
+ const e = a;
269
+ if (typeof e.id !== 'string' || e.id === '' || typeof e.meta !== 'object' || e.meta === null || typeof e.body !== 'string') {
270
+ return bad();
271
+ }
272
+ out.push({ id: e.id, meta: e.meta, body: e.body });
273
+ }
274
+ return ok(out);
275
+ }
276
+ // Captures a Ctx with buffered out/err so a seeded CLI call can be inspected.
277
+ // (Named captureCtx, not harness: the Harness registry owns that noun now.)
278
+ function captureCtx(cwd, env) {
279
+ const outs = [];
280
+ const errs = [];
281
+ const ctx = { cwd, env, isTTY: false, out: (l) => outs.push(l), err: (l) => errs.push(l), ask: async () => '' };
282
+ return { ctx, out: () => outs.join('\n'), err: () => errs.join('\n') };
283
+ }
284
+ export function seedScratchRepo(prefix) {
285
+ const root = mkdtempSync(join(tmpdir(), `cal-${prefix}-`));
286
+ git(root, 'init', '-b', 'main');
287
+ git(root, 'config', 'user.name', 'witness-calibration');
288
+ git(root, 'config', 'user.email', 'calibration@witness.invalid');
289
+ git(root, 'config', 'commit.gpgsign', 'false');
290
+ writeFileSync(join(root, 'witness.config.yaml'),
291
+ // No path argument at all — the only form portable across our engines range. A bare
292
+ // `tests/` arg is resolved as a module path on Node 24, and a glob is taken literally
293
+ // on Node 20 ("Could not find 'tests/**/*.test.mjs'": glob args landed in Node 21),
294
+ // which silently scores every implement sample 0. Node's default discovery already
295
+ // walks the repo for *.test.mjs and skips node_modules.
296
+ 'schema: 1\ncriteria:\n runner: \'node --test --test-name-pattern "@spec:{id}"\'\n');
297
+ writeFileSync(join(root, '.gitignore'), '.witness/lock\n.witness/allow.json\n.witness/calibration.local.yaml\n.witness/worktrees/\n');
298
+ git(root, 'add', '-A');
299
+ git(root, 'commit', '-m', 'seed');
300
+ return { root };
301
+ }
302
+ export function seedCanonDoc(root, id, content) {
303
+ const doc = splitDoc(content);
304
+ if (!doc.ok)
305
+ throw new Error(`unparseable canon seed doc: ${id}`);
306
+ const rel = `${doc.value.meta.type === 'plan' ? 'plans' : 'specs'}/${id}.md`;
307
+ writeDoc(join(root, rel), doc.value);
308
+ const res = stateCommit(root, [rel], `seed: canon(${id})`);
309
+ if (!res.ok)
310
+ throw new Error(`seed canon commit failed: ${id}: ${res.violations.map((x) => x.rule).join(',')}`);
311
+ }
312
+ export function seedStatus(root, id, status) {
313
+ const rel = existsSync(join(root, 'specs', `${id}.md`)) ? `specs/${id}.md` : `plans/${id}.md`;
314
+ const doc = readDoc(join(root, rel));
315
+ if (!doc.ok)
316
+ throw new Error(`unreadable doc for status seed: ${id}`);
317
+ writeDoc(join(root, rel), { meta: { ...doc.value.meta, status }, body: doc.value.body });
318
+ const res = stateCommit(root, [rel], `seed: status(${id}) -> ${status}`);
319
+ if (!res.ok)
320
+ throw new Error(`seed status commit failed: ${id}: ${res.violations.map((x) => x.rule).join(',')}`);
321
+ }
322
+ function firstCriterionTag(meta) {
323
+ const criteria = Array.isArray(meta.criteria) ? meta.criteria : [];
324
+ const withTest = criteria.find((c) => typeof c.test === 'string');
325
+ return withTest ? String(withTest.test).replace(/^@spec:/, '') : '';
326
+ }
327
+ export async function runDecomposeSeed(ctx, harness, model, seed) {
328
+ const { root } = seedScratchRepo(`decompose-${seed.id}`);
329
+ const canonDir = join(seed.dir, 'canon');
330
+ if (existsSync(canonDir)) {
331
+ for (const f of readdirSync(canonDir))
332
+ seedCanonDoc(root, f.replace(/\.md$/, ''), readFileSync(join(canonDir, f), 'utf8'));
333
+ }
334
+ const recapText = readFileSync(join(seed.dir, 'recap.json'), 'utf8');
335
+ const recap = JSON.parse(recapText);
336
+ writeFileSync(join(root, '.cal-recap.json'), recapText);
337
+ const recapH = captureCtx(root, ctx.env);
338
+ const recapCode = await main(recapH.ctx, ['recap', '--file', '.cal-recap.json']);
339
+ rmSync(join(root, '.cal-recap.json'), { force: true });
340
+ if (recapCode !== 0)
341
+ return ok({ ok: false, why: `seed recap failed: ${recapH.err()}` });
342
+ const indexH = captureCtx(root, ctx.env);
343
+ await main(indexH.ctx, ['index']);
344
+ const prompt = [
345
+ readFileSync(skillMdPath('decompose'), 'utf8'),
346
+ '## Calibration inputs',
347
+ `### recap.json\n\n${recapText}`,
348
+ `### witness index\n\n${indexH.out()}`,
349
+ NONINTERACTIVE_OVERRIDE,
350
+ ].join('\n\n');
351
+ const invoked = invokeReviewer(ctx, harness, { cwd: root, prompt, model });
352
+ if (!invoked.ok)
353
+ return invoked;
354
+ const raw = parseVerdictText(invoked.value.text);
355
+ if (!raw.ok)
356
+ return ok({ ok: false, why: 'envelope-unparseable' });
357
+ const envelope = artifactsEnvelope(raw.value);
358
+ if (!envelope.ok)
359
+ return ok({ ok: false, why: `envelope-shape: ${envelope.violations[0].rule}` });
360
+ const artifacts = envelope.value;
361
+ if (seed.expect.expect_empty) {
362
+ return artifacts.length === 0
363
+ ? ok({ ok: true, why: 'empty envelope as expected' })
364
+ : ok({ ok: false, why: 'expected an empty artifact set for a chore' });
365
+ }
366
+ for (const a of artifacts) {
367
+ const metaTmp = join(root, `.cal-meta-${a.id}.json`);
368
+ const bodyTmp = join(root, `.cal-body-${a.id}.md`);
369
+ writeFileSync(metaTmp, JSON.stringify(a.meta));
370
+ writeFileSync(bodyTmp, a.body);
371
+ const wH = captureCtx(root, ctx.env);
372
+ const code = await main(wH.ctx, ['write', a.id, '--effort', recap.effort, '--meta', `.cal-meta-${a.id}.json`, '--body', `.cal-body-${a.id}.md`]);
373
+ rmSync(metaTmp, { force: true });
374
+ rmSync(bodyTmp, { force: true });
375
+ if (code !== 0)
376
+ return ok({ ok: false, why: `write-refused:${a.id}: ${wH.err()}` });
377
+ }
378
+ if (seed.expect.forbid) {
379
+ const haystack = artifacts.map((a) => `${JSON.stringify(a.meta)}\n${a.body}`).join('\n').toLowerCase();
380
+ const hit = seed.expect.forbid.find((term) => haystack.includes(term.toLowerCase()));
381
+ if (hit)
382
+ return ok({ ok: false, why: `forbidden term present: ${hit}` });
383
+ }
384
+ if (seed.expect.expect_amend) {
385
+ const got = new Set(artifacts.map((a) => a.id));
386
+ const want = new Set(seed.expect.expect_amend);
387
+ if (got.size !== want.size || [...want].some((id) => !got.has(id))) {
388
+ return ok({ ok: false, why: `expected amend set {${[...want].join(',')}}, got {${[...got].join(',')}}` });
389
+ }
390
+ }
391
+ if (seed.expect.expect_extraction) {
392
+ if (artifacts.length < 3)
393
+ return ok({ ok: false, why: 'expected extraction: fewer than 3 artifacts emitted' });
394
+ const extracted = artifacts.some((a) => {
395
+ const dependents = artifacts.filter((other) => other.id !== a.id && Array.isArray(other.meta.depends) && other.meta.depends.includes(a.id));
396
+ return dependents.length >= 2;
397
+ });
398
+ if (!extracted)
399
+ return ok({ ok: false, why: 'no artifact is depended on by >=2 others — the duplicated fact was not extracted' });
400
+ }
401
+ const goalIds = new Set(recap.goals.map((g) => g.id));
402
+ const specArtifacts = artifacts.filter((a) => a.meta.type === 'spec');
403
+ const covered = new Set();
404
+ for (const a of specArtifacts) {
405
+ const covers = Array.isArray(a.meta.covers) ? a.meta.covers : [];
406
+ if (covers.length === 0)
407
+ return ok({ ok: false, why: `spec ${a.id} covers no goal` });
408
+ for (const g of covers)
409
+ covered.add(g);
410
+ }
411
+ const uncovered = [...goalIds].filter((g) => !covered.has(g));
412
+ if (uncovered.length > 0)
413
+ return ok({ ok: false, why: `goals uncovered: ${uncovered.join(',')}` });
414
+ return ok({ ok: true, why: 'first-try valid' });
415
+ }
416
+ export async function runPlanSeed(ctx, harness, model, seed) {
417
+ const { root } = seedScratchRepo(`plan-${seed.id}`);
418
+ const recapText = readFileSync(join(seed.dir, 'recap.json'), 'utf8');
419
+ const recap = JSON.parse(recapText);
420
+ writeFileSync(join(root, '.cal-recap.json'), recapText);
421
+ const recapH = captureCtx(root, ctx.env);
422
+ const recapCode = await main(recapH.ctx, ['recap', '--file', '.cal-recap.json']);
423
+ rmSync(join(root, '.cal-recap.json'), { force: true });
424
+ if (recapCode !== 0)
425
+ return ok({ ok: false, why: `seed recap failed: ${recapH.err()}` });
426
+ const parentMeta = JSON.parse(readFileSync(join(seed.dir, 'parent.meta.json'), 'utf8'));
427
+ const parentBody = readFileSync(join(seed.dir, 'parent.body.md'), 'utf8');
428
+ const parentId = firstCriterionTag(parentMeta);
429
+ writeFileSync(join(root, '.cal-parent-meta.json'), JSON.stringify(parentMeta));
430
+ writeFileSync(join(root, '.cal-parent-body.md'), parentBody);
431
+ const parentH = captureCtx(root, ctx.env);
432
+ const parentCode = await main(parentH.ctx, ['write', parentId, '--effort', recap.effort, '--meta', '.cal-parent-meta.json', '--body', '.cal-parent-body.md']);
433
+ rmSync(join(root, '.cal-parent-meta.json'), { force: true });
434
+ rmSync(join(root, '.cal-parent-body.md'), { force: true });
435
+ if (parentCode !== 0)
436
+ return ok({ ok: false, why: `seed parent write failed: ${parentH.err()}` });
437
+ seedStatus(root, parentId, 'approved');
438
+ const diffH = captureCtx(root, ctx.env);
439
+ await main(diffH.ctx, ['diff', parentId]);
440
+ const parentRendering = readFileSync(join(root, 'specs', `${parentId}.md`), 'utf8');
441
+ const prompt = [
442
+ readFileSync(skillMdPath('plan'), 'utf8'),
443
+ '## Calibration inputs',
444
+ `### recap.json\n\n${recapText}`,
445
+ `### specs/${parentId}.md\n\n${parentRendering}`,
446
+ `### witness diff ${parentId}\n\n${diffH.out()}`,
447
+ NONINTERACTIVE_OVERRIDE,
448
+ ].join('\n\n');
449
+ const invoked = invokeReviewer(ctx, harness, { cwd: root, prompt, model });
450
+ if (!invoked.ok)
451
+ return invoked;
452
+ const raw = parseVerdictText(invoked.value.text);
453
+ if (!raw.ok)
454
+ return ok({ ok: false, why: 'envelope-unparseable' });
455
+ const envelope = artifactsEnvelope(raw.value);
456
+ if (!envelope.ok)
457
+ return ok({ ok: false, why: `envelope-shape: ${envelope.violations[0].rule}` });
458
+ const artifacts = envelope.value;
459
+ const plan = artifacts[0];
460
+ if (artifacts.length !== 1 || !plan || plan.meta.type !== 'plan' || plan.meta.parent !== parentId) {
461
+ return ok({ ok: false, why: `expected exactly one plan artifact with parent ${parentId}, got ${artifacts.length} artifact(s)` });
462
+ }
463
+ const metaTmp = join(root, '.cal-plan-meta.json');
464
+ const bodyTmp = join(root, '.cal-plan-body.md');
465
+ writeFileSync(metaTmp, JSON.stringify(plan.meta));
466
+ writeFileSync(bodyTmp, plan.body);
467
+ const planH = captureCtx(root, ctx.env);
468
+ const code = await main(planH.ctx, ['write', plan.id, '--effort', recap.effort, '--meta', '.cal-plan-meta.json', '--body', '.cal-plan-body.md']);
469
+ rmSync(metaTmp, { force: true });
470
+ rmSync(bodyTmp, { force: true });
471
+ if (code !== 0)
472
+ return ok({ ok: false, why: `write-refused:${plan.id}: ${planH.err()}` });
473
+ return ok({ ok: true, why: 'first-try valid' });
474
+ }
475
+ // Decision 88 (worker half): the implement seed measures the stage skill as the
476
+ // RESOLVED harness runs it. A claude-only worker measured a reviewer-routed pipeline's
477
+ // other half on the wrong binary — and left `calibrate --only implement` unrunnable for
478
+ // a pure-pi user, the residual the routing decision exists to kill.
479
+ export async function defaultAgent(ctx, harness, worktree, prompt) {
480
+ const { cmd, args, env } = harness.worker.spawn(prompt);
481
+ spawnSync(cmd, args, {
482
+ cwd: worktree,
483
+ env: { ...ctx.env, ...env, WITNESS_BIN: `node ${join(dirname(fileURLToPath(import.meta.url)), 'bin.js')}` },
484
+ timeout: 900_000,
485
+ stdio: 'ignore',
486
+ });
487
+ }
488
+ export async function runImplementSeed(ctx, harness, seed, agent) {
489
+ const { root } = seedScratchRepo(`implement-${seed.id}`);
490
+ cpSync(join(seed.dir, 'repo'), root, { recursive: true });
491
+ git(root, 'add', '-A');
492
+ git(root, 'commit', '-m', 'seed: repo');
493
+ const recapText = readFileSync(join(seed.dir, 'recap.json'), 'utf8');
494
+ const recap = JSON.parse(recapText);
495
+ writeFileSync(join(root, '.cal-recap.json'), recapText);
496
+ const recapH = captureCtx(root, ctx.env);
497
+ const recapCode = await main(recapH.ctx, ['recap', '--file', '.cal-recap.json']);
498
+ rmSync(join(root, '.cal-recap.json'), { force: true });
499
+ if (recapCode !== 0)
500
+ return ok({ ok: false, why: `seed recap failed: ${recapH.err()}` });
501
+ const specMeta = JSON.parse(readFileSync(join(seed.dir, 'spec.meta.json'), 'utf8'));
502
+ const specBody = readFileSync(join(seed.dir, 'spec.body.md'), 'utf8');
503
+ const specId = firstCriterionTag(specMeta);
504
+ writeFileSync(join(root, '.cal-spec-meta.json'), JSON.stringify(specMeta));
505
+ writeFileSync(join(root, '.cal-spec-body.md'), specBody);
506
+ const specH = captureCtx(root, ctx.env);
507
+ const specCode = await main(specH.ctx, ['write', specId, '--effort', recap.effort, '--meta', '.cal-spec-meta.json', '--body', '.cal-spec-body.md']);
508
+ rmSync(join(root, '.cal-spec-meta.json'), { force: true });
509
+ rmSync(join(root, '.cal-spec-body.md'), { force: true });
510
+ if (specCode !== 0)
511
+ return ok({ ok: false, why: `seed spec write failed: ${specH.err()}` });
512
+ seedStatus(root, specId, 'approved');
513
+ const planMeta = JSON.parse(readFileSync(join(seed.dir, 'plan.meta.json'), 'utf8'));
514
+ const planBody = readFileSync(join(seed.dir, 'plan.body.md'), 'utf8');
515
+ const planId = `${specId}-plan-1`;
516
+ writeFileSync(join(root, '.cal-plan-meta.json'), JSON.stringify(planMeta));
517
+ writeFileSync(join(root, '.cal-plan-body.md'), planBody);
518
+ const planH = captureCtx(root, ctx.env);
519
+ const planCode = await main(planH.ctx, ['write', planId, '--effort', recap.effort, '--meta', '.cal-plan-meta.json', '--body', '.cal-plan-body.md']);
520
+ rmSync(join(root, '.cal-plan-meta.json'), { force: true });
521
+ rmSync(join(root, '.cal-plan-body.md'), { force: true });
522
+ if (planCode !== 0)
523
+ return ok({ ok: false, why: `seed plan write failed: ${planH.err()}` });
524
+ seedStatus(root, planId, 'approved');
525
+ seedStatus(root, planId, 'in-progress');
526
+ const wt = createWorktree(root, planId, 'main');
527
+ if (!wt.ok)
528
+ return wt;
529
+ const prompt = [
530
+ readFileSync(skillMdPath('implement'), 'utf8'),
531
+ planBody,
532
+ 'Your working directory IS the worktree; begin at step s1.',
533
+ ].join('\n\n');
534
+ await agent(ctx, harness, wt.value.path, prompt);
535
+ const cfg = loadConfig(wt.value.path);
536
+ if (!cfg.ok)
537
+ return cfg;
538
+ const base = diffBase(wt.value.path, cfg.value);
539
+ if (!base.ok)
540
+ return base;
541
+ // evidenceForDiff's required-tags array is empty (and .every() vacuously true) when
542
+ // nothing changed — an idle agent must not read as "satisfied" for lack of anything to check.
543
+ if (changedFiles(wt.value.path, base.value).length === 0) {
544
+ return ok({ ok: false, why: 'no changes vs base — the agent made no diff' });
545
+ }
546
+ const planDoc = findById(loadCanon(root), planId);
547
+ if (!planDoc)
548
+ return refuse([v('plan', 'seed-plan-missing', planId, 'the plan just written by the seed')]);
549
+ const report = evidenceForDiff(wt.value.path, root, planDoc, base.value);
550
+ return ok({ ok: report.satisfied, why: report.satisfied ? 'evidence satisfied' : JSON.stringify(report.required) });
551
+ }
552
+ export async function runSkillSuites(ctx, harness, model, samples, opts = {}) {
553
+ const only = opts.only;
554
+ const agent = opts.agent ?? defaultAgent;
555
+ const scores = [];
556
+ if (only === undefined || only === 'decompose') {
557
+ const seeds = loadSkillSeeds('decompose');
558
+ const perSeed = distribute(samples, seeds.length);
559
+ let okCount = 0;
560
+ let total = 0;
561
+ for (const [i, seed] of seeds.entries()) {
562
+ for (let n = 0; n < perSeed[i]; n += 1) {
563
+ const r = await runDecomposeSeed(ctx, harness, model, seed);
564
+ if (!r.ok)
565
+ return r;
566
+ total += 1;
567
+ if (r.value.ok)
568
+ okCount += 1;
569
+ }
570
+ }
571
+ scores.push({ skill: 'decompose', metric: 'first-try-valid', ok: okCount, total, pass: okCount >= threshold(total) });
572
+ }
573
+ if (only === undefined || only === 'plan') {
574
+ const seeds = loadSkillSeeds('plan');
575
+ const perSeed = distribute(samples, seeds.length);
576
+ let okCount = 0;
577
+ let total = 0;
578
+ for (const [i, seed] of seeds.entries()) {
579
+ for (let n = 0; n < perSeed[i]; n += 1) {
580
+ const r = await runPlanSeed(ctx, harness, model, seed);
581
+ if (!r.ok)
582
+ return r;
583
+ total += 1;
584
+ if (r.value.ok)
585
+ okCount += 1;
586
+ }
587
+ }
588
+ scores.push({ skill: 'plan', metric: 'first-try-valid', ok: okCount, total, pass: okCount >= threshold(total) });
589
+ }
590
+ if (only === undefined || only === 'implement') {
591
+ const seeds = loadSkillSeeds('implement');
592
+ let okCount = 0;
593
+ for (const seed of seeds) {
594
+ const r = await runImplementSeed(ctx, harness, seed, agent);
595
+ if (!r.ok)
596
+ return r;
597
+ if (r.value.ok)
598
+ okCount += 1;
599
+ }
600
+ scores.push({ skill: 'implement', metric: 'red-green-evidence', ok: okCount, total: seeds.length, pass: okCount === seeds.length });
601
+ }
602
+ return ok(scores);
603
+ }
604
+ export function publishScore(pkgRoot, model, report) {
605
+ const pkgPath = join(pkgRoot, 'package.json');
606
+ const name = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, 'utf8')).name : undefined;
607
+ if (name !== '@popovych.co/witness')
608
+ return refuse([v('--publish', 'not-package-repo', name ?? 'no package.json', 'run inside the witness repo')]);
609
+ const resultsDir = join(pkgRoot, 'calibration-results');
610
+ mkdirSync(resultsDir, { recursive: true });
611
+ const scorePath = join(resultsDir, `${model}.json`);
612
+ writeFileSync(scorePath, `${JSON.stringify({ model, aggregate: aggregate(report), report }, null, 2)}\n`);
613
+ const matrixPath = join(pkgRoot, 'calibration.yaml');
614
+ const previous = existsSync(matrixPath) ? (parse(readFileSync(matrixPath, 'utf8')).models ?? []) : [];
615
+ const scored = readdirSync(resultsDir)
616
+ .filter((f) => f.endsWith('.json'))
617
+ .map((f) => JSON.parse(readFileSync(join(resultsDir, f), 'utf8')))
618
+ .sort((a, b) => b.aggregate - a.aggregate)
619
+ .map((s) => s.model);
620
+ const models = [...scored, ...previous.filter((m) => !scored.includes(m))];
621
+ writeFileSync(matrixPath, stringify({ models }));
622
+ return ok({ path: scorePath });
623
+ }