@popovych.co/witness 0.14.0 → 0.14.2
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/drive.js +6 -5
- package/dist/gates/plan.js +2 -4
- package/dist/gitio.js +6 -0
- package/dist/matcher.js +9 -1
- package/dist/runner.js +4 -1
- package/dist/scan.js +7 -0
- package/dist/verbs/next.js +37 -5
- 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/drive.js
CHANGED
|
@@ -47,11 +47,12 @@ export function classifyAction(action, root) {
|
|
|
47
47
|
if (line.includes('merge PR') || (action.note?.includes('merge PR') ?? false)) {
|
|
48
48
|
return { kind: 'merge', line: action.note?.includes('merge PR') ? action.note : line };
|
|
49
49
|
}
|
|
50
|
-
// `witness check` is computeNext's terminal answer
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// `drive: idle — witness check` runs the verb
|
|
54
|
-
// session
|
|
50
|
+
// `witness check` is computeNext's terminal answer in two states: canon errors that
|
|
51
|
+
// stopped routing early (bare line), and an exhausted ladder (line + a "nothing is
|
|
52
|
+
// owed" note, D157). Idle PRINTS the line rather than swallowing it: a human who sees
|
|
53
|
+
// `drive: idle — witness check` runs the verb, and the note has already told the
|
|
54
|
+
// routed session which state it is in. Spawning a session to guess would burn a
|
|
55
|
+
// context on a diagnosis one command answers.
|
|
55
56
|
if (line === 'witness check' && action.stage === undefined)
|
|
56
57
|
return { kind: 'idle' };
|
|
57
58
|
return { kind: 'spawn', home: action.home ?? root, stage: action.stage, target: action.target, model: action.model };
|
package/dist/gates/plan.js
CHANGED
|
@@ -3,7 +3,7 @@ import { serializeDoc } from '../fm.js';
|
|
|
3
3
|
import { canonicalSha, short } from '../sha.js';
|
|
4
4
|
import { latestRecap } from '../journal.js';
|
|
5
5
|
import { baseForSpec } from '../history.js';
|
|
6
|
-
import { findById } from '../scan.js';
|
|
6
|
+
import { findById, plannableParent } from '../scan.js';
|
|
7
7
|
import { effortOf, planPairSha } from '../reviewed.js';
|
|
8
8
|
import { validateDoc } from '../schemas.js';
|
|
9
9
|
import { registerGate } from '../gate.js';
|
|
@@ -29,9 +29,7 @@ registerGate({
|
|
|
29
29
|
const schemaProblems = validateDoc(plan.meta, plan.body).map((x) => `${x.field}:${x.rule}`);
|
|
30
30
|
checks.push({ name: 'schema', ok: schemaProblems.length === 0, detail: schemaProblems.slice(0, 5).join(' · ') });
|
|
31
31
|
const parentStatus = String(parent.meta.status);
|
|
32
|
-
const parentOk = parent
|
|
33
|
-
? parentStatus === 'approved'
|
|
34
|
-
: parentStatus === 'approved' || parentStatus === 'live';
|
|
32
|
+
const parentOk = plannableParent(parent);
|
|
35
33
|
checks.push({ name: 'parent-approved', ok: parentOk,
|
|
36
34
|
detail: `${String(parent.meta.id)} is ${parentStatus}` });
|
|
37
35
|
const parentSha = canonicalSha(parent.meta, parent.body);
|
package/dist/gitio.js
CHANGED
|
@@ -2,11 +2,17 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { canonPaths } from './config.js';
|
|
4
4
|
import { ok, refuse, v } from './refusal.js';
|
|
5
|
+
// D158 (#20). A reviewer-bound diff is legitimately multi-megabyte and Node's spawnSync
|
|
6
|
+
// default is 1 MiB, which crashed ship/gate as `spawnSync git ENOBUFS` — and, worse,
|
|
7
|
+
// silently truncated every tryGit consumer into a wrong answer. Sized like reviewer.ts's
|
|
8
|
+
// 64 MiB and ship.ts's 16 MiB; V8's string ceiling bounds any sync read regardless.
|
|
9
|
+
const GIT_MAX_BUFFER = 256 * 1024 * 1024;
|
|
5
10
|
export function git(root, ...args) {
|
|
6
11
|
return execFileSync('git', args, {
|
|
7
12
|
cwd: root,
|
|
8
13
|
encoding: 'utf8',
|
|
9
14
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
15
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
10
16
|
}).replace(/\n$/, '');
|
|
11
17
|
}
|
|
12
18
|
export function tryGit(root, ...args) {
|
package/dist/matcher.js
CHANGED
|
@@ -35,7 +35,15 @@ export function sourceTags(root, excludes) {
|
|
|
35
35
|
if (skip.some((re) => re.test(rel)))
|
|
36
36
|
continue;
|
|
37
37
|
const abs = join(root, rel);
|
|
38
|
-
if (!existsSync(abs)
|
|
38
|
+
if (!existsSync(abs))
|
|
39
|
+
continue;
|
|
40
|
+
// D159 (#23). The listing is paths, not files: `--cached` includes a gitlink
|
|
41
|
+
// (submodule, mode 160000) and `--others` a symlink to a directory (skills-add
|
|
42
|
+
// wiring) — both stat as directories whose size clears the cap, and readFileSync
|
|
43
|
+
// threw EISDIR. Same isFile() guard codePromptBody applies to untracked paths;
|
|
44
|
+
// first shipped as a hand patch in a consumer's npx cache, upstreamed here.
|
|
45
|
+
const st = statSync(abs);
|
|
46
|
+
if (!st.isFile() || st.size > MAX_SCAN_BYTES)
|
|
39
47
|
continue;
|
|
40
48
|
const content = readFileSync(abs, 'utf8');
|
|
41
49
|
if (content.includes('\0'))
|
package/dist/runner.js
CHANGED
|
@@ -43,7 +43,10 @@ export function criteriaExcludes(cfg) {
|
|
|
43
43
|
}
|
|
44
44
|
export function execCommand(runRoot, ctx, cmd) {
|
|
45
45
|
try {
|
|
46
|
-
|
|
46
|
+
// D158. Suite stdout is unbounded and only the last 4000 chars are kept — but at
|
|
47
|
+
// Node's 1 MiB default a chatty GREEN suite threw ENOBUFS into the catch and read
|
|
48
|
+
// as a failed lane. 64 MiB: the same bound the reviewer spawn uses.
|
|
49
|
+
const out = execSync(cmd, { cwd: runRoot, env: ctx.env, stdio: 'pipe', maxBuffer: 64 * 1024 * 1024 });
|
|
47
50
|
return { exitZero: true, output: String(out).slice(-4000) };
|
|
48
51
|
}
|
|
49
52
|
catch (e) {
|
package/dist/scan.js
CHANGED
|
@@ -45,6 +45,13 @@ export function criteriaOwner(canon, id) {
|
|
|
45
45
|
export function findById(canon, id) {
|
|
46
46
|
return canon.docs.find((d) => d.meta.id === id);
|
|
47
47
|
}
|
|
48
|
+
// D157. The one derivation of "this doc can parent a plan today". `verbs/next.ts`'s
|
|
49
|
+
// queue and `gates/plan.ts`'s parent check both consume it; a second derivation is the
|
|
50
|
+
// drift that left `next` refusing to emit the exact command the gate accepts (issue #21).
|
|
51
|
+
export function plannableParent(doc) {
|
|
52
|
+
const s = String(doc.meta.status);
|
|
53
|
+
return doc.meta.type === 'principles' ? s === 'approved' : s === 'approved' || s === 'live';
|
|
54
|
+
}
|
|
48
55
|
export function findCycle(canon, extra) {
|
|
49
56
|
const deps = new Map();
|
|
50
57
|
for (const d of canon.docs) {
|
package/dist/verbs/next.js
CHANGED
|
@@ -4,7 +4,7 @@ import { EXIT } from '../cli.js';
|
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { pendingTxn } from '../txn.js';
|
|
6
6
|
import { primaryRoot } from '../gitio.js';
|
|
7
|
-
import { findById, loadCanon } from '../scan.js';
|
|
7
|
+
import { findById, loadCanon, plannableParent } from '../scan.js';
|
|
8
8
|
import { designArtifactCurrent, designPending, designUnseen } from '../design.js';
|
|
9
9
|
import { effortAbandoned, effortStreams, latestRecap, readStream } from '../journal.js';
|
|
10
10
|
import { diffReviewedSha, effortOf, effortReviewedSha, effortSpecs, effortWrites, implementReviewedSha, planPairSha } from '../reviewed.js';
|
|
@@ -425,6 +425,22 @@ function liveOwner(root, efforts, ...ids) {
|
|
|
425
425
|
.map((id) => efforts.find((e) => effortWrites(root, e.slug).has(id))?.slug)
|
|
426
426
|
.find((slug) => slug !== undefined);
|
|
427
427
|
}
|
|
428
|
+
// D157. The queue offered `<spec>-plan-1` unconditionally, which amends a DONE plan the
|
|
429
|
+
// moment a spec earns a second slice. An abandoned id is re-authorable — `write` returns
|
|
430
|
+
// it to draft — so the lowest abandoned suffix is offered back; otherwise the first
|
|
431
|
+
// unused one. When this runs, the spec's plans are all done or abandoned by filter B.
|
|
432
|
+
function nextPlanId(plans, spec) {
|
|
433
|
+
const suffixed = plans
|
|
434
|
+
.map((p) => ({ id: String(p.meta.id), status: String(p.meta.status) }))
|
|
435
|
+
.filter((p) => p.id.startsWith(`${spec}-plan-`))
|
|
436
|
+
.map((p) => ({ n: Number(p.id.slice(`${spec}-plan-`.length)), status: p.status }))
|
|
437
|
+
.filter((p) => Number.isInteger(p.n) && p.n > 0);
|
|
438
|
+
const abandoned = suffixed.filter((p) => p.status === 'abandoned').map((p) => p.n);
|
|
439
|
+
if (abandoned.length > 0)
|
|
440
|
+
return `${spec}-plan-${Math.min(...abandoned)}`;
|
|
441
|
+
const taken = suffixed.map((p) => p.n);
|
|
442
|
+
return `${spec}-plan-${taken.length > 0 ? Math.max(...taken) + 1 : 1}`;
|
|
443
|
+
}
|
|
428
444
|
// The stage rides with the line: routing to a recap is BRAINSTORM work, and a caller that
|
|
429
445
|
// kept its own `stage: 'plan'` would hand the plan skill a recap command to run.
|
|
430
446
|
function planWriteAction(root, efforts, planId, parentId, target) {
|
|
@@ -607,8 +623,20 @@ export function computeNext(root, ctx, canon, cfg) {
|
|
|
607
623
|
const s = String(doc.meta.status);
|
|
608
624
|
return doc.meta.type === 'plan' ? s === 'done' : s === 'live' || doc.meta.type === 'principles';
|
|
609
625
|
};
|
|
626
|
+
// D157. A spec is queued when the plan gate would accept it as a parent AND someone owes
|
|
627
|
+
// it a plan. `approved` is owed by definition (decompose just passed). `live` is owed
|
|
628
|
+
// only while a live effort that wrote it owns zero non-abandoned plans — efforts never
|
|
629
|
+
// auto-terminate, so without that clause every finished spec would queue forever.
|
|
630
|
+
// Reaching this rung proves decompose is settled: rung 6 returns first otherwise.
|
|
631
|
+
const owedByEffort = (id) => efforts.some((e) => {
|
|
632
|
+
const w = effortWrites(root, e.slug);
|
|
633
|
+
if (!w.has(id))
|
|
634
|
+
return false;
|
|
635
|
+
return !plans.some((p) => w.has(String(p.meta.id)) && String(p.meta.status) !== 'abandoned');
|
|
636
|
+
});
|
|
610
637
|
const planless = canon.docs
|
|
611
|
-
.filter((d) => d.meta.type === 'spec' &&
|
|
638
|
+
.filter((d) => d.meta.type === 'spec' && plannableParent(d))
|
|
639
|
+
.filter((d) => String(d.meta.status) === 'approved' || owedByEffort(String(d.meta.id)))
|
|
612
640
|
.filter((d) => !plans.some((p) => String(p.meta.parent) === String(d.meta.id) &&
|
|
613
641
|
!['done', 'abandoned'].includes(String(p.meta.status))))
|
|
614
642
|
.filter((d) => (d.meta.depends ?? []).every(ready))
|
|
@@ -617,9 +645,9 @@ export function computeNext(root, ctx, canon, cfg) {
|
|
|
617
645
|
// A spec whose plan write can actually be booked outranks one that needs a new effort
|
|
618
646
|
// opened first — stalling the whole pipeline on a recap while runnable work sits behind
|
|
619
647
|
// it is a worse answer, and alphabetical order alone does not know the difference.
|
|
620
|
-
const spec = planless.find((s) => liveOwner(root, efforts,
|
|
648
|
+
const spec = planless.find((s) => liveOwner(root, efforts, nextPlanId(plans, s), s) !== undefined)
|
|
621
649
|
?? planless[0];
|
|
622
|
-
const act = planWriteAction(root, efforts,
|
|
650
|
+
const act = planWriteAction(root, efforts, nextPlanId(plans, spec), spec, spec);
|
|
623
651
|
if (planless.length <= 1)
|
|
624
652
|
return act;
|
|
625
653
|
// The note said `multiple ready — choose: a b c`, which is a list, not a ranking: it
|
|
@@ -653,7 +681,11 @@ export function computeNext(root, ctx, canon, cfg) {
|
|
|
653
681
|
return { line: `witness start ${String(plan.meta.id)}`, target: String(plan.meta.id) };
|
|
654
682
|
}
|
|
655
683
|
}
|
|
656
|
-
|
|
684
|
+
// Rule B — the ladder is exhausted. Same line as rule A (canon errors, the early
|
|
685
|
+
// return above) but a different state; the note is what tells them apart on screen
|
|
686
|
+
// (D157 — the missing note is what turned issue #21 from a one-command diagnosis
|
|
687
|
+
// into a filed bug).
|
|
688
|
+
return { line: 'witness check', ...noteOf('nothing is owed — no effort, spec or plan has a next step; a clean check confirms it') };
|
|
657
689
|
}
|
|
658
690
|
// Exported for `drive`, which claims a flow with the same `--flow` semantics `next` has:
|
|
659
691
|
// a claim that refuses when false. A second copy of these three refusals would be a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popovych.co/witness",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
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.14.
|
|
4
|
+
"version": "0.14.2",
|
|
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.14.
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
|
|
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.14.
|
|
6
|
+
${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2} 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.14.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
- **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
|
|
@@ -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.14.
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
- **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
|
|
@@ -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.14.
|
|
19
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
- **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
|
|
@@ -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.14.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
- **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
|
|
@@ -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.14.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
- **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
|
|
@@ -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.14.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.2}"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
- **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
|