@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,229 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { CLAUDE_THINKING_BUDGET, parsePin } from './pin.js';
4
+ import { ok, refuse, v } from './refusal.js';
5
+ export const HARNESSES = ['claude-code', 'pi'];
6
+ export const DEFAULT_HARNESS = 'claude-code';
7
+ // The six shipped stage skills, by directory name. `check`'s visibility probe asks
8
+ // whether the resolved harness can see all of them; a partial install is a warn, not
9
+ // a pass, because the missing one is always the stage you are about to reach.
10
+ export const STAGE_SKILLS = [
11
+ 'witness-brainstorm', 'witness-decompose', 'witness-design',
12
+ 'witness-implement', 'witness-plan', 'witness-ship',
13
+ ];
14
+ function parseClaudeEnvelope(stdout) {
15
+ try {
16
+ const envelope = JSON.parse(stdout);
17
+ if (typeof envelope.result !== 'string')
18
+ throw new Error('missing result');
19
+ return ok({ text: envelope.result });
20
+ }
21
+ catch {
22
+ return refuse([v('claude', 'envelope-unparseable', stdout.slice(0, 120), 'a --output-format json envelope carrying a result string')]);
23
+ }
24
+ }
25
+ // pi --mode json emits an NDJSON event stream; the terminal agent_end event carries the
26
+ // full message history. Provider failures arrive IN-stream (stopReason: "error") — the
27
+ // process can still exit 0, so exit-code checks alone cannot detect them.
28
+ function parsePiEnvelope(stdout) {
29
+ let end;
30
+ for (const line of stdout.split('\n')) {
31
+ if (!line.startsWith('{'))
32
+ continue;
33
+ try {
34
+ const evt = JSON.parse(line);
35
+ if (evt.type === 'agent_end')
36
+ end = evt;
37
+ }
38
+ catch { /* interleaved non-JSON output — skip the line, keep scanning */ }
39
+ }
40
+ const assistant = end?.messages?.filter((m) => m.role === 'assistant').at(-1);
41
+ if (assistant?.stopReason === 'error') {
42
+ return refuse([v('pi', 'reviewer-invocation', (assistant.errorMessage ?? 'provider error').slice(0, 200), 'a provider the pinned model can reach — check auth and billing for that provider')]);
43
+ }
44
+ const text = (assistant?.content ?? []).filter((c) => c.type === 'text').map((c) => c.text ?? '').join('\n');
45
+ if (text === '') {
46
+ return refuse([v('pi', 'envelope-unparseable', stdout.slice(0, 120), 'a --mode json event stream whose agent_end carries assistant text')]);
47
+ }
48
+ return ok({ text });
49
+ }
50
+ // Revision 10: a typed constant, not `harness/<name>.json`. Every consumer is
51
+ // TypeScript and the name set is closed by loadHarness's refusal, so a data file bought
52
+ // extensibility the design forbids — while costing an unchecked `as Harness` cast, a
53
+ // registry-missing path, packageRoot(), and a packaging failure mode NO test can catch:
54
+ // drop the dir from package.json `files` and every verb refuses in the published
55
+ // package while the whole suite stays green, because vitest reads from the repo root.
56
+ const REGISTRY = {
57
+ 'claude-code': {
58
+ name: 'claude-code',
59
+ launch: 'claude',
60
+ relay: '/clear',
61
+ settings: '.claude/settings.json',
62
+ bundled: true,
63
+ payload: [
64
+ { from: 'plugin/commands/witness.md', to: '.claude/commands/witness.md' },
65
+ { from: 'plugin/hooks/canon-guard.mjs', to: '.claude/hooks/canon-guard.mjs' },
66
+ { from: 'plugin/hooks/guard-state.mjs', to: '.claude/hooks/guard-state.mjs' },
67
+ { from: 'plugin/hooks/session-dashboard.sh', to: '.claude/hooks/session-dashboard.sh' },
68
+ ],
69
+ skills: { project: '.claude/skills', global: '.claude/skills' },
70
+ reviewer: {
71
+ spawn(pin) {
72
+ const args = ['-p', '--output-format', 'json'];
73
+ const env = {};
74
+ if (pin !== undefined) {
75
+ args.push('--model', pin.model);
76
+ if (pin.thinking !== 'off')
77
+ env.MAX_THINKING_TOKENS = String(CLAUDE_THINKING_BUDGET[pin.thinking]);
78
+ }
79
+ return { cmd: 'claude', args, env };
80
+ },
81
+ parseEnvelope: parseClaudeEnvelope,
82
+ },
83
+ worker: {
84
+ // claude gates every tool behind an approval prompt, which no headless run can
85
+ // answer — the bypass flag is what makes print mode able to edit at all.
86
+ spawn: (prompt) => ({ cmd: 'claude', args: ['-p', prompt, '--dangerously-skip-permissions'], env: {} }),
87
+ },
88
+ },
89
+ pi: {
90
+ name: 'pi',
91
+ launch: 'pi',
92
+ defaultProvider: 'anthropic',
93
+ relay: '/new',
94
+ bundled: false,
95
+ payload: [
96
+ { from: 'plugin/commands/witness.md', to: '.pi/prompts/witness.md' },
97
+ { from: 'plugin/hooks/canon-guard.mjs', to: '.pi/extensions/canon-guard.mjs' },
98
+ // Source-tree sibling of canon-guard.mjs, exactly as it is in .pi/extensions/:
99
+ // the adapter imports the core with ONE static relative specifier, and a
100
+ // specifier that only resolves after install is a trap no unit test can hold.
101
+ { from: 'plugin/hooks/witness-pi.ts', to: '.pi/extensions/witness.ts' },
102
+ { from: 'plugin/hooks/session-dashboard.sh', to: '.pi/extensions/session-dashboard.sh' },
103
+ ],
104
+ skills: { project: '.pi/skills', global: '.pi/agent/skills' },
105
+ // Hermetic (Decision 88): every omitted flag here is a machine-local variable that
106
+ // would silently change reviewer behavior — this machine's `defaultThinkingLevel:
107
+ // xhigh` was the probable true cause of row 87's "stalls on long prompts".
108
+ reviewer: {
109
+ spawn(pin) {
110
+ const args = ['-p', '--mode', 'json', '--no-session', '--no-extensions',
111
+ '--no-skills', '--no-context-files', '--thinking', pin?.thinking ?? 'off'];
112
+ if (pin !== undefined)
113
+ args.push('--model', `${pin.provider ?? 'anthropic'}/${pin.model}`);
114
+ return { cmd: 'pi', args, env: {} };
115
+ },
116
+ parseEnvelope: parsePiEnvelope,
117
+ },
118
+ worker: {
119
+ // No bypass flag exists or is needed: pi's built-in tools are not approval-gated
120
+ // (`pi --help` lists only --approve/--no-approve, which govern trusting
121
+ // project-local files). --no-session keeps a calibration run out of the user's
122
+ // session store; the worker otherwise keeps its skills and context files, which
123
+ // are exactly what the implement seed is measuring.
124
+ spawn: (prompt) => ({ cmd: 'pi', args: ['-p', prompt, '--no-session'], env: {} }),
125
+ },
126
+ },
127
+ };
128
+ export function loadHarness(name) {
129
+ const harness = REGISTRY[name];
130
+ if (harness === undefined) {
131
+ return refuse([v('harness', 'unknown-harness', name, HARNESSES.join(' | '))]);
132
+ }
133
+ return ok(harness);
134
+ }
135
+ const relabel = (violations, field) => violations.map((x) => ({ ...x, field }));
136
+ // Decision 5. Detection is the authority; config is the fallback. A config-authority
137
+ // default in a fresh repo emits a runnable-LOOKING, unrunnable handoff behind a warning
138
+ // that gets scrolled past — bug B2's exact shape. Detection tests PRESENCE: neither
139
+ // CLAUDECODE=1 nor PI_CODING_AGENT=true is a documented value contract.
140
+ //
141
+ // Deliberately NOT wired into loadConfig: every verb calls that, so an invalid
142
+ // `harness:` there would brick `witness check` on a key nothing read. Verbs that need
143
+ // a harness ask for one; `check` reports a malformed config value as a finding.
144
+ export function resolveHarness(env, raw) {
145
+ const override = env.WITNESS_HARNESS;
146
+ if (override !== undefined && override !== '') {
147
+ const r = loadHarness(override);
148
+ return r.ok ? ok({ harness: r.value, source: 'env' }) : refuse(relabel(r.violations, 'WITNESS_HARNESS'));
149
+ }
150
+ if (env.PI_CODING_AGENT !== undefined) {
151
+ const r = loadHarness('pi');
152
+ return r.ok ? ok({ harness: r.value, source: 'detected' }) : refuse(r.violations);
153
+ }
154
+ if (env.CLAUDECODE !== undefined) {
155
+ const r = loadHarness('claude-code');
156
+ return r.ok ? ok({ harness: r.value, source: 'detected' }) : refuse(r.violations);
157
+ }
158
+ const configured = raw.harness;
159
+ if (configured !== undefined) {
160
+ const r = loadHarness(String(configured));
161
+ return r.ok ? ok({ harness: r.value, source: 'config' }) : refuse(r.violations);
162
+ }
163
+ const r = loadHarness(DEFAULT_HARNESS);
164
+ return r.ok ? ok({ harness: r.value, source: 'default' }) : refuse(r.violations);
165
+ }
166
+ // Decision 9: the model flag is a renderer, not a string. Pi's default provider is
167
+ // `google`, so a bare `--model claude-opus-5` in a Pi handoff resolves wrong or not at
168
+ // all — and a relay session silently running an unpinned model corrupts what row 83's
169
+ // pins and the calibration story rest on.
170
+ //
171
+ // Revision 9: the provider is the HARNESS's default, never a config key.
172
+ //
173
+ // Extended by Decision 88: the flag renders over the PARSED pin, not the raw string.
174
+ // Treating the whole string as {model} emitted `--model anthropic/google/gemini-3.6-pro`
175
+ // for a provider-qualified pin — pastes cleanly, resolves to nothing: bug B2's shape.
176
+ // Pi renders the provider and its native `:thinking` suffix; claude-code renders the
177
+ // bare id and carries non-off thinking as a MAX_THINKING_TOKENS prefix on the line.
178
+ function modelArg(harness, model) {
179
+ if (model === undefined || model === '')
180
+ return '';
181
+ const parsed = parsePin('gates.model', model);
182
+ if (!parsed.ok)
183
+ return ''; // stagePin refused upstream; render nothing rather than garbage
184
+ const pin = parsed.value;
185
+ if (harness.name === 'pi') {
186
+ const suffix = pin.thinking === 'off' ? '' : `:${pin.thinking}`;
187
+ return ` --model ${pin.provider ?? harness.defaultProvider ?? ''}/${pin.model}${suffix}`;
188
+ }
189
+ return ` --model ${pin.model}`;
190
+ }
191
+ // Single quotes: a double-quoted form trips toon esc() quoting and emits an
192
+ // unpasteable line (see the note this replaces at verbs/next.ts:69).
193
+ export function handoffLine(harness, home, model) {
194
+ const parsed = model !== undefined && model !== '' ? parsePin('gates.model', model) : undefined;
195
+ const budget = harness.name === 'claude-code' && parsed?.ok === true && parsed.value.thinking !== 'off'
196
+ ? `MAX_THINKING_TOKENS=${CLAUDE_THINKING_BUDGET[parsed.value.thinking]} `
197
+ : '';
198
+ return `cd '${home}' && ${budget}${harness.launch}${modelArg(harness, model)} '/witness'`;
199
+ }
200
+ // No comma: toon's esc() quotes any value containing one (toon.ts:3), and a quoted
201
+ // `relay: "/clear, then /witness"` is what the implement skill would then print
202
+ // verbatim — the same class of defect as the double-quoted handoff note above.
203
+ export function relayLine(harness) {
204
+ return `${harness.relay} then /witness`;
205
+ }
206
+ // Decision 14. Pi resolves project skills at resolve(cwd, '.pi', 'skills') with no
207
+ // upward walk, and implement runs with cwd inside .witness/worktrees/<plan-id> — an
208
+ // untracked directory the installer never touched. A project-scope install therefore
209
+ // loses every skill in the stage that does the most work.
210
+ export function skillsVisibility(env, root, harness) {
211
+ const home = env.HOME ?? env.USERPROFILE ?? '';
212
+ const has = (dir) => STAGE_SKILLS.every((s) => existsSync(join(dir, s, 'SKILL.md')));
213
+ if (home !== '' && has(join(home, harness.skills.global)))
214
+ return 'global';
215
+ if (has(join(root, harness.skills.project)))
216
+ return 'project-only';
217
+ return 'absent';
218
+ }
219
+ // Harness-compat rung of pin validation. Grammar and alias checks live in stagePin
220
+ // (model.ts); THIS check needs the harness, which stagePin deliberately never resolves.
221
+ export function validatePin(harness, field, raw) {
222
+ const r = parsePin(field, raw);
223
+ if (!r.ok)
224
+ return r;
225
+ if (harness.name === 'claude-code' && r.value.provider !== undefined) {
226
+ return refuse([v(field, 'provider-unrunnable', raw, 'a bare model id — the claude CLI cannot run provider-qualified models')]);
227
+ }
228
+ return r;
229
+ }
@@ -0,0 +1,35 @@
1
+ import { splitDoc } from './fm.js';
2
+ import { tryGit } from './gitio.js';
3
+ import { canonicalSha } from './sha.js';
4
+ export function contentAtSha(root, rel, sha) {
5
+ const log = tryGit(root, 'log', '--format=%H', '--', rel);
6
+ if (!log.ok || log.out === '')
7
+ return undefined;
8
+ for (const commit of log.out.split('\n')) {
9
+ const shown = tryGit(root, 'show', `${commit}:${rel}`);
10
+ if (!shown.ok)
11
+ continue;
12
+ const parsed = splitDoc(shown.out + '\n');
13
+ if (!parsed.ok)
14
+ continue;
15
+ if (canonicalSha(parsed.value.meta, parsed.value.body) === sha)
16
+ return parsed.value;
17
+ }
18
+ return undefined;
19
+ }
20
+ export function baseForSpec(root, canon, specId, excludePlanId) {
21
+ const candidates = canon.docs.filter((d) => d.meta.type === 'plan' &&
22
+ d.meta.parent === specId &&
23
+ typeof d.meta['derives-from'] === 'string' &&
24
+ d.meta.id !== excludePlanId);
25
+ if (candidates.length === 0)
26
+ return { kind: 'empty' };
27
+ const stamped = candidates.map((p) => {
28
+ const res = tryGit(root, 'log', '-1', '--format=%ct', '--', p.rel);
29
+ const at = res.ok && res.out !== '' ? Number(res.out) : Number.POSITIVE_INFINITY;
30
+ return { p, at };
31
+ });
32
+ stamped.sort((a, b) => b.at - a.at || String(b.p.meta.id).localeCompare(String(a.p.meta.id)));
33
+ const latest = stamped[0].p;
34
+ return { kind: 'plan-pin', sha: String(latest.meta['derives-from']), planId: String(latest.meta.id) };
35
+ }
@@ -0,0 +1,133 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { tryGit } from './gitio.js';
5
+ import { ok, refuse, v } from './refusal.js';
6
+ // dist/install.js and src/install.ts both sit one level under the package root, so the
7
+ // same '..' works built and under vitest — the pattern model.ts:14 already uses. This is
8
+ // the only place that still needs it: locating the shipped `plugin/` tree, which is a
9
+ // genuine data dir already listed in package.json `files`.
10
+ export function packageRoot() {
11
+ return join(dirname(fileURLToPath(import.meta.url)), '..');
12
+ }
13
+ // Project-scope hook commands. ${CLAUDE_PLUGIN_ROOT} exists only for plugin installs;
14
+ // a project install anchors on $CLAUDE_PROJECT_DIR, which Claude Code substitutes in
15
+ // hook commands (verified against 2.1.220).
16
+ const GUARD_CMD = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-state.mjs"';
17
+ const DASHBOARD_CMD = 'sh "$CLAUDE_PROJECT_DIR/.claude/hooks/session-dashboard.sh"';
18
+ const PIN = /@popovych\.co\/witness@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g;
19
+ // Revision 6. Committing the payload is load-bearing: the implement stage runs with cwd
20
+ // inside .witness/worktrees/<plan-id>, which is a checkout of the branch, so only
21
+ // committed files reach it. A gitignored target therefore has exactly one honest answer
22
+ // — refuse. `git add -f` would override a rule the human wrote down; writing without
23
+ // committing would leave the payload in the primary root and the worktree with no guard,
24
+ // while every `check` reads clean. Called BEFORE the lock and before any scaffold write,
25
+ // so a refusal leaves nothing half-installed.
26
+ export function preflightPayload(root, harness) {
27
+ const ignored = [...harness.payload.map((p) => p.to), ...(harness.settings ? [harness.settings] : [])]
28
+ .filter((rel) => tryGit(root, 'check-ignore', '-q', '--', rel).ok);
29
+ if (ignored.length === 0)
30
+ return ok(undefined);
31
+ return refuse(ignored.map((rel) => v(rel, 'payload-ignored', 'matched by .gitignore', 'a committable path — worktrees are branch checkouts, so only committed payloads reach ' +
32
+ '.witness/worktrees/<plan-id>; un-ignore it, or install a different agent here')));
33
+ }
34
+ // A shipped file and an installed file that differ ONLY by version pin are the same
35
+ // file, one upgrade apart. Substituting the installed pin into the shipped text and
36
+ // comparing is exact — no heuristics, no diffing.
37
+ function pinOnlyDifference(shipped, installed) {
38
+ const installedPin = installed.match(PIN)?.[0];
39
+ if (installedPin === undefined)
40
+ return false;
41
+ return shipped.replace(PIN, installedPin) === installed;
42
+ }
43
+ // Revision 1: SYNC, not install-once. These files are witness's artifacts that happen
44
+ // to live in the user's repo, and the engine file's pin is the single point deciding
45
+ // which CLI version the entire pipeline runs — so an install-once rule pinned every repo
46
+ // to whatever version first touched it, forever, with `payload: already installed`
47
+ // printed on every attempt to fix it. A file the human actually edited is still theirs:
48
+ // report it, never clobber it.
49
+ export function installPayload(root, harness) {
50
+ const pre = preflightPayload(root, harness);
51
+ if (!pre.ok)
52
+ return refuse(pre.violations);
53
+ const out = { written: [], restamped: [], modified: [] };
54
+ for (const { from, to } of harness.payload) {
55
+ const src = join(packageRoot(), from);
56
+ if (!existsSync(src)) {
57
+ return refuse([v('payload', 'source-missing', from, 'a file shipped in the witness tarball — reinstall @popovych.co/witness')]);
58
+ }
59
+ const shipped = readFileSync(src, 'utf8');
60
+ const dst = join(root, to);
61
+ if (!existsSync(dst)) {
62
+ mkdirSync(dirname(dst), { recursive: true });
63
+ writeFileSync(dst, shipped);
64
+ out.written.push(to);
65
+ continue;
66
+ }
67
+ const installed = readFileSync(dst, 'utf8');
68
+ if (installed === shipped)
69
+ continue;
70
+ if (pinOnlyDifference(shipped, installed)) {
71
+ writeFileSync(dst, shipped);
72
+ out.restamped.push(to);
73
+ continue;
74
+ }
75
+ out.modified.push(to);
76
+ }
77
+ return ok(out);
78
+ }
79
+ function hasCommand(entries, command) {
80
+ return (entries ?? []).some((e) => (e.hooks ?? []).some((h) => h.command === command));
81
+ }
82
+ // Merge, never replace: .claude/settings.json is the human's file and may already carry
83
+ // their own hooks, model pin and permissions. We append only what is missing, matched
84
+ // by command string, and preserve every other key byte for byte.
85
+ export function mergeSettings(existing) {
86
+ let doc = {};
87
+ if (existing !== undefined && existing.trim() !== '') {
88
+ try {
89
+ doc = JSON.parse(existing);
90
+ }
91
+ catch {
92
+ // Unparseable settings are the human's problem to fix; refusing to touch them is
93
+ // safer than rewriting them. Signal "no change" and let init report it.
94
+ return { text: existing, changed: false };
95
+ }
96
+ }
97
+ const hooks = (doc.hooks ?? {});
98
+ let changed = false;
99
+ const pre = [...(hooks.PreToolUse ?? [])];
100
+ for (const matcher of ['Edit|Write|MultiEdit', 'Bash']) {
101
+ if (!pre.some((e) => e.matcher === matcher && hasCommand([e], GUARD_CMD))) {
102
+ pre.push({ matcher, hooks: [{ type: 'command', command: GUARD_CMD }] });
103
+ changed = true;
104
+ }
105
+ }
106
+ const start = [...(hooks.SessionStart ?? [])];
107
+ if (!hasCommand(start, DASHBOARD_CMD)) {
108
+ start.push({ hooks: [{ type: 'command', command: DASHBOARD_CMD }] });
109
+ changed = true;
110
+ }
111
+ if (!changed)
112
+ return { text: existing ?? '', changed: false };
113
+ doc.hooks = { ...hooks, PreToolUse: pre, SessionStart: start };
114
+ return { text: `${JSON.stringify(doc, null, 2)}\n`, changed: true };
115
+ }
116
+ // The config rung of Decision 5's ladder. Recorded once, on the run that installs the
117
+ // first payload set; a later `--agent` for a second harness leaves it alone, because
118
+ // detection outranks it anyway and rewriting config is not what that run was asked for.
119
+ export function recordHarness(configText, name) {
120
+ if (/^harness:/m.test(configText))
121
+ return { text: configText, changed: false };
122
+ const suffix = configText.endsWith('\n') ? '' : '\n';
123
+ return { text: `${configText}${suffix}harness: ${name} # detection wins; this is the fallback\n`, changed: true };
124
+ }
125
+ export function writeSettings(root, rel, text) {
126
+ const p = join(root, rel);
127
+ mkdirSync(dirname(p), { recursive: true });
128
+ writeFileSync(p, text);
129
+ }
130
+ export function readIfExists(root, rel) {
131
+ const p = join(root, rel);
132
+ return existsSync(p) ? readFileSync(p, 'utf8') : undefined;
133
+ }
@@ -0,0 +1,43 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const dir = (root) => join(root, '.witness', 'journal');
4
+ export const journalRel = (id) => `.witness/journal/${id}.jsonl`;
5
+ export function streamExists(root, id) {
6
+ return existsSync(join(root, journalRel(id)));
7
+ }
8
+ export const entryLine = (entry) => JSON.stringify({ v: 1, ...entry });
9
+ export function appendEntry(root, id, entry) {
10
+ mkdirSync(dir(root), { recursive: true });
11
+ const line = entryLine(entry);
12
+ appendFileSync(join(root, journalRel(id)), line + '\n');
13
+ return line;
14
+ }
15
+ export function readStream(root, id) {
16
+ if (!streamExists(root, id))
17
+ return [];
18
+ return readFileSync(join(root, journalRel(id)), 'utf8')
19
+ .split('\n')
20
+ .filter(Boolean)
21
+ .map((l) => JSON.parse(l));
22
+ }
23
+ export function latestRecap(root, effort) {
24
+ const recaps = readStream(root, effort).filter((e) => e.t === 'recap');
25
+ return recaps.at(-1);
26
+ }
27
+ export function effortStreams(root) {
28
+ if (!existsSync(dir(root)))
29
+ return [];
30
+ return readdirSync(dir(root))
31
+ .filter((f) => f.endsWith('.jsonl'))
32
+ .map((f) => f.slice(0, -'.jsonl'.length))
33
+ .filter((id) => readStream(root, id)[0]?.t === 'recap')
34
+ .sort();
35
+ }
36
+ export function effortAbandoned(entries) {
37
+ return entries.some((e) => e.t === 'human-decision' && e.decision === 'abandon-effort');
38
+ }
39
+ export function policyPins(entries) {
40
+ return entries
41
+ .filter((e) => e.t === 'policy-pin')
42
+ .map((e, i) => ({ ordinal: Number(e.ordinal ?? i + 1), text: String(e.text ?? '') }));
43
+ }
package/dist/junit.js ADDED
@@ -0,0 +1,54 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { globToRegExp, walkFiles } from './glob.js';
4
+ import { ok, refuse, v } from './refusal.js';
5
+ const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
6
+ function decode(s) {
7
+ return s.replace(/&(#x?[0-9a-fA-F]+|[a-z]+);/g, (whole, e) => {
8
+ if (e.startsWith('#x') || e.startsWith('#X'))
9
+ return String.fromCodePoint(Number.parseInt(e.slice(2), 16));
10
+ if (e.startsWith('#'))
11
+ return String.fromCodePoint(Number.parseInt(e.slice(1), 10));
12
+ return ENTITIES[e] ?? whole;
13
+ });
14
+ }
15
+ function attrs(tag) {
16
+ const out = {};
17
+ for (const m of tag.matchAll(/([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
18
+ out[m[1] ?? ''] = decode(m[2] ?? m[3] ?? '');
19
+ }
20
+ return out;
21
+ }
22
+ export function parseJUnit(xml) {
23
+ const clean = xml.replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, '').replace(/<!--[\s\S]*?-->/g, '');
24
+ const out = [];
25
+ const re = /<testcase\b([^>]*?)(\/)?>/g;
26
+ let m;
27
+ while ((m = re.exec(clean)) !== null) {
28
+ const a = attrs(m[1] ?? '');
29
+ let status = 'passed';
30
+ if (m[2] !== '/') {
31
+ const end = clean.indexOf('</testcase>', re.lastIndex);
32
+ const inner = clean.slice(re.lastIndex, end === -1 ? undefined : end);
33
+ if (/<(?:failure|error)\b/.test(inner))
34
+ status = 'failed';
35
+ else if (/<skipped\b/.test(inner))
36
+ status = 'skipped';
37
+ if (end !== -1)
38
+ re.lastIndex = end + '</testcase>'.length;
39
+ }
40
+ out.push({ name: a.name ?? '', classname: a.classname ?? '', status });
41
+ }
42
+ return out;
43
+ }
44
+ export function reportFiles(root, glob) {
45
+ const re = globToRegExp(glob);
46
+ return walkFiles(root).filter((f) => re.test(f));
47
+ }
48
+ export function mergeReports(root, glob) {
49
+ const files = reportFiles(root, glob);
50
+ if (files.length === 0) {
51
+ return refuse([v('criteria.report', 'no-reports', `junit:${glob} matched no files`, 'at least one report — did the suite run?')]);
52
+ }
53
+ return ok(files.flatMap((f) => parseJUnit(readFileSync(join(root, f), 'utf8'))));
54
+ }
package/dist/lock.js ADDED
@@ -0,0 +1,37 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ok, refuse, v } from './refusal.js';
4
+ const lockPath = (root) => join(root, '.witness', 'lock');
5
+ function isAlive(pid) {
6
+ try {
7
+ process.kill(pid, 0);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export function acquireLock(root, pid = process.pid) {
15
+ mkdirSync(join(root, '.witness'), { recursive: true });
16
+ const p = lockPath(root);
17
+ const release = () => rmSync(p, { force: true });
18
+ try {
19
+ writeFileSync(p, JSON.stringify({ pid }), { flag: 'wx' });
20
+ return ok(release);
21
+ }
22
+ catch {
23
+ let holder;
24
+ try {
25
+ holder = JSON.parse(readFileSync(p, 'utf8')).pid;
26
+ }
27
+ catch {
28
+ holder = undefined;
29
+ }
30
+ if (holder !== undefined && holder !== pid && isAlive(holder)) {
31
+ return refuse([v('.witness/lock', 'locked', `held by pid ${holder}`, 'one clone, one writer — wait for the other witness invocation')]);
32
+ }
33
+ rmSync(p, { force: true });
34
+ writeFileSync(p, JSON.stringify({ pid }), { flag: 'wx' });
35
+ return ok(release);
36
+ }
37
+ }
@@ -0,0 +1,52 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { globToRegExp } from './glob.js';
4
+ import { git } from './gitio.js';
5
+ export function normalizeName(s) {
6
+ return s.toLowerCase().replace(/_/g, '-');
7
+ }
8
+ const CHARSET = /[a-z0-9-]/;
9
+ const ALNUM = /[a-z0-9]/;
10
+ export function matchesTag(testName, specId) {
11
+ const n = normalizeName(testName);
12
+ for (const form of [`@spec:${specId}`, `spec-${specId}`]) {
13
+ for (let i = n.indexOf(form); i !== -1; i = n.indexOf(form, i + 1)) {
14
+ const before = i === 0 ? undefined : n[i - 1];
15
+ const after = n[i + form.length];
16
+ const beforeOk = form.startsWith('@') || before === undefined || !ALNUM.test(before);
17
+ const afterOk = after === undefined || !CHARSET.test(after);
18
+ if (beforeOk && afterOk)
19
+ return true;
20
+ }
21
+ }
22
+ return false;
23
+ }
24
+ export function extractCanonicalTags(text) {
25
+ return [...text.matchAll(/@spec:([a-z0-9-]+)/g)].map((m) => m[1] ?? '').filter(Boolean);
26
+ }
27
+ const MAX_SCAN_BYTES = 512 * 1024;
28
+ export function sourceTags(root, excludes) {
29
+ const res = { counts: new Map(), files: new Map() };
30
+ const skip = excludes.map(globToRegExp);
31
+ const listed = git(root, 'ls-files', '--cached', '--others', '--exclude-standard')
32
+ .split('\n')
33
+ .filter(Boolean);
34
+ for (const rel of new Set(listed)) {
35
+ if (skip.some((re) => re.test(rel)))
36
+ continue;
37
+ const abs = join(root, rel);
38
+ if (!existsSync(abs) || statSync(abs).size > MAX_SCAN_BYTES)
39
+ continue;
40
+ const content = readFileSync(abs, 'utf8');
41
+ if (content.includes('\0'))
42
+ continue;
43
+ for (const tag of extractCanonicalTags(content)) {
44
+ res.counts.set(tag, (res.counts.get(tag) ?? 0) + 1);
45
+ const list = res.files.get(tag) ?? [];
46
+ if (!list.includes(rel))
47
+ list.push(rel);
48
+ res.files.set(tag, list);
49
+ }
50
+ }
51
+ return res;
52
+ }
package/dist/model.js ADDED
@@ -0,0 +1,73 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parse } from 'yaml';
5
+ import { parsePin } from './pin.js';
6
+ import { ok, refuse, v } from './refusal.js';
7
+ export const MODEL_ALIASES = ['sonnet', 'opus', 'haiku', 'fable', 'default', 'latest'];
8
+ export const SESSION_DEFAULT = 'session-default';
9
+ export function shippedMatrixPath() {
10
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'calibration.yaml');
11
+ }
12
+ // Per-(harness, model) calibration (Decision 88): a pi-invoked reviewer on the same
13
+ // model id is a DIFFERENT reviewer. Legacy top-level `models:` predates the harness
14
+ // dimension and was only ever measured through claude -p — it reads as claude-code.
15
+ function readModels(path, harness) {
16
+ if (!existsSync(path))
17
+ return [];
18
+ const doc = parse(readFileSync(path, 'utf8'));
19
+ const scoped = doc?.matrices?.[harness]?.models;
20
+ if (Array.isArray(scoped))
21
+ return scoped.map(String);
22
+ return harness === 'claude-code' && Array.isArray(doc?.models) ? doc.models.map(String) : [];
23
+ }
24
+ export function loadMatrix(root, harness) {
25
+ return {
26
+ shipped: readModels(shippedMatrixPath(), harness),
27
+ local: readModels(join(root, '.witness', 'calibration.local.yaml'), harness),
28
+ };
29
+ }
30
+ // the stage's model pin from config — per-gate wins over the global gates.model;
31
+ // the same pin drives that stage's gate reviewers AND its worker agent (implement)
32
+ export function stagePin(cfg, gate) {
33
+ const gates = (cfg.raw.gates ?? {});
34
+ const gateBlock = gate !== undefined && typeof gates[gate] === 'object' && gates[gate] !== null
35
+ ? gates[gate]
36
+ : undefined;
37
+ const pinRaw = gateBlock?.model ?? gates.model;
38
+ const pinField = gateBlock?.model !== undefined ? `gates.${gate}.model` : 'gates.model';
39
+ const pin = pinRaw === undefined ? undefined : String(pinRaw);
40
+ if (pin !== undefined) {
41
+ const parsed = parsePin(pinField, pin);
42
+ if (!parsed.ok)
43
+ return refuse(parsed.violations);
44
+ if (MODEL_ALIASES.includes(parsed.value.model)) {
45
+ return refuse([v(pinField, 'alias-refused', pin, 'an exact model id — aliases re-point under the calibration (Decision 55)')]);
46
+ }
47
+ }
48
+ return ok(pin);
49
+ }
50
+ export function resolveModel(cfg, matrix, gate) {
51
+ const pinR = stagePin(cfg, gate);
52
+ if (!pinR.ok)
53
+ return refuse(pinR.violations);
54
+ const pin = pinR.value;
55
+ const calibrated = [...matrix.shipped, ...matrix.local.filter((m) => !matrix.shipped.includes(m))];
56
+ const chain = [];
57
+ if (pin !== undefined)
58
+ chain.push(pin);
59
+ for (const m of calibrated)
60
+ if (!chain.includes(m))
61
+ chain.push(m);
62
+ chain.push(SESSION_DEFAULT);
63
+ const calibrationOf = (id) => matrix.shipped.includes(id) ? 'shipped' : matrix.local.includes(id) ? 'local' : 'none';
64
+ const head = chain[0];
65
+ const headLabel = head === SESSION_DEFAULT ? '(session default)' : head;
66
+ const matrixEmpty = matrix.shipped.length === 0 && matrix.local.length === 0;
67
+ const warning = calibrationOf(head) !== 'none'
68
+ ? undefined
69
+ : matrixEmpty
70
+ ? `calibration matrix is empty — no calibrated model exists yet; ${headLabel} runs uncalibrated`
71
+ : `reviewer model ${headLabel} is below the model floor — no calibration matrix entry covers it`;
72
+ return ok({ chain, calibrationOf, warning });
73
+ }