@popovych.co/witness 0.5.0 → 0.5.1
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/dist/gates/ship.js +14 -10
- package/dist/gitio.js +22 -0
- package/dist/verbs/next.js +37 -4
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/commands/witness.md +1 -1
- package/plugin/hooks/session-dashboard.sh +1 -1
- package/plugin/skills/witness-brainstorm/SKILL.md +1 -1
- package/plugin/skills/witness-decompose/SKILL.md +1 -1
- package/plugin/skills/witness-design/SKILL.md +1 -1
- package/plugin/skills/witness-implement/SKILL.md +1 -1
- package/plugin/skills/witness-plan/SKILL.md +1 -1
- package/plugin/skills/witness-ship/SKILL.md +1 -1
package/dist/gates/ship.js
CHANGED
|
@@ -11,6 +11,7 @@ import { effortOf, worktreeTreeSha } from '../reviewed.js';
|
|
|
11
11
|
import { worktreePath } from '../worktree.js';
|
|
12
12
|
import { codePromptBody } from './implement.js';
|
|
13
13
|
import { registerGate } from '../gate.js';
|
|
14
|
+
import { gateSettled } from '../verbs/next.js';
|
|
14
15
|
import { lastGateRun } from '../rounds.js';
|
|
15
16
|
import { serializeDoc } from '../fm.js';
|
|
16
17
|
async function commandLane(wt, root, ctx, name, cmd) {
|
|
@@ -46,16 +47,19 @@ registerGate({
|
|
|
46
47
|
const checks = [];
|
|
47
48
|
const entries = readStream(root, planId);
|
|
48
49
|
const lastImplement = lastGateRun(entries, 'implement');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
// One settle predicate, not two. This block used to inline its own copy of
|
|
51
|
+
// gateSettled, which meant `ship` and `next` could answer "is implement settled?"
|
|
52
|
+
// differently for the same journal — the split-brain that reads as a deadlock when
|
|
53
|
+
// two sessions compare notes.
|
|
54
|
+
//
|
|
55
|
+
// The missing sha argument is DELIBERATE and load-bearing, not an oversight. D75's
|
|
56
|
+
// `worktreeTreeSha` covers the whole worktree, so ship's own `pr #N` stamp — pulled
|
|
57
|
+
// into the worktree by the watch-phase rebase — moves the tree and would lapse the
|
|
58
|
+
// very gate ship is checking: approve → pr → lapse → gate, the livelock D75/D77
|
|
59
|
+
// record. `next` re-arms on tree movement because authoring is what it routes to;
|
|
60
|
+
// ship asks only whether implement was ever settled. Passing a sha here re-opens
|
|
61
|
+
// that livelock — do not "fix" this by adding one.
|
|
62
|
+
const implementSettled = gateSettled(entries, 'implement');
|
|
59
63
|
checks.push({ name: 'implement-gate', ok: implementSettled,
|
|
60
64
|
detail: lastImplement
|
|
61
65
|
? `last implement round ${lastImplement.round}: ${lastImplement.outcome}`
|
package/dist/gitio.js
CHANGED
|
@@ -63,13 +63,35 @@ export function dirtyStatePaths(root) {
|
|
|
63
63
|
.filter((p) => !LOCAL_STATE_FILES.has(p));
|
|
64
64
|
}
|
|
65
65
|
export const TRAILER = 'Witness-State: 1';
|
|
66
|
+
// Row 87's rule: nothing to COMMIT is the same legal silent success as nothing to write.
|
|
67
|
+
// It lives here, at the one choke point every state commit passes through, because git
|
|
68
|
+
// rejects BOTH empty cases fatally and a caller cannot tell them apart in advance:
|
|
69
|
+
// • no paths at all → `commit --only ... --` dies "No paths with --include/--only"
|
|
70
|
+
// (a drift sweep with no live specs journals nothing and stamps nothing);
|
|
71
|
+
// • paths with no diff → `commit --only` exits non-zero (a restamp that restores a
|
|
72
|
+
// file to exactly its HEAD content, or a pathspec the repo gitignores).
|
|
73
|
+
// Either way there is no state to record, so HEAD is already the answer. Implementing
|
|
74
|
+
// this at init.ts's call site alone left every other caller throwing out of gitio and
|
|
75
|
+
// surfacing through bin.ts as `unexpected-error` — "report a bug" on a healthy repo.
|
|
76
|
+
function headSha(root) {
|
|
77
|
+
const head = tryGit(root, 'rev-parse', 'HEAD');
|
|
78
|
+
return head.ok ? head.out : ''; // unborn HEAD: no commit exists to name
|
|
79
|
+
}
|
|
66
80
|
export function commitWithTrailer(root, files, subject) {
|
|
81
|
+
if (files.length === 0)
|
|
82
|
+
return ok({ sha: headSha(root) });
|
|
67
83
|
// tryGit, not git: a path already fully processed by a prior `git mv` (both sides
|
|
68
84
|
// of a rename staged atomically) is absent from working tree AND index, so `add`
|
|
69
85
|
// reports it unmatched — even though the rename itself is already staged. `commit
|
|
70
86
|
// --only` resolves its pathspecs against the index-vs-HEAD diff, where the rename
|
|
71
87
|
// is visible, so it still succeeds and is the real validation here.
|
|
72
88
|
tryGit(root, 'add', '--', ...files);
|
|
89
|
+
// Ask git what actually changed rather than assuming every file we touched is dirty.
|
|
90
|
+
// Runs AFTER the add, so a freshly staged untracked path reads as `A ` and commits;
|
|
91
|
+
// a staged rename reads as `R ` on whichever side the pathspec names.
|
|
92
|
+
if (tryGit(root, 'status', '--porcelain', '--', ...files).out === '') {
|
|
93
|
+
return ok({ sha: headSha(root) });
|
|
94
|
+
}
|
|
73
95
|
// --no-verify: state commits are machine-authored and scope-restricted to non-source
|
|
74
96
|
// paths (isStatePath), so a host lint/test hook has nothing to validate here — while
|
|
75
97
|
// its stash/restore step can destroy the human's unrelated dirty work, and a
|
package/dist/verbs/next.js
CHANGED
|
@@ -14,7 +14,7 @@ import { handoffLine, relayLine, resolveHarness } from '../harness.js';
|
|
|
14
14
|
import { worktreeFlow, worktreePath } from '../worktree.js';
|
|
15
15
|
import { lazyStamp } from '../stamp.js';
|
|
16
16
|
import { ok, refuse, renderRefusal, v } from '../refusal.js';
|
|
17
|
-
import { kv } from '../toon.js';
|
|
17
|
+
import { kv, rows } from '../toon.js';
|
|
18
18
|
import { boundReached, lastGateRun, openReopen, pendingDecision } from '../rounds.js';
|
|
19
19
|
// A gate is settled only if the verdict that settled it still describes the CURRENT
|
|
20
20
|
// content. `reviewed_sha` is the tree we actually judged; when the caller can compute
|
|
@@ -60,6 +60,21 @@ function authoringOwed(entries, gate, currentSha) {
|
|
|
60
60
|
e.gate === gate &&
|
|
61
61
|
['revise', 'revise-upstream'].includes(e.decision));
|
|
62
62
|
}
|
|
63
|
+
// D75 re-arms a gate whose reviewed sha has moved, and `worktreeTreeSha` covers the
|
|
64
|
+
// WHOLE worktree — so any state commit reaching it lapses the gate. That is correct, but
|
|
65
|
+
// the row it produces is a bare `gate implement`, indistinguishable from a CLI stuck on
|
|
66
|
+
// a stale answer. A human who just watched that gate pass reads it as the latter, and
|
|
67
|
+
// with a second session in the worktree answering `ship` the pair looks like a deadlock
|
|
68
|
+
// with no error anywhere. The lapse is a fact the CLI already knows; say it.
|
|
69
|
+
function lapseNote(entries, gate, currentSha) {
|
|
70
|
+
const last = lastGateRun(entries, gate);
|
|
71
|
+
if (!last || currentSha === undefined || last.reviewed_sha === currentSha)
|
|
72
|
+
return undefined;
|
|
73
|
+
// sha-free: asks "was it ever settled", which is the only thing that can lapse
|
|
74
|
+
if (!gateSettled(entries, gate))
|
|
75
|
+
return undefined;
|
|
76
|
+
return `${gate} approval lapsed — judged @${last.reviewed_sha.slice(0, 7)}, worktree now @${currentSha.slice(0, 7)} — re-gate to judge the current tree`;
|
|
77
|
+
}
|
|
63
78
|
// The action for ONE flow. A flow is a plan with status `in-progress`: it begins at
|
|
64
79
|
// `witness start` and ends at the merge stamp. Anything else returns undefined and is
|
|
65
80
|
// not a flow. Exported because `--flow` and the dashboard must answer with the SAME
|
|
@@ -87,8 +102,13 @@ export function flowAction(root, cfg, plan) {
|
|
|
87
102
|
const satisfied = files.length > 0 && baseR.ok && evidenceForDiff(wt, root, plan, baseR.value).satisfied;
|
|
88
103
|
if (!satisfied)
|
|
89
104
|
return { line: `witness test-evidence ${id} --phase red|green`, stage: 'implement', target: id, ...inWorktree };
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
const treeSha = worktreeTreeSha(wt);
|
|
106
|
+
if (!gateSettled(entries, 'implement', treeSha)) {
|
|
107
|
+
return {
|
|
108
|
+
line: `witness gate implement ${id}`, target: id, ...inWorktree,
|
|
109
|
+
...noteOf(lapseNote(entries, 'implement', treeSha)),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
92
112
|
return { line: `witness ship ${id}`, stage: 'ship', target: id, ...atRoot };
|
|
93
113
|
}
|
|
94
114
|
// How far along a flow is — the drain order when several are actionable. Most-advanced
|
|
@@ -198,8 +218,13 @@ export function computeNext(root, ctx, canon, cfg) {
|
|
|
198
218
|
.sort((a, b) => flowRank(b) - flowRank(a) || String(a.target).localeCompare(String(b.target)));
|
|
199
219
|
if (flows.length > 0) {
|
|
200
220
|
const waiting = pendingDecisionsAll(root, efforts, specs, plans);
|
|
221
|
+
// noteOf, not an overwrite: a lapse note explains WHY this row is the gate rather
|
|
222
|
+
// than the ship it was a moment ago, and must not be evicted by the waiting list.
|
|
201
223
|
return waiting.length > 0
|
|
202
|
-
? {
|
|
224
|
+
? {
|
|
225
|
+
...flows[0],
|
|
226
|
+
...noteOf(flows[0].note, `${waiting.length} waiting: ${waiting.map((w) => `${w.gate} ${w.target}`).join(' · ')}`),
|
|
227
|
+
}
|
|
203
228
|
: flows[0];
|
|
204
229
|
}
|
|
205
230
|
// TIER 2 — human-owed decisions and bound endgames. Unchanged from today's rungs; they
|
|
@@ -398,6 +423,14 @@ export async function run(ctx, argv) {
|
|
|
398
423
|
const lazy = lazyStamp(root, ctx, canon);
|
|
399
424
|
if (lazy.stamped.length > 0)
|
|
400
425
|
canon = loadCanon(root);
|
|
426
|
+
// `dashboard` renders these; `next` computed them and dropped them. A merge stamp that
|
|
427
|
+
// cannot proceed (commit refused, PR closed unmerged, lock held) leaves the flow parked
|
|
428
|
+
// at ship forever, and `next` — the one verb the driving loop calls every turn — said
|
|
429
|
+
// nothing at all. Printed BEFORE the routing block so it cannot split the contiguous
|
|
430
|
+
// next:/stage:/target:/note:/home:/run:/relay: unit the stage skills read verbatim.
|
|
431
|
+
if (lazy.stale.length > 0) {
|
|
432
|
+
rows('stale', ['plan', 'why'], lazy.stale).forEach(ctx.out);
|
|
433
|
+
}
|
|
401
434
|
const { values } = parseArgs({ args: argv, options: { flow: { type: 'string' } }, allowPositionals: true });
|
|
402
435
|
// Precedence: an explicit --flow is a CLAIM about a flow and refuses when false.
|
|
403
436
|
// A worktree cwd is AMBIENT context — it may scope a read-only question, never select
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popovych.co/witness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"spec-driven-development",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "witness",
|
|
3
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.5.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Kostiantyn Popovych"
|
|
7
7
|
},
|
|
@@ -10,7 +10,7 @@ The CLI decides, you act. If `$ARGUMENTS` contains `--manual`, append `--manual`
|
|
|
10
10
|
Resolve the CLI once:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.1}"
|
|
14
14
|
```
|
|
15
15
|
|
|
16
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.
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
# context). Silent everywhere that isn't a witness repo; failures swallowed:
|
|
4
4
|
# session start must never hang or die on this.
|
|
5
5
|
[ -f witness.config.yaml ] || exit 0
|
|
6
|
-
${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.
|
|
6
|
+
${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.1} 2>/dev/null || true
|
|
@@ -15,7 +15,7 @@ witness: ends in a confirmed scope recap, writes no document. See NOTICE.md. -->
|
|
|
15
15
|
Resolve the CLI once per session:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.1}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
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.)
|
|
@@ -10,7 +10,7 @@ description: Slice a witness effort's confirmed recap into vertical spec slices,
|
|
|
10
10
|
Resolve the CLI once per session:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.1}"
|
|
14
14
|
```
|
|
15
15
|
|
|
16
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.)
|
|
@@ -16,7 +16,7 @@ No text copied verbatim; the design-critic lens is new material. -->
|
|
|
16
16
|
Resolve the CLI once per session:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.
|
|
19
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.1}"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
- **Never edit `specs/**`, `plans/**`, or `designs/**`** — the CLI is the sole writer of state. Author HTML in `$(mktemp -d)` and hand it to `witness design`. (The canon guard blocks direct edits; the trailer audit catches end-runs.)
|
|
@@ -15,7 +15,7 @@ tests carry the parent spec's tag in their NAME. See NOTICE.md. -->
|
|
|
15
15
|
Resolve the CLI once per session:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.1}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
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.)
|
|
@@ -15,7 +15,7 @@ parent's criteria. See NOTICE.md. -->
|
|
|
15
15
|
Resolve the CLI once per session:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.1}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
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.)
|
|
@@ -15,7 +15,7 @@ model can. See NOTICE.md. -->
|
|
|
15
15
|
Resolve the CLI once per session:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.5.1}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
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.)
|