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