@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
package/dist/worktree.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, sep } from 'node:path';
|
|
3
|
+
import { git, tryGit } from './gitio.js';
|
|
4
|
+
import { ok, refuse, v } from './refusal.js';
|
|
5
|
+
export function worktreesDir(root) {
|
|
6
|
+
return join(root, '.witness', 'worktrees');
|
|
7
|
+
}
|
|
8
|
+
export function worktreePath(root, planId) {
|
|
9
|
+
return join(worktreesDir(root), planId);
|
|
10
|
+
}
|
|
11
|
+
// primaryRoot resolves a worktree cwd up to the primary root and drops WHICH worktree it
|
|
12
|
+
// was. That identity is the flow, so recover it here rather than re-deriving from git.
|
|
13
|
+
// (Lives here, not in gitio.ts, because gitio.ts is what this module imports — the other
|
|
14
|
+
// direction would close an import cycle.)
|
|
15
|
+
export function worktreeFlow(cwd, root) {
|
|
16
|
+
const top = tryGit(cwd, 'rev-parse', '--show-toplevel');
|
|
17
|
+
if (!top.ok)
|
|
18
|
+
return undefined;
|
|
19
|
+
const dir = worktreesDir(root);
|
|
20
|
+
if (!top.out.startsWith(dir + sep))
|
|
21
|
+
return undefined;
|
|
22
|
+
const rest = top.out.slice(dir.length + 1);
|
|
23
|
+
// Round-tripping the last path segment back to a plan id is safe: worktreePath is a
|
|
24
|
+
// bare join with no slugification, and doc ids are /^[a-z0-9-]+$/ (dsl.ts).
|
|
25
|
+
return rest.length > 0 && !rest.includes(sep) ? rest : undefined;
|
|
26
|
+
}
|
|
27
|
+
export function branchName(planId) {
|
|
28
|
+
return `witness/${planId}`;
|
|
29
|
+
}
|
|
30
|
+
export function ensureExcluded(root) {
|
|
31
|
+
const commonDir = git(root, 'rev-parse', '--git-common-dir').trim();
|
|
32
|
+
const gitDir = isAbsolute(commonDir) ? commonDir : join(root, commonDir);
|
|
33
|
+
const infoDir = join(gitDir, 'info');
|
|
34
|
+
mkdirSync(infoDir, { recursive: true });
|
|
35
|
+
const excludePath = join(infoDir, 'exclude');
|
|
36
|
+
const current = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
|
|
37
|
+
const lines = current.split('\n');
|
|
38
|
+
// screens are witnessed evidence, regenerable, never committed — ignored in every
|
|
39
|
+
// worktree so they stay out of changedFiles() and the reviewed tree-sha
|
|
40
|
+
for (const line of ['.witness/worktrees/', '.witness/screens/']) {
|
|
41
|
+
if (!lines.includes(line))
|
|
42
|
+
appendFileSync(excludePath, `${line}\n`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function createWorktree(root, planId, baseBranch) {
|
|
46
|
+
const path = worktreePath(root, planId);
|
|
47
|
+
const branch = branchName(planId);
|
|
48
|
+
ensureExcluded(root);
|
|
49
|
+
mkdirSync(worktreesDir(root), { recursive: true });
|
|
50
|
+
if (existsSync(path))
|
|
51
|
+
return ok({ path, branch });
|
|
52
|
+
tryGit(root, 'worktree', 'prune');
|
|
53
|
+
const branchExists = tryGit(root, 'rev-parse', '--verify', '--quiet', `refs/heads/${branch}`).ok;
|
|
54
|
+
const r = branchExists
|
|
55
|
+
? tryGit(root, 'worktree', 'add', path, branch)
|
|
56
|
+
: tryGit(root, 'worktree', 'add', '-b', branch, path, baseBranch);
|
|
57
|
+
if (!r.ok) {
|
|
58
|
+
return refuse([v('worktree', 'worktree-create-failed', r.out.slice(0, 200), 'git worktree add to succeed')]);
|
|
59
|
+
}
|
|
60
|
+
return ok({ path, branch });
|
|
61
|
+
}
|
|
62
|
+
export function removeWorktree(root, planId) {
|
|
63
|
+
const path = worktreePath(root, planId);
|
|
64
|
+
if (existsSync(path))
|
|
65
|
+
tryGit(root, 'worktree', 'remove', '--force', path);
|
|
66
|
+
tryGit(root, 'worktree', 'prune');
|
|
67
|
+
}
|
|
68
|
+
export function listWorktrees(root) {
|
|
69
|
+
const dir = worktreesDir(root);
|
|
70
|
+
if (!existsSync(dir))
|
|
71
|
+
return [];
|
|
72
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@popovych.co/witness",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Evidence-gated spec-driven development pipeline for AI coding agents: brainstorm to PR in six stages, where specs are state, plans are motion, and no transition passes without evidence. Drives from Claude Code or Pi.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"spec-driven-development",
|
|
7
|
+
"sdd",
|
|
8
|
+
"ai-agent",
|
|
9
|
+
"agentic-workflow",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"claude-code-plugin",
|
|
12
|
+
"anthropic",
|
|
13
|
+
"llm",
|
|
14
|
+
"code-review",
|
|
15
|
+
"tdd",
|
|
16
|
+
"test-driven-development",
|
|
17
|
+
"developer-tools",
|
|
18
|
+
"cli",
|
|
19
|
+
"pipeline",
|
|
20
|
+
"codegen",
|
|
21
|
+
"ai-coding-assistant",
|
|
22
|
+
"specs",
|
|
23
|
+
"agent-skills"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://github.com/popovych-co/witness#readme",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/popovych-co/witness.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/popovych-co/witness/issues"
|
|
32
|
+
},
|
|
33
|
+
"author": "Kostiantyn Popovych",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"type": "module",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"witness": "./dist/bin.js"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"prompts",
|
|
48
|
+
"calibration.yaml",
|
|
49
|
+
"calibration",
|
|
50
|
+
"plugin",
|
|
51
|
+
"LICENSES",
|
|
52
|
+
"NOTICE.md"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:watch": "vitest",
|
|
59
|
+
"sync-versions": "node scripts/sync-versions.mjs"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"yaml": "^2.7.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/node": "^22.10.0",
|
|
66
|
+
"tsx": "^4.19.0",
|
|
67
|
+
"typescript": "^5.7.0",
|
|
68
|
+
"vitest": "^3.0.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "witness",
|
|
3
|
+
"description": "Spec-driven development with evidence gates: brainstorm → PR through six stages and five gates. Skills author, the pinned witness CLI writes state and witnesses evidence; gates stop where judgment is owed.",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Kostiantyn Popovych"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/popovych-co/witness",
|
|
9
|
+
"repository": "https://github.com/popovych-co/witness",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"spec-driven-development",
|
|
13
|
+
"evidence-gated",
|
|
14
|
+
"code-review",
|
|
15
|
+
"tdd",
|
|
16
|
+
"workflow",
|
|
17
|
+
"agent-skills"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the witness pipeline — ask the CLI what's next, do it, loop; stop where a human is owed a decision
|
|
3
|
+
argument-hint: "[--manual]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /witness — the engine
|
|
7
|
+
|
|
8
|
+
The CLI decides, you act. If `$ARGUMENTS` contains `--manual`, append `--manual` to **every** `witness gate` command you run this session — caution is a per-run mood, never config.
|
|
9
|
+
|
|
10
|
+
Resolve the CLI once:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.4.0}"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If the repo has no `witness.config.yaml`, run `$WITNESS init` first (one bootstrap commit), then proceed. If init or any verb refuses on a dirty tree, the human commits or cleans it — **never `git commit` yourself, anywhere, at any stage**: the CLI makes its own state commits, and the ship phase makes the sole code commit.
|
|
17
|
+
|
|
18
|
+
## Loop
|
|
19
|
+
|
|
20
|
+
Every turn is `witness next`, read, act on the first matching row, repeat:
|
|
21
|
+
|
|
22
|
+
1. Run `$WITNESS next`.
|
|
23
|
+
2. Read its TOON lines: `next:` (a command line), and optional `stage:`, `target:`, `note:`, `home:` (the directory this action's session belongs in), `run:` (the paste-ready handoff command), `relay:` (how a session continues in a fresh context in the same `home:`). `run:` and `relay:` are rendered by the CLI for the harness you are running on — never rewrite them, never substitute a command you remember.
|
|
24
|
+
3. Act on the **first** matching row, then go to 1:
|
|
25
|
+
|
|
26
|
+
| Signal | Action |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `note:` contains `multiple ready — choose` | Show the listed spec ids, ask the human which to plan. Then use the `witness-plan` skill with the chosen id. |
|
|
29
|
+
| `next:` names `witness decide` | A gate is stopped and the decision is the human's. Run `$WITNESS decide <gate> <target> --show`, render the checks and findings verbatim, render the `exits:` line the CLI emitted verbatim — never a remembered set, which is wrong at the round bound and in the reopened and stale states — and **END YOUR TURN**. Never run `--approve`, `--revise`, or `--stop` on your own judgment. |
|
|
30
|
+
| `home:` present and ≠ your cwd | This stage belongs to a different session. Print the `run:` line verbatim for the human (if this session is `--manual`-armed, change the argument to `'/witness --manual'`), say that work continues in the fresh session, and **END YOUR TURN**. Never use a stage skill or run the `next:` command from the wrong `home:` — a fresh session is the execution model, and it is the only one. |
|
|
31
|
+
| `stage: brainstorm` | Use the `witness-brainstorm` skill. |
|
|
32
|
+
| `stage: decompose` | Use the `witness-decompose` skill with `target` (the effort slug). |
|
|
33
|
+
| `stage: design` | Use the `witness-design` skill with `target` (the spec id). |
|
|
34
|
+
| `stage: plan` | Use the `witness-plan` skill with `target` (the spec id). |
|
|
35
|
+
| `stage: implement` | Use the `witness-implement` skill with `target` (the plan id). |
|
|
36
|
+
| `stage: ship`, or `next:` names `witness ship` | Use the `witness-ship` skill with `target` (the plan id). |
|
|
37
|
+
| any other `next:` line (`witness gate …`, `witness start …`, `witness check`, `witness recover …`, `witness check --drift`) | Run it verbatim via Bash (plus `--manual` on `gate` when armed). Exit 0 or 1 → loop. Exit 2 or 3 → render the refusal; if it names a mechanical remedy you can do without judgment (e.g. `witness recover --complete`), do it and loop, else stop for the human. |
|
|
38
|
+
|
|
39
|
+
## Stop conditions (end your turn)
|
|
40
|
+
|
|
41
|
+
- A `decide` line surfaced — the standing stops (ship always; feature scope; fix-created-spec) and every blocking finding land here by design.
|
|
42
|
+
- The same `next:` line came back twice with nothing changed in between → report `no progress: <line>`, what you tried, and hand off.
|
|
43
|
+
- An exit 2/3 you cannot mechanically satisfy — `needs-unmet` (show the human `witness satisfy <id> --need <text>`), `slug-reuse`, lock contention, calibration floor stops under `--manual`.
|
|
44
|
+
- A skill ended its turn for a gate stop — that stop is yours too; do not restart the loop until the human decides.
|
|
45
|
+
|
|
46
|
+
**Never merge a PR** — merging is the human's act on GitHub; the lazy stamp finishes the lifecycle on the next scan. **Never edit `specs/` or `plans/`** (or wherever the repo's `paths:` config relocates them) — that is what the skills' write path is for. If the dashboard warns the reviewer model is uncalibrated, surface the warning; under `--manual` treat it as a stop.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// witness canon guard — the shared core behind every harness's write block.
|
|
2
|
+
//
|
|
3
|
+
// Dependency-free ON PURPOSE (Decision 13): the Claude Code hook runs from
|
|
4
|
+
// ${CLAUDE_PLUGIN_ROOT}, which has no node_modules, and a Pi user's repo has no reason
|
|
5
|
+
// to have @popovych.co/witness installed. An import from the CLI package would
|
|
6
|
+
// resolve-fail into the fail-open path on both — a SILENTLY ABSENT guard. Adapters
|
|
7
|
+
// import this file by relative path and stay thin.
|
|
8
|
+
//
|
|
9
|
+
// Friction, not the guarantee (the trailer audit is) — Decision 31. Anything
|
|
10
|
+
// unparseable falls open: a broken guard must never brick a session.
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
13
|
+
|
|
14
|
+
// Claude Code and Pi vocabularies. Pi has no MultiEdit; Claude Code has no lowercase
|
|
15
|
+
// forms. Listing both keeps the adapters free of translation tables.
|
|
16
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'edit', 'write']);
|
|
17
|
+
const BASH_TOOLS = new Set(['Bash', 'bash']);
|
|
18
|
+
|
|
19
|
+
function configRoot(startDir) {
|
|
20
|
+
let dir = startDir;
|
|
21
|
+
for (;;) {
|
|
22
|
+
if (existsSync(join(dir, 'witness.config.yaml'))) return dir;
|
|
23
|
+
const parent = dirname(dir);
|
|
24
|
+
if (parent === dir) return undefined;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Canon roots from the repo's `paths:` config key (flow or block style), extracted
|
|
30
|
+
// without a YAML dependency. Anything unparseable falls back to the defaults.
|
|
31
|
+
function canonDirs(root) {
|
|
32
|
+
try {
|
|
33
|
+
const text = readFileSync(join(root, 'witness.config.yaml'), 'utf8');
|
|
34
|
+
const flow = text.match(/^paths:[ \t]*\{([^}]*)\}/m);
|
|
35
|
+
const section = flow ? flow[1] : (text.match(/^paths:[ \t]*\n((?:[ \t]+\S.*\n?)*)/m) || [])[1] || '';
|
|
36
|
+
const dir = (key) => {
|
|
37
|
+
const m = section.match(new RegExp(`(?:^|[\\s{,])${key}:[ \\t]*['"]?([^'"\\s,}#]+)`, 'm'));
|
|
38
|
+
return m ? m[1].replace(/\/+$/, '') : key;
|
|
39
|
+
};
|
|
40
|
+
return [dir('specs'), dir('plans'), dir('designs')];
|
|
41
|
+
} catch {
|
|
42
|
+
return ['specs', 'plans', 'designs'];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isStateRel(rel, dirs) {
|
|
47
|
+
const posix = rel.split('\\').join('/');
|
|
48
|
+
return dirs.some((d) => posix === d || posix.startsWith(`${d}/`));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
|
|
53
|
+
function statePathRe(dirs) {
|
|
54
|
+
return new RegExp(`(^|[\\s"'=;|&(])(\\./)?(${dirs.map(escapeRe).join('|')})/`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const WRITEISH = /(>>?|\btee\b|\bsed\b[^\n]*\s-i\b|\bmv\b|\bcp\b|\brm\b|\btouch\b|\btruncate\b|\bdd\b)/;
|
|
58
|
+
|
|
59
|
+
function reasonFor(what) {
|
|
60
|
+
return (
|
|
61
|
+
`witness: ${what} is CLI-written state — use the witness CLI (write / design / adopt), ` +
|
|
62
|
+
'never a direct edit. Direct edits are refused; the Witness-State trailer audit catches end-runs.'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {{ tool?: unknown, input?: unknown, cwd?: unknown }} call
|
|
68
|
+
* @returns {{ block: true, reason: string } | undefined}
|
|
69
|
+
*/
|
|
70
|
+
export function canonGuard(call) {
|
|
71
|
+
try {
|
|
72
|
+
const tool = typeof call?.tool === 'string' ? call.tool : '';
|
|
73
|
+
const args = call?.input && typeof call.input === 'object' ? call.input : {};
|
|
74
|
+
const cwd = typeof call?.cwd === 'string' && call.cwd !== '' ? call.cwd : process.cwd();
|
|
75
|
+
|
|
76
|
+
if (WRITE_TOOLS.has(tool)) {
|
|
77
|
+
// Claude Code sends file_path; pi sends path (core/tools/write.js:12).
|
|
78
|
+
const fp = typeof args.file_path === 'string' ? args.file_path : args.path;
|
|
79
|
+
if (typeof fp !== 'string' || fp === '') return undefined;
|
|
80
|
+
const abs = isAbsolute(fp) ? fp : resolve(cwd, fp);
|
|
81
|
+
const root = configRoot(dirname(abs));
|
|
82
|
+
if (!root) return undefined;
|
|
83
|
+
const rel = relative(root, abs);
|
|
84
|
+
if (rel.startsWith('..')) return undefined;
|
|
85
|
+
if (!isStateRel(rel, canonDirs(root))) return undefined;
|
|
86
|
+
return { block: true, reason: reasonFor(rel.split('\\').join('/')) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (BASH_TOOLS.has(tool)) {
|
|
90
|
+
const cmd = args.command;
|
|
91
|
+
if (typeof cmd !== 'string') return undefined;
|
|
92
|
+
const root = configRoot(cwd);
|
|
93
|
+
if (!root) return undefined;
|
|
94
|
+
const dirs = canonDirs(root);
|
|
95
|
+
if (statePathRe(dirs).test(cmd) && WRITEISH.test(cmd)) {
|
|
96
|
+
return { block: true, reason: reasonFor(`a ${dirs.map((d) => `${d}/`).join(' or ')} path in that command`) };
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return undefined;
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// witness PreToolUse guard — the Claude Code adapter over canonGuard.
|
|
3
|
+
// stdin JSON in, exit 2 + stderr out (stderr is fed back to the model). The logic
|
|
4
|
+
// lives in canon-guard.mjs so the Pi extension shares it byte for byte. Any
|
|
5
|
+
// parse/read failure exits 0: a broken hook must never brick a session.
|
|
6
|
+
import { canonGuard } from './canon-guard.mjs';
|
|
7
|
+
|
|
8
|
+
let raw = '';
|
|
9
|
+
process.stdin.on('data', (chunk) => {
|
|
10
|
+
raw += chunk;
|
|
11
|
+
});
|
|
12
|
+
process.stdin.on('end', () => {
|
|
13
|
+
let input;
|
|
14
|
+
try {
|
|
15
|
+
input = JSON.parse(raw);
|
|
16
|
+
} catch {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
let verdict;
|
|
20
|
+
try {
|
|
21
|
+
verdict = canonGuard({
|
|
22
|
+
tool: input.tool_name,
|
|
23
|
+
input: input.tool_input,
|
|
24
|
+
cwd: typeof input.cwd === 'string' && input.cwd !== '' ? input.cwd : process.cwd(),
|
|
25
|
+
});
|
|
26
|
+
} catch {
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
if (verdict && verdict.block) {
|
|
30
|
+
process.stderr.write(`${verdict.reason}\n`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
process.exit(0);
|
|
34
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
6
|
+
"hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/guard-state.mjs\"" }]
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"matcher": "Bash",
|
|
10
|
+
"hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/guard-state.mjs\"" }]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"SessionStart": [
|
|
14
|
+
{ "hooks": [{ "type": "command", "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/session-dashboard.sh\"" }] }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# witness SessionStart hook — inject the dashboard (stdout becomes session
|
|
3
|
+
# context). Silent everywhere that isn't a witness repo; failures swallowed:
|
|
4
|
+
# session start must never hang or die on this.
|
|
5
|
+
[ -f witness.config.yaml ] || exit 0
|
|
6
|
+
${WITNESS_BIN:-npx -y @popovych.co/witness@0.4.0} 2>/dev/null || true
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// witness Pi extension — the canon guard plus the session dashboard.
|
|
2
|
+
//
|
|
3
|
+
// The Pi half of Decision 8: one pure canonGuard, thin per-harness adapters. The tool
|
|
4
|
+
// names are pi's (`write`, `edit`, `bash` — there is no MultiEdit) and the path key is
|
|
5
|
+
// `path`; canonGuard accepts both vocabularies so this file needs no translation table.
|
|
6
|
+
//
|
|
7
|
+
// canon-guard.mjs is imported by RELATIVE path and is a sibling in the installed
|
|
8
|
+
// layout (.pi/extensions/) — see the pi descriptor's payload recipe. A package import
|
|
9
|
+
// would resolve-fail in a repo with no witness install (Decision 13).
|
|
10
|
+
//
|
|
11
|
+
// Every failure path falls open: a broken extension must never brick a session. On Pi
|
|
12
|
+
// that is NOT free. docs/extensions.md, "Error Handling": *extension errors are logged,
|
|
13
|
+
// agent continues; `tool_call` errors block the tool (fail-safe)* — the inversion is
|
|
14
|
+
// scoped to exactly the event the guard uses. So the try/catch below is load-bearing,
|
|
15
|
+
// not garnish: without it an unparseable witness.config.yaml would refuse every write
|
|
16
|
+
// in the repo, `src/` included. Do not remove it because canonGuard already catches.
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { canonGuard } from "./canon-guard.mjs";
|
|
22
|
+
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
|
|
25
|
+
// The Claude Code SessionStart hook injects the dashboard as session context. Pi's
|
|
26
|
+
// analogue is before_agent_start's `message`, which is stored in the session and sent
|
|
27
|
+
// to the model — a real transcript entry, not a toast. Once per session, latched.
|
|
28
|
+
function dashboardText(cwd: string): string {
|
|
29
|
+
const script = process.env.WITNESS_DASHBOARD ?? join(here, "session-dashboard.sh");
|
|
30
|
+
try {
|
|
31
|
+
const r = spawnSync("sh", [script], { cwd, encoding: "utf8", timeout: 60_000 });
|
|
32
|
+
return (r.stdout ?? "").trim();
|
|
33
|
+
} catch {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function (pi: ExtensionAPI) {
|
|
39
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
40
|
+
try {
|
|
41
|
+
const verdict = canonGuard({ tool: event.toolName, input: event.input, cwd: ctx.cwd });
|
|
42
|
+
if (!verdict) return undefined;
|
|
43
|
+
// Isolated: the toast is cosmetic, the verdict is not. A notify failure must
|
|
44
|
+
// never downgrade a block we already decided into an allow.
|
|
45
|
+
try {
|
|
46
|
+
if (ctx.hasUI) ctx.ui.notify("witness: canon write blocked — use the CLI", "warning");
|
|
47
|
+
} catch {
|
|
48
|
+
/* cosmetic only */
|
|
49
|
+
}
|
|
50
|
+
return { block: true, reason: verdict.reason };
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let injected = false;
|
|
57
|
+
pi.on("session_start", async () => {
|
|
58
|
+
injected = false;
|
|
59
|
+
});
|
|
60
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
61
|
+
if (injected) return undefined;
|
|
62
|
+
injected = true;
|
|
63
|
+
const text = dashboardText(ctx.cwd);
|
|
64
|
+
if (text === "") return undefined;
|
|
65
|
+
return { message: { customType: "witness-dashboard", content: text, display: true } };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: witness-brainstorm
|
|
3
|
+
description: Stateless scope interview for a witness effort — one question at a time, each with a firm recommendation, ending in a confirmed scope recap persisted via witness recap. Use when starting a new effort or re-scoping one; normally invoked by /witness.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# witness-brainstorm — scope interview → confirmed recap
|
|
7
|
+
|
|
8
|
+
<!-- Derived (MIT): interview style from mattpocock/skills grill-me (one
|
|
9
|
+
question at a time, always with a recommendation); intent-before-design
|
|
10
|
+
exploration from obra/superpowers brainstorming. Rewritten stateless for
|
|
11
|
+
witness: ends in a confirmed scope recap, writes no document. See NOTICE.md. -->
|
|
12
|
+
|
|
13
|
+
## Ground rules (every witness skill)
|
|
14
|
+
|
|
15
|
+
Resolve the CLI once per session:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.4.0}"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **Never edit `specs/**` or `plans/**`** (the canon dirs — `paths:` in witness.config.yaml may relocate them) — not with an edit tool, not with a write tool, not with Bash redirection. The CLI is the sole writer of state; you author in scratch files under `$(mktemp -d)` and hand them to the CLI. (The canon guard blocks you; the trailer audit catches what it can't.)
|
|
22
|
+
- **Never invoke gate reviewers or relay verdicts.** `witness gate` runs reviewers itself and journals what they said; your summary of a verdict is not evidence.
|
|
23
|
+
- **Refusal repair loop:** a `witness` verb exiting 2 prints structured violations (`field · rule · got · want`). Fix your input and retry — **3 total attempts** per artifact, then stop, show the human the violation list verbatim, and end your turn.
|
|
24
|
+
- **A refused or hook-blocked command is a stop, not a step to drop.** Re-issue it on its own; if it still refuses, tell the human what was blocked and why. Never proceed by deleting the refused half of a compound command — a dropped step is silent, and silence is how a skipped check becomes a shipped defect.
|
|
25
|
+
- **Re-entrancy:** derive position from CLI output (`$WITNESS next`, the dashboard, `log`, `index`) — never from conversation memory. Killed and re-run, you must converge.
|
|
26
|
+
|
|
27
|
+
## What you produce
|
|
28
|
+
|
|
29
|
+
No canon, only evidence: a **confirmed scope recap** persisted by `witness recap` as the birth entry of the effort journal. This skill writes no spec, no plan, no brief — decompose consumes the recap after you, including after session death.
|
|
30
|
+
|
|
31
|
+
## Interview protocol
|
|
32
|
+
|
|
33
|
+
**One question per turn.** Every question carries a concrete recommendation, a one-line why, and the strongest rejected alternative. Lock each answer before the next; walk in dependency order:
|
|
34
|
+
|
|
35
|
+
1. **Class** — `feature | fix | chore`. Recommend from the opening ask: `fix` restores promised behavior; `chore` is motion with no state change (deps, tooling); everything else is `feature`.
|
|
36
|
+
2. **Problem & outcome goals** — what observably changes, for whom. Number them `g1, g2, …`. Each goal is an outcome someone can observe — "Guests can pay by card", never "Integrate Stripe".
|
|
37
|
+
3. **Non-goals** — what this effort deliberately will not do: `n1, n2, …`. Push for at least one; scope without a "no" is unbounded.
|
|
38
|
+
4. **Constraints** — standing limits (compliance, performance budgets, compatibility): `c1, …`. Empty is fine.
|
|
39
|
+
5. **Slice candidates** — non-binding thin vertical slices you would expect decompose to cut. Plain strings.
|
|
40
|
+
|
|
41
|
+
**Short forms.** `fix`: two questions — (1) what breaks, with the observable evidence; (2) the fix boundary — what must NOT change (its non-goals). `chore`: two questions — what maintenance, which area it touches.
|
|
42
|
+
|
|
43
|
+
## Confirmation
|
|
44
|
+
|
|
45
|
+
Render the recap as a table (class · goals · non-goals · constraints · slices) and get an explicit yes. Adjust and re-render until confirmed. The recap is decompose's input contract — a fuzzy recap poisons every stage after it.
|
|
46
|
+
|
|
47
|
+
## Persist
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
DIR=$(mktemp -d)
|
|
51
|
+
cat > "$DIR/recap.json" <<'EOF'
|
|
52
|
+
{
|
|
53
|
+
"effort": "auth-hardening",
|
|
54
|
+
"class": "feature",
|
|
55
|
+
"goals": [{ "id": "g1", "text": "Refresh tokens rotate before expiry" }],
|
|
56
|
+
"non_goals": [{ "id": "n1", "text": "No SSO provider changes" }],
|
|
57
|
+
"constraints": [],
|
|
58
|
+
"slices": ["token rotation"]
|
|
59
|
+
}
|
|
60
|
+
EOF
|
|
61
|
+
$WITNESS recap --file "$DIR/recap.json"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Effort slug: kebab-case `[a-z0-9-]+`, minted from the scope title. A `slug-reuse` refusal means that history exists — mint a different slug; histories never merge, and `--amend` is never the dodge.
|
|
65
|
+
- **Amending scope** — mid-effort scope change, or the decompose gate's stop screen handed off to re-interview: run the interview shortened to what changed, confirm, then `$WITNESS recap --amend <effort> --file "$DIR/recap.json"`. An amendment re-arms the decompose gate's feature-class stop: scope changed, scope gets re-approved.
|
|
66
|
+
- The refusal repair loop applies (3 attempts, then stop with the violation list).
|
|
67
|
+
|
|
68
|
+
## Stateless by design
|
|
69
|
+
|
|
70
|
+
Session died mid-interview? Start over — nothing was persisted; that is the accepted cost. If the dashboard's efforts table already shows the effort (the recap landed), do not re-interview — hand back to /witness.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: witness-decompose
|
|
3
|
+
description: Slice a witness effort's confirmed recap into vertical spec slices, or route a fix to the one spec it amends — manifests handed to witness write, then the decompose gate. Chores never reach this stage. Normally invoked by /witness with the effort slug.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# witness-decompose — recap → sliced specs → gate
|
|
7
|
+
|
|
8
|
+
## Ground rules (every witness skill)
|
|
9
|
+
|
|
10
|
+
Resolve the CLI once per session:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.4.0}"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- **Never edit `specs/**` or `plans/**`** (the canon dirs — `paths:` in witness.config.yaml may relocate them) — not with an edit tool, not with a write tool, not with Bash redirection. The CLI is the sole writer of state; you author in scratch files under `$(mktemp -d)` and hand them to the CLI. (The canon guard blocks you; the trailer audit catches what it can't.)
|
|
17
|
+
- **Never invoke gate reviewers or relay verdicts.** `witness gate` runs reviewers itself and journals what they said; your summary of a verdict is not evidence.
|
|
18
|
+
- **Refusal repair loop:** a `witness` verb exiting 2 prints structured violations (`field · rule · got · want`). Fix your input and retry — **3 total attempts** per artifact, then stop, show the human the violation list verbatim, and end your turn.
|
|
19
|
+
- **A refused or hook-blocked command is a stop, not a step to drop.** Re-issue it on its own; if it still refuses, tell the human what was blocked and why. Never proceed by deleting the refused half of a compound command — a dropped step is silent, and silence is how a skipped check becomes a shipped defect.
|
|
20
|
+
- **Re-entrancy:** derive position from CLI output (`$WITNESS next`, the dashboard, `log`, `index`) — never from conversation memory. Killed and re-run, you must converge.
|
|
21
|
+
|
|
22
|
+
## Inputs (rebuild them, never remember them)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
$WITNESS log <effort> # the latest recap entry is your contract: class, goals g*, non-goals n*, constraints c*
|
|
26
|
+
$WITNESS index # live canon: id · summary · status · depends, grouped by dir
|
|
27
|
+
$WITNESS decide decompose <effort> --show # ONLY when re-entered after a revise
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The class comes from the recap. Never ask for it again.
|
|
31
|
+
|
|
32
|
+
## Route by class
|
|
33
|
+
|
|
34
|
+
- **feature** — slice (next section). Expect the gate to stop for scope approval afterwards: that is the standing stop working, not a failure.
|
|
35
|
+
- **fix** — find **THE one spec** to amend: match the broken behavior against `witness index` summaries; when summaries tie, grep `specs/` read-only. Amend exactly one spec. If the fix genuinely needs a brand-new spec, write it — the gate's tripwire stops for a human, which is the designed check on your routing (on a young canon this fires often; say so rather than fighting it).
|
|
36
|
+
- **chore** — **write NO specs** (a chore writing spec content is refused at write time, by definition of the class). There is nothing here for you: `next` routes a chore straight to the plan stage, because the decompose gate refuses `nothing-to-gate` without written specs and `write` refuses spec content from a chore — the stage is unsatisfiable in both directions. The parent for the coming plan is chosen when that plan is authored. Hand back immediately if you were invoked for one.
|
|
37
|
+
|
|
38
|
+
## Slicing rules (feature)
|
|
39
|
+
|
|
40
|
+
- Thin **vertical** slices: each spec independently shippable, observable end-to-end. Never layers ("the schema", "the client").
|
|
41
|
+
- Each spec answers exactly one question; no two overlap; **one fact, one home**. A fact needed by two slices gets extracted into its own spec, and both `depends` on it.
|
|
42
|
+
- Amending a living spec is the same write with the existing id — its status resets to `draft` (re-approval owed). If the CLI warns the spec has an in-flight child plan, surface that warning at the gate stop.
|
|
43
|
+
- Boundary changes (split, merge, extraction out of a live spec) → the new spec's manifest carries `supersedes: <old-id>`; rewrite every dependent's `depends` in the same effort — the CLI refuses dangling edges.
|
|
44
|
+
- Cross-cutting changes → sequenced per-slice amendments ordered via `depends`, expand-contract style (accept both → switch → drop old), each step independently shippable.
|
|
45
|
+
- Self-check totality before gating: every goal covered by ≥ 1 spec, every spec covers ≥ 1 goal.
|
|
46
|
+
|
|
47
|
+
## Author each spec
|
|
48
|
+
|
|
49
|
+
Manifest + body in scratch, then the write:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
DIR=$(mktemp -d)
|
|
53
|
+
cat > "$DIR/meta.json" <<'EOF'
|
|
54
|
+
{
|
|
55
|
+
"type": "spec",
|
|
56
|
+
"summary": "Refresh tokens rotate before expiry",
|
|
57
|
+
"depends": ["auth-login"],
|
|
58
|
+
"needs": [],
|
|
59
|
+
"criteria": [
|
|
60
|
+
{ "id": "ac-rotate", "test": "@spec:auth-refresh" },
|
|
61
|
+
{ "id": "ac-smoke", "cmd": "npm run smoke:auth" }
|
|
62
|
+
],
|
|
63
|
+
"covers": ["g1"]
|
|
64
|
+
}
|
|
65
|
+
EOF
|
|
66
|
+
cat > "$DIR/body.md" <<'EOF'
|
|
67
|
+
## Motivation
|
|
68
|
+
|
|
69
|
+
Why this slice exists — the recap goal in context, one short paragraph.
|
|
70
|
+
|
|
71
|
+
## Behavior
|
|
72
|
+
|
|
73
|
+
What must observably be true, public surface only. State every `cmd:`
|
|
74
|
+
criterion's expected behavior and thresholds HERE — a fact that lives only
|
|
75
|
+
inside a script has a hidden home and drifts invisibly.
|
|
76
|
+
EOF
|
|
77
|
+
$WITNESS write auth-refresh --effort <effort> --meta "$DIR/meta.json" --body "$DIR/body.md"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Get these right the first time — the dashboard trends your first-try valid rate:
|
|
81
|
+
|
|
82
|
+
- `summary` ≤ 120 chars: what the slice **is** (Motivation carries the why).
|
|
83
|
+
- `criteria`: ≥ 1 entry, unique ids, each exactly one of `test` (value must be `@spec:<this-spec-id>`) or `cmd`. A criterion that cannot fail is not a criterion.
|
|
84
|
+
- `covers`: the recap goal ids this spec honestly advances — the critic judges the mapping's quality.
|
|
85
|
+
- Body: exactly the `## Motivation` and `## Behavior` headings; behavior-only — internal interfaces never appear in a spec.
|
|
86
|
+
- `id`: `[a-z0-9-]+`; new ids land at `specs/<id>.md` (or the repo's configured specs dir).
|
|
87
|
+
- `needs`: external prerequisites only, machine-checkable (`env:` / `cmd:` / `manual:` + `satisfied`).
|
|
88
|
+
- `ui`: set `ui: true` in the manifest **iff** the slice changes browser-rendered or interactive behavior (a screen, a form, client-side interaction). A browser-behavior slice **without** the flag, or a flag on a non-UI slice, is a slicing-critic finding. A `ui` spec earns a **design stage** before its plan — the look is approved before code (Decision 69).
|
|
89
|
+
|
|
90
|
+
## Gate
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
$WITNESS gate decompose --effort <effort> # append --manual when the run asked for it
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **Auto-pass** (green path) → done; hand back to /witness.
|
|
97
|
+
- **Stop** (standing stop, blocking findings, fix-created-spec tripwire) → render the gate output verbatim, print the human's exits — `witness decide decompose <effort> --approve | --revise --note "…" | --stop` — and END YOUR TURN. You never decide.
|
|
98
|
+
- **Re-entered after `--revise`** → `decide --show` reconstructs the verdict + human note; findings anchor to spec headings. Fix via new `witness write` calls (same ids amend in place), self-check totality, re-gate. The 3-round bound is the CLI's — surface it, never fight it. `--show` also emits `state:` and `exits:` — a `reopened` or `settled` state means the verdict above it is history, so act on the `exits:` line, not on remembered findings.
|
|
99
|
+
- Findings implicate the **scope itself** (goals wrong, not slicing wrong)? Tell the human that `--revise --upstream` on the stop screen routes back to re-interview via `witness recap --amend`.
|