@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,105 @@
|
|
|
1
|
+
import { EXIT } from '../cli.js';
|
|
2
|
+
import { guardTxn } from '../txn.js';
|
|
3
|
+
import { primaryRoot } from '../gitio.js';
|
|
4
|
+
import { findById, loadCanon } from '../scan.js';
|
|
5
|
+
import { effortAbandoned, latestRecap, readStream, streamExists } from '../journal.js';
|
|
6
|
+
import { effortWrites } from '../reviewed.js';
|
|
7
|
+
import { canonicalSha } from '../sha.js';
|
|
8
|
+
import { renderRefusal, v } from '../refusal.js';
|
|
9
|
+
import { kv } from '../toon.js';
|
|
10
|
+
import { executeAbandon, planItems, specRevertFor } from '../abandon.js';
|
|
11
|
+
export async function run(ctx, argv) {
|
|
12
|
+
const target = argv.find((a) => !a.startsWith('--'));
|
|
13
|
+
if (!target) {
|
|
14
|
+
ctx.err('usage: witness abandon <plan-id | effort-slug>');
|
|
15
|
+
return EXIT.REFUSED;
|
|
16
|
+
}
|
|
17
|
+
const rootR = primaryRoot(ctx.cwd);
|
|
18
|
+
if (!rootR.ok) {
|
|
19
|
+
renderRefusal(rootR.violations).forEach((l) => ctx.err(l));
|
|
20
|
+
return EXIT.REFUSED;
|
|
21
|
+
}
|
|
22
|
+
const root = rootR.value;
|
|
23
|
+
const blocked = guardTxn(ctx, root);
|
|
24
|
+
if (blocked !== undefined)
|
|
25
|
+
return blocked;
|
|
26
|
+
const canon = loadCanon(root);
|
|
27
|
+
const doc = findById(canon, target);
|
|
28
|
+
if (doc && doc.meta.type === 'plan') {
|
|
29
|
+
if (String(doc.meta.status) === 'abandoned') {
|
|
30
|
+
ctx.out(kv('abandon', `${target} already abandoned`));
|
|
31
|
+
return EXIT.OK;
|
|
32
|
+
}
|
|
33
|
+
const itemR = planItems(root, canon, doc, new Set([target]), 'pair-parent');
|
|
34
|
+
if (!itemR.ok) {
|
|
35
|
+
renderRefusal(itemR.violations).forEach((l) => ctx.err(l));
|
|
36
|
+
return EXIT.REFUSED;
|
|
37
|
+
}
|
|
38
|
+
const done = executeAbandon(root, ctx, [itemR.value], undefined, `abandon(${target})`);
|
|
39
|
+
if (!done.ok) {
|
|
40
|
+
renderRefusal(done.violations).forEach((l) => ctx.err(l));
|
|
41
|
+
return EXIT.REFUSED;
|
|
42
|
+
}
|
|
43
|
+
ctx.out(kv('abandoned', target));
|
|
44
|
+
if (itemR.value.revert) {
|
|
45
|
+
ctx.out(kv('reverted', `${String(itemR.value.revert.doc.meta.id)} (${itemR.value.revert.action}) — journal untouched`));
|
|
46
|
+
}
|
|
47
|
+
return EXIT.OK;
|
|
48
|
+
}
|
|
49
|
+
if (streamExists(root, target) && latestRecap(root, target)) {
|
|
50
|
+
const entries = readStream(root, target);
|
|
51
|
+
if (effortAbandoned(entries)) {
|
|
52
|
+
ctx.out(kv('abandon', `${target} already abandoned`));
|
|
53
|
+
return EXIT.OK;
|
|
54
|
+
}
|
|
55
|
+
const writes = effortWrites(root, target);
|
|
56
|
+
const abandonSet = new Set(writes.keys());
|
|
57
|
+
const items = [];
|
|
58
|
+
const problems = [];
|
|
59
|
+
for (const id of writes.keys()) {
|
|
60
|
+
const artifact = findById(canon, id);
|
|
61
|
+
if (!artifact)
|
|
62
|
+
continue; // already gone (re-slice, earlier abandon)
|
|
63
|
+
if (artifact.meta.type !== 'plan')
|
|
64
|
+
continue;
|
|
65
|
+
// 'stamp-only': the spec loop below is this path's sole spec revert, pinned on
|
|
66
|
+
// THIS effort's write sha. A paired revert here would re-derive the owner and
|
|
67
|
+
// could land on a sibling effort's amendment — see Pairing in abandon.ts.
|
|
68
|
+
const itemR = planItems(root, canon, artifact, abandonSet, 'stamp-only');
|
|
69
|
+
if (!itemR.ok) {
|
|
70
|
+
problems.push(...itemR.violations);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
items.push(itemR.value);
|
|
74
|
+
}
|
|
75
|
+
for (const [id, w] of writes) {
|
|
76
|
+
const artifact = findById(canon, id);
|
|
77
|
+
if (!artifact || artifact.meta.type !== 'spec')
|
|
78
|
+
continue;
|
|
79
|
+
if (canonicalSha(artifact.meta, artifact.body) !== w.sha) {
|
|
80
|
+
problems.push(v('spec', 'stacked-amendment', id, 'the effort\'s write is no longer the newest'));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const revertR = specRevertFor(root, canon, artifact, w.sha, abandonSet);
|
|
84
|
+
if (!revertR.ok) {
|
|
85
|
+
problems.push(...revertR.violations);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (revertR.value)
|
|
89
|
+
items.push({ revert: revertR.value });
|
|
90
|
+
}
|
|
91
|
+
if (problems.length > 0) {
|
|
92
|
+
renderRefusal(problems).forEach((l) => ctx.err(l));
|
|
93
|
+
return EXIT.REFUSED;
|
|
94
|
+
}
|
|
95
|
+
const done = executeAbandon(root, ctx, items, { effort: target }, `abandon(${target})`);
|
|
96
|
+
if (!done.ok) {
|
|
97
|
+
renderRefusal(done.violations).forEach((l) => ctx.err(l));
|
|
98
|
+
return EXIT.REFUSED;
|
|
99
|
+
}
|
|
100
|
+
ctx.out(kv('abandoned', `effort ${target} — ${items.length} artifacts walked, journal remembers the attempt`));
|
|
101
|
+
return EXIT.OK;
|
|
102
|
+
}
|
|
103
|
+
renderRefusal([v('target', 'unknown-target', target, 'a plan id or an effort slug')]).forEach((l) => ctx.err(l));
|
|
104
|
+
return EXIT.REFUSED;
|
|
105
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { adoptedCommits, lastWitnessed, untrailedCommitsFor } from '../adopt.js';
|
|
3
|
+
import { EXIT } from '../cli.js';
|
|
4
|
+
import { canonPaths } from '../config.js';
|
|
5
|
+
import { runCriteria } from '../criteria.js';
|
|
6
|
+
import { clearDrift, stampDrift } from '../drift.js';
|
|
7
|
+
import { readDoc } from '../fm.js';
|
|
8
|
+
import { dirtyStatePaths, primaryRoot, stateCommit } from '../gitio.js';
|
|
9
|
+
import { appendEntry, entryLine, journalRel } from '../journal.js';
|
|
10
|
+
import { acquireLock } from '../lock.js';
|
|
11
|
+
import { renderRefusal, v } from '../refusal.js';
|
|
12
|
+
import { findById, findCycle, loadCanon } from '../scan.js';
|
|
13
|
+
import { validateDoc } from '../schemas.js';
|
|
14
|
+
import { canonicalSha, short } from '../sha.js';
|
|
15
|
+
import { kv } from '../toon.js';
|
|
16
|
+
import { crashPoint, guardTxn, withTxn } from '../txn.js';
|
|
17
|
+
export async function run(ctx, argv) {
|
|
18
|
+
const [rel] = argv.filter((a) => !a.startsWith('--'));
|
|
19
|
+
const rootRes = primaryRoot(ctx.cwd);
|
|
20
|
+
if (!rootRes.ok) {
|
|
21
|
+
renderRefusal(rootRes.violations).forEach(ctx.err);
|
|
22
|
+
return EXIT.REFUSED;
|
|
23
|
+
}
|
|
24
|
+
const root = rootRes.value;
|
|
25
|
+
const paths = canonPaths(root);
|
|
26
|
+
if (!rel || !(rel.startsWith(`${paths.specs}/`) || rel.startsWith(`${paths.plans}/`)) || !rel.endsWith('.md')) {
|
|
27
|
+
renderRefusal([v('path', 'usage', String(rel ?? ''), `witness adopt <${paths.specs}/... | ${paths.plans}/...>.md`)]).forEach(ctx.err);
|
|
28
|
+
return EXIT.REFUSED;
|
|
29
|
+
}
|
|
30
|
+
const blocked = guardTxn(ctx, root);
|
|
31
|
+
if (blocked !== undefined)
|
|
32
|
+
return blocked;
|
|
33
|
+
const raw = readDoc(join(root, rel));
|
|
34
|
+
if (!raw.ok) {
|
|
35
|
+
renderRefusal(raw.violations).forEach(ctx.err);
|
|
36
|
+
return EXIT.REFUSED;
|
|
37
|
+
}
|
|
38
|
+
const { meta, body } = raw.value;
|
|
39
|
+
const violations = validateDoc(meta, body);
|
|
40
|
+
const canon = loadCanon(root);
|
|
41
|
+
const id = String(meta.id);
|
|
42
|
+
if (canon.docs.filter((d) => String(d.meta.id) === id).length > 1) {
|
|
43
|
+
violations.push(v('id', 'duplicate-id', id, 'one doc per id'));
|
|
44
|
+
}
|
|
45
|
+
for (const dep of Array.isArray(meta.depends) ? meta.depends : []) {
|
|
46
|
+
if (!findById(canon, dep))
|
|
47
|
+
violations.push(v('depends', 'unknown-dep', dep, 'an existing doc id'));
|
|
48
|
+
}
|
|
49
|
+
const cycle = findCycle(canon);
|
|
50
|
+
if (cycle)
|
|
51
|
+
violations.push(v('depends', 'cycle', cycle.join(' -> '), 'an acyclic graph'));
|
|
52
|
+
if (violations.length) {
|
|
53
|
+
renderRefusal(violations).forEach(ctx.err);
|
|
54
|
+
ctx.err(`help: fix the file and re-run, or revert instead: git checkout HEAD -- ${rel}`);
|
|
55
|
+
return EXIT.REFUSED;
|
|
56
|
+
}
|
|
57
|
+
const witnessed = lastWitnessed(root, rel);
|
|
58
|
+
const currentSha = canonicalSha(meta, body);
|
|
59
|
+
const changed = witnessed?.sha !== currentSha;
|
|
60
|
+
const criteriaChanged = JSON.stringify(meta.criteria ?? null) !== JSON.stringify(witnessed?.criteria ?? null);
|
|
61
|
+
const commits = untrailedCommitsFor(root, rel).filter((sha) => !adoptedCommits(root).has(sha));
|
|
62
|
+
const dirty = dirtyStatePaths(root).includes(rel);
|
|
63
|
+
if (!changed && !dirty && commits.length === 0) {
|
|
64
|
+
ctx.out(kv('adopt', `${id}: nothing to adopt — content matches the last witnessed state`));
|
|
65
|
+
return EXIT.OK;
|
|
66
|
+
}
|
|
67
|
+
const doc = findById(canon, id);
|
|
68
|
+
const isLiveSpec = doc?.meta.type === 'spec' && doc.meta.status === 'live';
|
|
69
|
+
let reverify;
|
|
70
|
+
if (isLiveSpec && changed && doc) {
|
|
71
|
+
const lane = await runCriteria(root, ctx, doc);
|
|
72
|
+
if (!lane.ok) {
|
|
73
|
+
renderRefusal(lane.violations).forEach(ctx.err);
|
|
74
|
+
return EXIT.REFUSED;
|
|
75
|
+
}
|
|
76
|
+
reverify = { ok: lane.value.ok, criteria: lane.value.criteria };
|
|
77
|
+
}
|
|
78
|
+
const unreviewed = isLiveSpec && changed && !criteriaChanged;
|
|
79
|
+
const entry = {
|
|
80
|
+
t: 'adopt',
|
|
81
|
+
artifact: id,
|
|
82
|
+
sha: currentSha,
|
|
83
|
+
commits,
|
|
84
|
+
...(reverify ? { reverify } : {}),
|
|
85
|
+
...(unreviewed ? { unreviewed_amendment: true } : {}),
|
|
86
|
+
};
|
|
87
|
+
const flagTouched = doc && reverify !== undefined;
|
|
88
|
+
const files = [...new Set([...(dirty || flagTouched ? [rel] : []), journalRel(id)])];
|
|
89
|
+
const lock = acquireLock(root);
|
|
90
|
+
if (!lock.ok) {
|
|
91
|
+
renderRefusal(lock.violations).forEach(ctx.err);
|
|
92
|
+
return EXIT.BLOCKED;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const marker = { op: `adopt(${id})`, files, journalMulti: [{ stream: id, line: entryLine(entry) }] };
|
|
96
|
+
const committed = withTxn(root, marker, () => {
|
|
97
|
+
if (doc && reverify && !reverify.ok)
|
|
98
|
+
stampDrift(root, doc, currentSha);
|
|
99
|
+
else if (doc && reverify?.ok && doc.meta.drift !== undefined)
|
|
100
|
+
clearDrift(root, doc);
|
|
101
|
+
appendEntry(root, id, entry);
|
|
102
|
+
crashPoint(ctx.env, 'adopt-journal');
|
|
103
|
+
return stateCommit(root, files, `adopt(${id}): ${short(currentSha)}`);
|
|
104
|
+
});
|
|
105
|
+
if (!committed.ok) {
|
|
106
|
+
renderRefusal(committed.violations).forEach(ctx.err);
|
|
107
|
+
return EXIT.REFUSED;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
lock.ok && lock.value();
|
|
112
|
+
}
|
|
113
|
+
ctx.out(kv('adopt', `${id} · ${short(currentSha)} · ${commits.length} commit(s) absolved${unreviewed ? ' · unreviewed-amendment' : ''}`));
|
|
114
|
+
if (reverify)
|
|
115
|
+
ctx.out(kv('reverify', reverify.ok ? 'lane green — stays live' : 'lane RED — drift stamped immediately (hand-edit, no debounce)'));
|
|
116
|
+
ctx.out('help: history: witness log ' + id);
|
|
117
|
+
return reverify && !reverify.ok ? EXIT.FINDINGS : EXIT.OK;
|
|
118
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { EXIT } from '../cli.js';
|
|
2
|
+
import { kv, rows } from '../toon.js';
|
|
3
|
+
import { renderRefusal, v } from '../refusal.js';
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import { resolveHarness } from '../harness.js';
|
|
6
|
+
import { MODEL_ALIASES } from '../model.js';
|
|
7
|
+
import { primaryRoot } from '../gitio.js';
|
|
8
|
+
import { PROMPT_NAMES } from '../reviewer.js';
|
|
9
|
+
import { addToLocalOverlay, publishScore, reportPass, runReviewerSuites, runSkillSuites, SKILL_NAMES, } from '../calibrate.js';
|
|
10
|
+
export async function run(ctx, argv) {
|
|
11
|
+
const args = [...argv];
|
|
12
|
+
const flags = { suite: 'all', only: undefined, samples: 10, publish: false };
|
|
13
|
+
const positional = [];
|
|
14
|
+
while (args.length > 0) {
|
|
15
|
+
const a = args.shift();
|
|
16
|
+
if (a === '--suite')
|
|
17
|
+
flags.suite = args.shift() ?? '';
|
|
18
|
+
else if (a === '--only')
|
|
19
|
+
flags.only = args.shift();
|
|
20
|
+
else if (a === '--samples')
|
|
21
|
+
flags.samples = Number(args.shift() ?? '10');
|
|
22
|
+
else if (a === '--publish')
|
|
23
|
+
flags.publish = true;
|
|
24
|
+
else
|
|
25
|
+
positional.push(a);
|
|
26
|
+
}
|
|
27
|
+
const model = positional[0];
|
|
28
|
+
if (model === undefined || model === '') {
|
|
29
|
+
for (const line of renderRefusal([v('model', 'required', '', 'witness calibrate <exact-model-id>')]))
|
|
30
|
+
ctx.err(line);
|
|
31
|
+
return EXIT.REFUSED;
|
|
32
|
+
}
|
|
33
|
+
if (MODEL_ALIASES.includes(model)) {
|
|
34
|
+
for (const line of renderRefusal([v('model', 'alias-refused', model, 'an exact model id')]))
|
|
35
|
+
ctx.err(line);
|
|
36
|
+
return EXIT.REFUSED;
|
|
37
|
+
}
|
|
38
|
+
if (!['all', 'reviewers', 'skills'].includes(flags.suite)) {
|
|
39
|
+
for (const line of renderRefusal([v('--suite', 'unknown-suite', flags.suite, 'all | reviewers | skills')]))
|
|
40
|
+
ctx.err(line);
|
|
41
|
+
return EXIT.REFUSED;
|
|
42
|
+
}
|
|
43
|
+
let runReviewers = flags.suite === 'all' || flags.suite === 'reviewers';
|
|
44
|
+
let runSkills = flags.suite === 'all' || flags.suite === 'skills';
|
|
45
|
+
let reviewerOnly;
|
|
46
|
+
let skillOnly;
|
|
47
|
+
if (flags.only !== undefined) {
|
|
48
|
+
if (PROMPT_NAMES.includes(flags.only)) {
|
|
49
|
+
reviewerOnly = flags.only;
|
|
50
|
+
runReviewers = true;
|
|
51
|
+
runSkills = false;
|
|
52
|
+
}
|
|
53
|
+
else if (SKILL_NAMES.includes(flags.only)) {
|
|
54
|
+
skillOnly = flags.only;
|
|
55
|
+
runSkills = true;
|
|
56
|
+
runReviewers = false;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const want = `reviewer: ${PROMPT_NAMES.join(' ')} · skill: ${SKILL_NAMES.join(' ')}`;
|
|
60
|
+
for (const line of renderRefusal([v('--only', 'unknown-name', flags.only, want)]))
|
|
61
|
+
ctx.err(line);
|
|
62
|
+
return EXIT.REFUSED;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const rootR = primaryRoot(ctx.cwd);
|
|
66
|
+
if (!rootR.ok) {
|
|
67
|
+
for (const line of renderRefusal(rootR.violations))
|
|
68
|
+
ctx.err(line);
|
|
69
|
+
return EXIT.REFUSED;
|
|
70
|
+
}
|
|
71
|
+
// Decision 88: calibration measures the (harness, model) pair — the same spawn the
|
|
72
|
+
// gate battery will use. Resolved once, threaded into every suite and the overlay.
|
|
73
|
+
const cfgR = loadConfig(rootR.value);
|
|
74
|
+
const hxR = resolveHarness(ctx.env, cfgR.ok ? cfgR.value.raw : {});
|
|
75
|
+
if (!hxR.ok) {
|
|
76
|
+
for (const line of renderRefusal(hxR.violations))
|
|
77
|
+
ctx.err(line);
|
|
78
|
+
return EXIT.REFUSED;
|
|
79
|
+
}
|
|
80
|
+
const harness = hxR.value.harness;
|
|
81
|
+
const reviewers = runReviewers ? await runReviewerSuites(ctx, harness, model, flags.samples, reviewerOnly) : undefined;
|
|
82
|
+
if (reviewers && !reviewers.ok) {
|
|
83
|
+
for (const line of renderRefusal(reviewers.violations))
|
|
84
|
+
ctx.err(line);
|
|
85
|
+
return EXIT.REFUSED;
|
|
86
|
+
}
|
|
87
|
+
const skills = runSkills ? await runSkillSuites(ctx, harness, model, flags.samples, { only: skillOnly }) : undefined;
|
|
88
|
+
if (skills && !skills.ok) {
|
|
89
|
+
for (const line of renderRefusal(skills.violations))
|
|
90
|
+
ctx.err(line);
|
|
91
|
+
return EXIT.REFUSED;
|
|
92
|
+
}
|
|
93
|
+
const report = { model, reviewers: reviewers?.value ?? [], skills: skills?.value ?? [] };
|
|
94
|
+
ctx.out(kv('calibrate', model));
|
|
95
|
+
if (report.reviewers.length > 0) {
|
|
96
|
+
for (const line of rows('reviewers', ['reviewer', 'catch', 'clean', 'inject', 'pass'], report.reviewers.map((s) => ({
|
|
97
|
+
reviewer: s.reviewer,
|
|
98
|
+
catch: `${s.catches.ok}/${s.catches.total}`,
|
|
99
|
+
clean: `${s.clean.ok}/${s.clean.total}`,
|
|
100
|
+
inject: s.inject.total === 0 ? '-' : `${s.inject.ok}/${s.inject.total}`,
|
|
101
|
+
pass: s.pass ? 'yes' : 'NO',
|
|
102
|
+
}))))
|
|
103
|
+
ctx.out(line);
|
|
104
|
+
}
|
|
105
|
+
if (report.skills.length > 0) {
|
|
106
|
+
for (const line of rows('skills', ['skill', 'metric', 'score', 'pass'], report.skills.map((s) => ({
|
|
107
|
+
skill: s.skill,
|
|
108
|
+
metric: s.metric,
|
|
109
|
+
score: `${s.ok}/${s.total}`,
|
|
110
|
+
pass: s.pass ? 'yes' : 'NO',
|
|
111
|
+
}))))
|
|
112
|
+
ctx.out(line);
|
|
113
|
+
}
|
|
114
|
+
const pass = reportPass(report);
|
|
115
|
+
ctx.out(kv('result', pass ? 'PASS' : 'FAIL'));
|
|
116
|
+
if (!pass)
|
|
117
|
+
return EXIT.FINDINGS;
|
|
118
|
+
addToLocalOverlay(rootR.value, model, harness.name);
|
|
119
|
+
ctx.out(kv('overlay', `.witness/calibration.local.yaml + ${harness.name}/${model} (gate-runs stamp calibration: local)`));
|
|
120
|
+
if (flags.publish) {
|
|
121
|
+
const pub = publishScore(rootR.value, model, report);
|
|
122
|
+
if (!pub.ok) {
|
|
123
|
+
for (const line of renderRefusal(pub.violations))
|
|
124
|
+
ctx.err(line);
|
|
125
|
+
return EXIT.REFUSED;
|
|
126
|
+
}
|
|
127
|
+
ctx.out(kv('published', pub.value.path));
|
|
128
|
+
}
|
|
129
|
+
return EXIT.OK;
|
|
130
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { readdirSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { adoptedCommits } from '../adopt.js';
|
|
4
|
+
import { EXIT, version } from '../cli.js';
|
|
5
|
+
import { HARNESSES, resolveHarness, skillsVisibility } from '../harness.js';
|
|
6
|
+
import { probe } from '../probe.js';
|
|
7
|
+
import { loadConfig } from '../config.js';
|
|
8
|
+
import { designPending } from '../design.js';
|
|
9
|
+
import { runDrift } from '../drift.js';
|
|
10
|
+
import { auditStateCommits, dirtyStatePaths, primaryRoot } from '../gitio.js';
|
|
11
|
+
import { contentAtSha } from '../history.js';
|
|
12
|
+
import { effortStreams, readStream } from '../journal.js';
|
|
13
|
+
import { sourceTags } from '../matcher.js';
|
|
14
|
+
import { evaluateNeeds } from '../needs.js';
|
|
15
|
+
import { renderRefusal } from '../refusal.js';
|
|
16
|
+
import { pendingDecision } from '../rounds.js';
|
|
17
|
+
import { criteriaExcludes } from '../runner.js';
|
|
18
|
+
import { findById, findCycle, loadCanon } from '../scan.js';
|
|
19
|
+
import { canonicalSha } from '../sha.js';
|
|
20
|
+
import { lazyStamp } from '../stamp.js';
|
|
21
|
+
import { kv, rows } from '../toon.js';
|
|
22
|
+
import { pendingTxn } from '../txn.js';
|
|
23
|
+
import { listWorktrees, worktreePath } from '../worktree.js';
|
|
24
|
+
const f = (level, area, field, rule, detail) => ({ level, area, field, rule, detail });
|
|
25
|
+
export async function run(ctx, argv = []) {
|
|
26
|
+
if (argv.includes('--drift'))
|
|
27
|
+
return runDrift(ctx, argv);
|
|
28
|
+
const rootRes = primaryRoot(ctx.cwd);
|
|
29
|
+
if (!rootRes.ok) {
|
|
30
|
+
renderRefusal(rootRes.violations).forEach(ctx.err);
|
|
31
|
+
return EXIT.REFUSED;
|
|
32
|
+
}
|
|
33
|
+
const root = rootRes.value;
|
|
34
|
+
const findings = [];
|
|
35
|
+
const cfg = loadConfig(root);
|
|
36
|
+
if (!cfg.ok)
|
|
37
|
+
cfg.violations.forEach((x) => findings.push(f('error', 'config', x.field, x.rule, x.got)));
|
|
38
|
+
else if (cfg.value.warning)
|
|
39
|
+
findings.push(f('warn', 'config', 'schema', 'older-schema', cfg.value.warning));
|
|
40
|
+
const canon0 = loadCanon(root);
|
|
41
|
+
const lazy = lazyStamp(root, ctx, canon0);
|
|
42
|
+
const canon = lazy.stamped.length > 0 ? loadCanon(root) : canon0;
|
|
43
|
+
canon.errors.forEach((x) => findings.push(f('error', 'parse', x.field, x.rule, x.got)));
|
|
44
|
+
for (const doc of canon.docs) {
|
|
45
|
+
doc.violations.forEach((x) => findings.push(f('error', 'schema', `${doc.rel}: ${x.field}`, x.rule, x.got)));
|
|
46
|
+
}
|
|
47
|
+
const byId = new Map();
|
|
48
|
+
canon.docs.forEach((d) => byId.set(String(d.meta.id), (byId.get(String(d.meta.id)) ?? 0) + 1));
|
|
49
|
+
for (const [id, count] of byId) {
|
|
50
|
+
if (count > 1)
|
|
51
|
+
findings.push(f('error', 'graph', id, 'duplicate-id', `${count} docs carry this id`));
|
|
52
|
+
}
|
|
53
|
+
for (const doc of canon.docs) {
|
|
54
|
+
const depends = Array.isArray(doc.meta.depends) ? doc.meta.depends : [];
|
|
55
|
+
depends.forEach((dep) => {
|
|
56
|
+
if (!findById(canon, dep))
|
|
57
|
+
findings.push(f('error', 'graph', `${doc.rel}: depends`, 'unknown-dep', dep));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const cycle = findCycle(canon);
|
|
61
|
+
if (cycle)
|
|
62
|
+
findings.push(f('error', 'graph', 'depends', 'cycle', cycle.join(' -> ')));
|
|
63
|
+
for (const plan of canon.docs.filter((d) => d.meta.type === 'plan')) {
|
|
64
|
+
const parent = typeof plan.meta.parent === 'string' ? findById(canon, plan.meta.parent) : undefined;
|
|
65
|
+
if (!parent) {
|
|
66
|
+
findings.push(f('error', 'invariants', `${plan.rel}: parent`, 'unknown-parent', String(plan.meta.parent)));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (plan.meta.status !== 'draft' && plan.meta.status !== 'abandoned' && parent.meta.status === 'draft') {
|
|
70
|
+
findings.push(f('error', 'invariants', `${plan.rel}: status`, 'status-pairing', `plan is ${plan.meta.status} but parent is draft`));
|
|
71
|
+
}
|
|
72
|
+
const pin = plan.meta['derives-from'];
|
|
73
|
+
if (typeof pin === 'string' && !contentAtSha(root, parent.rel, pin)) {
|
|
74
|
+
findings.push(f('error', 'invariants', `${plan.rel}: derives-from`, 'pin-unresolvable', `no committed version of ${parent.rel} matches ${pin.slice(0, 7)}`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const plan of canon.docs.filter((d) => d.meta.type === 'plan')) {
|
|
78
|
+
const id = String(plan.meta.id);
|
|
79
|
+
if (String(plan.meta.status) === 'in-progress') {
|
|
80
|
+
const parent = findById(canon, String(plan.meta.parent));
|
|
81
|
+
if (parent && canonicalSha(parent.meta, parent.body) !== String(plan.meta['derives-from'])) {
|
|
82
|
+
findings.push(f('warn', 'motion', id, 'mid-flight-amendment', `parent ${String(parent.meta.id)} moved off the pin — ship will re-verify against current content`));
|
|
83
|
+
}
|
|
84
|
+
if (!existsSync(worktreePath(root, id))) {
|
|
85
|
+
findings.push(f('warn', 'motion', id, 'missing-worktree', `witness start ${id} recreates it`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const gate of ['plan', 'implement', 'ship']) {
|
|
89
|
+
if (pendingDecision(readStream(root, id), gate)) {
|
|
90
|
+
findings.push(f('warn', 'motion', id, 'gate-awaiting-decision', `witness decide ${gate} ${id} --show`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const slug of effortStreams(root)) {
|
|
95
|
+
if (pendingDecision(readStream(root, slug), 'decompose')) {
|
|
96
|
+
findings.push(f('warn', 'motion', slug, 'gate-awaiting-decision', `witness decide decompose ${slug} --show`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const spec of canon.docs.filter((d) => d.meta.type === 'spec')) {
|
|
100
|
+
const id = String(spec.meta.id);
|
|
101
|
+
if (pendingDecision(readStream(root, id), 'design')) {
|
|
102
|
+
findings.push(f('warn', 'motion', id, 'gate-awaiting-decision', `witness decide design ${id} --show`));
|
|
103
|
+
}
|
|
104
|
+
if (designPending(root, spec) && String(spec.meta.status) === 'approved') {
|
|
105
|
+
findings.push(f('warn', 'motion', id, 'design-pending', `ui spec owes a design — witness design ${id} --file <html>`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const planId of listWorktrees(root)) {
|
|
109
|
+
const doc = findById(canon, planId);
|
|
110
|
+
if (!doc || ['done', 'abandoned'].includes(String(doc.meta.status))) {
|
|
111
|
+
findings.push(f('warn', 'motion', planId, 'stray-worktree', 'witness clean sweeps it'));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const doc of canon.docs) {
|
|
115
|
+
const needs = Array.isArray(doc.meta.needs) ? doc.meta.needs : [];
|
|
116
|
+
if (needs.length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
for (const r of await evaluateNeeds(root, ctx, needs)) {
|
|
119
|
+
if (r.status === 'ok')
|
|
120
|
+
continue;
|
|
121
|
+
const rule = r.status === 'unmet' ? 'need-unmet' : `need-${r.status}`;
|
|
122
|
+
findings.push(f('warn', 'needs', `${doc.rel}: ${r.label}`, rule, r.detail));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const absolved = adoptedCommits(root);
|
|
126
|
+
auditStateCommits(root).forEach((c) => {
|
|
127
|
+
if (!c.trailered && !absolved.has(c.sha)) {
|
|
128
|
+
findings.push(f('error', 'audit', c.sha.slice(0, 7), 'untrailered-commit', `${c.subject} — adopt: witness adopt <path> · or revert`));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
if (pendingTxn(root)) {
|
|
132
|
+
findings.push(f('error', 'audit', '.witness/txn.json', 'pending-txn', 'crashed invocation — witness recover --complete | --rollback'));
|
|
133
|
+
}
|
|
134
|
+
else if (dirtyStatePaths(root).length) {
|
|
135
|
+
dirtyStatePaths(root).forEach((p) => findings.push(f('error', 'audit', p, 'hand-edit-in-progress', 'uncommitted change on a state path — adopt: witness adopt <path> · or revert')));
|
|
136
|
+
}
|
|
137
|
+
const journalDir = join(root, '.witness', 'journal');
|
|
138
|
+
if (existsSync(journalDir)) {
|
|
139
|
+
for (const file of readdirSync(journalDir)) {
|
|
140
|
+
if (!file.endsWith('.jsonl'))
|
|
141
|
+
continue;
|
|
142
|
+
const id = file.slice(0, -'.jsonl'.length);
|
|
143
|
+
const entries = readStream(root, id);
|
|
144
|
+
if (entries[0]?.t === 'recap')
|
|
145
|
+
continue;
|
|
146
|
+
if (!findById(canon, id))
|
|
147
|
+
findings.push(f('warn', 'journal', file, 'orphan-journal', 'stream matches no doc (frozen lineage or stray)'));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (cfg.ok) {
|
|
151
|
+
const src = sourceTags(root, criteriaExcludes(cfg.value));
|
|
152
|
+
for (const [tag, files] of src.files) {
|
|
153
|
+
if (!findById(canon, tag)) {
|
|
154
|
+
findings.push(f('warn', 'criteria', tag, 'orphan-tag', `${files[0] ?? '?'} tags a spec that does not exist`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!probe('gh', ['--version'], ctx.env))
|
|
159
|
+
findings.push(f('warn', 'probes', 'gh', 'missing', 'PR operations (later slice) will stop loudly'));
|
|
160
|
+
else if (!probe('gh', ['auth', 'status'], ctx.env))
|
|
161
|
+
findings.push(f('warn', 'probes', 'gh', 'unauthenticated', 'run gh auth login'));
|
|
162
|
+
if (cfg.ok) {
|
|
163
|
+
for (const [key, paths] of Object.entries(cfg.value.docs)) {
|
|
164
|
+
for (const p of paths ?? []) {
|
|
165
|
+
if (!existsSync(join(root, p))) {
|
|
166
|
+
findings.push(f('error', 'config', `docs.${key}`, 'doc-missing', `${p} — gates inject configured docs fail-closed`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// `harness:` is the config rung of the resolution ladder, consulted only when no
|
|
172
|
+
// detection rung answered — so a typo there is silent on the machine that has one.
|
|
173
|
+
// check is the diagnostic verb: it reports the value regardless of who answered.
|
|
174
|
+
const configuredHarness = cfg.ok ? cfg.value.raw.harness : undefined;
|
|
175
|
+
if (configuredHarness !== undefined && !HARNESSES.includes(String(configuredHarness))) {
|
|
176
|
+
findings.push(f('error', 'config', 'harness', 'unknown-harness', `${String(configuredHarness)} — expected ${HARNESSES.join(' | ')}`));
|
|
177
|
+
}
|
|
178
|
+
const hxR = resolveHarness(ctx.env, cfg.ok ? cfg.value.raw : {});
|
|
179
|
+
if (!hxR.ok) {
|
|
180
|
+
hxR.violations.forEach((x) => findings.push(f('error', 'harness', x.field, x.rule, x.got)));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const harness = hxR.value.harness;
|
|
184
|
+
// Decision 88: the judgment lane runs on the RESOLVED harness — the probe follows it.
|
|
185
|
+
// (Decision 12 probed `claude` unconditionally; that was true only while every
|
|
186
|
+
// harness's reviewers were claude, and it is a false prerequisite for a pi user.)
|
|
187
|
+
if (!probe(harness.launch, ['--version'], ctx.env)) {
|
|
188
|
+
findings.push(f('warn', 'probes', harness.launch, 'missing', `the ${harness.launch} CLI runs this harness's gate reviewers — install and authenticate it`));
|
|
189
|
+
}
|
|
190
|
+
// Decision 14: pi resolves project skills cwd-relative with no upward walk, and
|
|
191
|
+
// implement runs with cwd inside .witness/worktrees/<plan-id>. A project-scope
|
|
192
|
+
// install therefore loses every skill in the stage that does the most work.
|
|
193
|
+
const visibility = skillsVisibility(ctx.env, root, harness);
|
|
194
|
+
if (visibility === 'project-only') {
|
|
195
|
+
findings.push(f('warn', 'harness', 'skills', 'skills-project-scope', `${harness.skills.project} is invisible from a worktree cwd — reinstall at global scope (${harness.skills.global} under $HOME)`));
|
|
196
|
+
}
|
|
197
|
+
else if (visibility === 'absent' && !harness.bundled) {
|
|
198
|
+
findings.push(f('warn', 'harness', 'skills', 'skills-not-installed', `${harness.name} sees none of the six stage skills — npx skills add <witness tarball url> at global scope`));
|
|
199
|
+
}
|
|
200
|
+
// Revision 3. Skills present + payload absent is the worst state in the design: the
|
|
201
|
+
// pipeline looks like it works, and nothing blocks a direct edit to canon. `bundled`
|
|
202
|
+
// is the same bit that silences the skills warning — Claude Code's marketplace plugin
|
|
203
|
+
// ships engine, guard and dashboard out of band, so absence there proves nothing.
|
|
204
|
+
const installed = harness.payload.map((p) => p.to).filter((rel) => existsSync(join(root, rel)));
|
|
205
|
+
if (installed.length === 0) {
|
|
206
|
+
if (!harness.bundled) {
|
|
207
|
+
findings.push(f('warn', 'harness', 'payload', 'payload-not-installed', `${harness.name} has no engine, guard or dashboard here — run witness init --agent ${harness.name}`));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// Revision 1's other half: the sync can restamp, but only if someone knows to run
|
|
212
|
+
// it. The engine file's pin decides which CLI the whole pipeline runs, so a lagging
|
|
213
|
+
// pin is a finding, not a detail.
|
|
214
|
+
// The capture must be a semver and nothing else. Both payloads embed the pin as
|
|
215
|
+
// `${WITNESS_BIN:-npx -y @popovych.co/witness@<v>}`, so a trailing-delimiter
|
|
216
|
+
// class that omits `}` swallows the brace and NEVER equals version() — which made
|
|
217
|
+
// payload-stale fire on every fresh install until Task 9's manual pass caught it.
|
|
218
|
+
// Same shape as install.ts's PIN, deliberately.
|
|
219
|
+
const stale = installed.filter((rel) => {
|
|
220
|
+
const m = /@popovych\.co\/witness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/.exec(readFileSync(join(root, rel), 'utf8'));
|
|
221
|
+
return m !== null && m[1] !== version();
|
|
222
|
+
});
|
|
223
|
+
if (stale.length > 0) {
|
|
224
|
+
findings.push(f('warn', 'harness', 'payload', 'payload-stale', `${stale.join(' · ')} pin an older CLI than ${version()} — run witness init --agent ${harness.name} to restamp`));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const errors = findings.filter((x) => x.level === 'error');
|
|
229
|
+
if (findings.length)
|
|
230
|
+
rows('findings', ['level', 'area', 'field', 'rule', 'detail'], findings).forEach(ctx.out);
|
|
231
|
+
ctx.out(kv('checks', `${canon.docs.length} docs · ${auditStateCommits(root).length} commits audited · ${errors.length} errors`));
|
|
232
|
+
return errors.length ? EXIT.FINDINGS : EXIT.OK;
|
|
233
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EXIT } from '../cli.js';
|
|
2
|
+
import { primaryRoot } from '../gitio.js';
|
|
3
|
+
import { findById, loadCanon } from '../scan.js';
|
|
4
|
+
import { renderRefusal } from '../refusal.js';
|
|
5
|
+
import { kv, rows } from '../toon.js';
|
|
6
|
+
import { listWorktrees, removeWorktree } from '../worktree.js';
|
|
7
|
+
export async function run(ctx, _argv) {
|
|
8
|
+
const rootR = primaryRoot(ctx.cwd);
|
|
9
|
+
if (!rootR.ok) {
|
|
10
|
+
renderRefusal(rootR.violations).forEach((l) => ctx.err(l));
|
|
11
|
+
return EXIT.REFUSED;
|
|
12
|
+
}
|
|
13
|
+
const root = rootR.value;
|
|
14
|
+
const canon = loadCanon(root);
|
|
15
|
+
const swept = [];
|
|
16
|
+
for (const planId of listWorktrees(root)) {
|
|
17
|
+
const doc = findById(canon, planId);
|
|
18
|
+
const status = doc ? String(doc.meta.status) : undefined;
|
|
19
|
+
if (!doc) {
|
|
20
|
+
removeWorktree(root, planId);
|
|
21
|
+
swept.push({ plan: planId, why: 'no such plan' });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (status === 'done' || status === 'abandoned') {
|
|
25
|
+
removeWorktree(root, planId);
|
|
26
|
+
swept.push({ plan: planId, why: `plan is ${status}` });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (swept.length)
|
|
30
|
+
ctx.out(rows('swept', ['plan', 'why'], swept).join('\n'));
|
|
31
|
+
else
|
|
32
|
+
ctx.out(kv('clean', 'no stray worktrees'));
|
|
33
|
+
return EXIT.OK;
|
|
34
|
+
}
|