@popovych.co/witness 0.14.7 → 0.14.8

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/abandon.js CHANGED
@@ -10,7 +10,7 @@ import { crashPoint, withTxn } from './txn.js';
10
10
  import { appendEntry, effortAbandoned, effortStreams, entryLine, journalRel, readStream } from './journal.js';
11
11
  import { findById, plansOn } from './scan.js';
12
12
  import { effortOf, effortWrites } from './reviewed.js';
13
- import { ok, refuse, v } from './refusal.js';
13
+ import { humanCall, ok, refuse, v } from './refusal.js';
14
14
  import { prepareStamp, writeStamp } from './stamp.js';
15
15
  import { kv } from './toon.js';
16
16
  import { removeWorktree, worktreePath } from './worktree.js';
@@ -37,6 +37,21 @@ export function pinCommit(root, rel, pin, from) {
37
37
  return found;
38
38
  }
39
39
  export const keptLine = (k) => `${String(k.doc.meta.id)} — not unwound · still standing on it: ${k.standing.join(' ')}`;
40
+ // D167. The abandon says everything it did to canon, on both paths, as `kept:` already did: what
41
+ // it reverted, what came back with it, and what the walk took back on the way.
42
+ export function abandonLines(items, kept) {
43
+ return [
44
+ ...items.flatMap((item) => [
45
+ ...(item.revert ? [kv('reverted', `${String(item.revert.doc.meta.id)} (${item.revert.action}) — journal untouched`)] : []),
46
+ ...(item.revert?.returns ?? []).flatMap((r) => [
47
+ kv('returned', `${r.entry.artifact} — back in canon as ${r.entry.to} · the re-slice into ${r.by} is taken back`),
48
+ ...r.unwound.map((u) => kv('unwound', `${u.artifact} — stays out of canon · an abandoned effort created it`)),
49
+ ]),
50
+ ...(item.kept ? [kv('kept', keptLine(item.kept))] : []),
51
+ ]),
52
+ ...kept.map((k) => kv('kept', keptLine(k))),
53
+ ];
54
+ }
40
55
  // D166 (review). The writes the way back takes back besides this abandon's own targets: every
41
56
  // write of an effort already abandoned — whose abandon skipped a spec a later re-slice had
42
57
  // departed — and every write of the effort being abandoned now (`effort`, absent on the plan
@@ -117,6 +132,28 @@ export function specRevertFor(root, canon, spec, pin, abandonSet, effort) {
117
132
  const design = priorDesign !== undefined || existsSync(join(root, designR)) ? { rel: designR, prior: priorDesign } : undefined;
118
133
  return ok({ doc: spec, action: 'restore', content: prior.out, toStatus, design, returns });
119
134
  }
135
+ // D167. A return lands on the path its spec left from, and `rename` moves an id but leaves its
136
+ // file, so another doc may hold that path — or another spec this abandon brings back may have
137
+ // left from it too. Written over, either left canon with nothing to say so. Only a doc this same
138
+ // abandon departs may stand there; any other holder, or a second return, is a human's call.
139
+ export function returnCollisions(root, canon, items) {
140
+ const departing = new Set(items.flatMap((i) => (i.revert?.action === 'delete' ? [i.revert.doc.rel] : [])));
141
+ const returns = items.flatMap((i) => (i.revert?.returns ?? []).map((r) => ({ r, going: String(i.revert.doc.meta.id) })));
142
+ const landing = new Map();
143
+ for (const { r } of returns) {
144
+ const ids = landing.get(r.rel) ?? [];
145
+ if (!ids.includes(r.entry.artifact))
146
+ landing.set(r.rel, [...ids, r.entry.artifact]);
147
+ }
148
+ return [
149
+ ...returns.filter(({ r }) => !departing.has(r.rel) && existsSync(join(root, r.rel))).map(({ r, going }) => {
150
+ const held = canon.docs.find((d) => d.rel === r.rel);
151
+ const holder = held ? String(held.meta.id) : 'a doc canon cannot read';
152
+ return humanCall(v('spec', 'path-taken', `${r.rel} holds ${holder}`, `that path free first — ${r.entry.artifact} comes back to it when ${going} goes · what becomes of ${holder} is a human's call`));
153
+ }),
154
+ ...[...landing].filter(([, ids]) => ids.length > 1).map(([rel, ids]) => humanCall(v('spec', 'path-taken', `${rel} takes ${ids.join(' and ')}`, `one return per path — each left from it · which one comes back is a human's call`))),
155
+ ];
156
+ }
120
157
  export function executeAbandon(root, ctx, items, effortEntry, subject) {
121
158
  const files = new Set();
122
159
  const journalMulti = [];
@@ -187,10 +224,13 @@ export function executeAbandon(root, ctx, items, effortEntry, subject) {
187
224
  else if (d !== undefined && existsSync(join(root, d.rel)))
188
225
  unlinkSync(join(root, d.rel));
189
226
  }
190
- for (const r of item.revert.returns)
191
- r.restore();
192
227
  }
193
228
  }
229
+ // D167. Every departure lands before any return (P9b): restored per item, a later item's
230
+ // departure unlinked the spec an earlier item's return had just brought back to its path.
231
+ for (const item of items)
232
+ for (const r of item.revert?.returns ?? [])
233
+ r.restore();
194
234
  for (const s of stamps)
195
235
  writeStamp(root, s);
196
236
  for (const se of specEntries)
package/dist/adopt.js CHANGED
@@ -6,9 +6,11 @@ import { readStream } from './journal.js';
6
6
  import { canonicalSha } from './sha.js';
7
7
  const hasTrailer = (root, sha) => git(root, 'show', '-s', '--format=%B', sha).split('\n').some((l) => l.trim() === TRAILER);
8
8
  const commitsTouching = (root, rel) => git(root, 'log', '--format=%H', '--', rel).split('\n').filter(Boolean);
9
- export function lastWitnessed(root, rel) {
9
+ // `absolved`: commits an `adopt` entry took in (`adoptedCommits`), which count as witnessed too —
10
+ // a hand commit that `adopt` absolved is the doc's witnessed state from then on.
11
+ export function lastWitnessed(root, rel, absolved = new Set()) {
10
12
  for (const sha of commitsTouching(root, rel)) {
11
- if (!hasTrailer(root, sha))
13
+ if (!hasTrailer(root, sha) && !absolved.has(sha))
12
14
  continue;
13
15
  const raw = tryGit(root, 'show', `${sha}:${rel}`);
14
16
  if (!raw.ok)
@@ -16,7 +18,7 @@ export function lastWitnessed(root, rel) {
16
18
  const doc = splitDoc(raw.out);
17
19
  if (!doc.ok)
18
20
  continue;
19
- return { sha: canonicalSha(doc.value.meta, doc.value.body), criteria: doc.value.meta.criteria ?? null };
21
+ return { sha: canonicalSha(doc.value.meta, doc.value.body), criteria: doc.value.meta.criteria ?? null, id: String(doc.value.meta.id) };
20
22
  }
21
23
  return undefined;
22
24
  }
package/dist/departure.js CHANGED
@@ -4,6 +4,7 @@ import { deferralMoves } from './deferral.js';
4
4
  import { designRel } from './design.js';
5
5
  import { FLOOR_STREAM } from './floor.js';
6
6
  import { splitDoc } from './fm.js';
7
+ import { adoptedCommits, lastWitnessed } from './adopt.js';
7
8
  import { gitBlob, tryGit } from './gitio.js';
8
9
  import { journalRel, readStream } from './journal.js';
9
10
  import { v } from './refusal.js';
@@ -121,6 +122,8 @@ function returnsThrough(root, paths, commit, chain, unwound, takenBack, seen) {
121
122
  // Each link of the chain hands home the debts that came to it from the next one down.
122
123
  moves: chain.flatMap((s, k) => deferralMoves(readStream(root, s), s, id, (d) => d.moved_from === (chain[k + 1] ?? id))),
123
124
  unwound,
125
+ rel,
126
+ by: successor,
124
127
  }];
125
128
  });
126
129
  }
@@ -145,3 +148,22 @@ export function idRetired(root, id, type) {
145
148
  return v('id', 'id-taken', id, `a new id — ${id} was a spec its abandon unwound`);
146
149
  return undefined;
147
150
  }
151
+ // D167. Whether a doc under this id may land on this path, which was last witnessed (by a
152
+ // trailer, or by an `adopt` that absolved the commit) under `prior`. A superseded spec comes back
153
+ // to the path it left from, so its departure holds that path as it holds the id. Any other id
154
+ // left a path only by hand, since `rename` keeps the file: the path is held only while the doc
155
+ // on it is what `prior` became — an in-place id change, which is `rename` done by hand. A
156
+ // removal, a `prior` living at another path, or a path left empty holds nothing: nothing lands back.
157
+ export function pathRetired(root, canon, rel, id) {
158
+ const prior = lastWitnessed(root, rel, adoptedCommits(root))?.id;
159
+ if (prior === undefined || prior === id)
160
+ return undefined;
161
+ const exit = canonExit(readStream(root, prior));
162
+ if (exit?.to === 'superseded') {
163
+ return v('id', 'path-taken', rel, `a new id — ${rel} is where ${prior} comes back if the re-slice that superseded it is taken back`);
164
+ }
165
+ const elsewhere = findById(canon, prior);
166
+ if (exit !== undefined || (elsewhere !== undefined && elsewhere.rel !== rel) || !existsSync(join(root, rel)))
167
+ return undefined;
168
+ return v('id', 'path-taken', rel, `the id it was witnessed under — ${rel} held ${prior} · witness rename is how an id changes`);
169
+ }
package/dist/gate.js CHANGED
@@ -470,10 +470,12 @@ export async function runGate(ctx, gateName, target, flags) {
470
470
  const marker = {
471
471
  op: `gate-${spec.gate}`,
472
472
  files: [...new Set(files)],
473
+ // D167. Per stream, the marker lists lines in the order the body appends them, because
474
+ // `landedJournal` reads a crash by that order — and the plan gate stamps its own target.
473
475
  journalMulti: [
474
476
  { stream: target, line: entryLine(entry) },
475
- ...stamps.map((s) => ({ stream: s.stream, line: s.line })),
476
477
  ...discharged.map((d) => ({ stream: target, line: entryLine(d) })),
478
+ ...stamps.map((s) => ({ stream: s.stream, line: s.line })),
477
479
  ],
478
480
  };
479
481
  const txn = withTxn(root, marker, () => {
@@ -8,6 +8,10 @@ import { effortOf, planPairSha } from '../reviewed.js';
8
8
  import { validateDoc } from '../schemas.js';
9
9
  import { registerGate } from '../gate.js';
10
10
  import { designPending, designStamp } from '../design.js';
11
+ // D167. One derivation for the gate's stamp and `decide`'s: a pass approves a draft and nothing
12
+ // else. Row 95 keeps an in-flight status through a re-author, and a passing re-gate must not
13
+ // take it back — nor stamp a finished plan approved again.
14
+ const draftApproval = (plan) => String(plan.meta.status) === 'draft' ? [{ artifact: String(plan.meta.id), to: 'approved' }] : [];
11
15
  registerGate({
12
16
  gate: 'plan',
13
17
  targetKind: 'plan',
@@ -69,7 +73,7 @@ registerGate({
69
73
  },
70
74
  promptBody,
71
75
  checks,
72
- stamps: [{ artifact: planId, to: 'approved' }],
76
+ stamps: draftApproval(plan),
73
77
  });
74
78
  },
75
79
  currentSha(_root, canon, _cfg, planId) {
@@ -83,6 +87,6 @@ registerGate({
83
87
  },
84
88
  approveStamps(_root, canon, planId) {
85
89
  const doc = findById(canon, planId);
86
- return doc && String(doc.meta.status) === 'draft' ? [{ artifact: planId, to: 'approved' }] : [];
90
+ return doc ? draftApproval(doc) : [];
87
91
  },
88
92
  });
package/dist/refusal.js CHANGED
@@ -11,13 +11,18 @@ export function v(field, rule, got, want, remedy) {
11
11
  export const humanCall = (x) => ({ ...x, human: true });
12
12
  export function renderRefusal(violations) {
13
13
  const runnable = [...new Set(violations.map((x) => x.remedy).filter((r) => r !== undefined && !/<[^>]+>/.test(r)))];
14
+ // D167. Row 108's principle cuts both ways: the stop is true for the human's rows and false for
15
+ // the reader's own, so the trailer names the human's rules and leaves the rest self-repairable.
16
+ const human = [...new Set(violations.filter((x) => x.human).map((x) => x.rule))];
17
+ const mixed = violations.some((x) => !x.human);
14
18
  return [
15
19
  // The destructure keeps `remedy` out of the row objects: the column list already
16
20
  // governs what prints, but a remedy is not a column and must not become one.
17
21
  ...rows('refused', ['field', 'rule', 'got', 'want'], violations.map(({ remedy: _remedy, ...rest }) => rest)),
18
22
  ...runnable.map((r) => `run: ${r}`),
19
- violations.some((x) => x.human)
20
- ? 'help: a row here is a human\'s decision — stop and surface the list; re-running cannot clear it'
23
+ human.length
24
+ ? `help: a human's decision: ${human.join(' · ')} — stop and surface the list; re-running cannot clear it`
25
+ + (mixed ? ' · the other rows are structured for self-repair once it is settled' : '')
21
26
  : 'help: fix each row and re-run — rows are structured for self-repair',
22
27
  ];
23
28
  }
@@ -7,7 +7,7 @@ import { effortWrites } from '../reviewed.js';
7
7
  import { canonicalSha } from '../sha.js';
8
8
  import { renderRefusal, v } from '../refusal.js';
9
9
  import { kv } from '../toon.js';
10
- import { executeAbandon, keptLine, planItems, specRevertFor } from '../abandon.js';
10
+ import { abandonLines, executeAbandon, planItems, returnCollisions, specRevertFor } from '../abandon.js';
11
11
  export async function run(ctx, argv) {
12
12
  const target = argv.find((a) => !a.startsWith('--'));
13
13
  if (!target) {
@@ -35,17 +35,18 @@ export async function run(ctx, argv) {
35
35
  renderRefusal(itemR.violations).forEach((l) => ctx.err(l));
36
36
  return EXIT.REFUSED;
37
37
  }
38
+ const taken = returnCollisions(root, canon, [itemR.value]);
39
+ if (taken.length > 0) {
40
+ renderRefusal(taken).forEach((l) => ctx.err(l));
41
+ return EXIT.REFUSED;
42
+ }
38
43
  const done = executeAbandon(root, ctx, [itemR.value], undefined, `abandon(${target})`);
39
44
  if (!done.ok) {
40
45
  renderRefusal(done.violations).forEach((l) => ctx.err(l));
41
46
  return EXIT.REFUSED;
42
47
  }
43
48
  ctx.out(kv('abandoned', target));
44
- if (itemR.value.revert) {
45
- ctx.out(kv('reverted', `${String(itemR.value.revert.doc.meta.id)} (${itemR.value.revert.action}) — journal untouched`));
46
- }
47
- if (itemR.value.kept)
48
- ctx.out(kv('kept', keptLine(itemR.value.kept)));
49
+ abandonLines([itemR.value], []).forEach((l) => ctx.out(l));
49
50
  return EXIT.OK;
50
51
  }
51
52
  if (streamExists(root, target) && latestRecap(root, target)) {
@@ -103,6 +104,7 @@ export async function run(ctx, argv) {
103
104
  else if (outcome)
104
105
  items.push({ revert: outcome });
105
106
  }
107
+ problems.push(...returnCollisions(root, canon, items));
106
108
  if (problems.length > 0) {
107
109
  renderRefusal(problems).forEach((l) => ctx.err(l));
108
110
  return EXIT.REFUSED;
@@ -112,9 +114,8 @@ export async function run(ctx, argv) {
112
114
  renderRefusal(done.violations).forEach((l) => ctx.err(l));
113
115
  return EXIT.REFUSED;
114
116
  }
115
- ctx.out(kv('abandoned', `effort ${target} — ${items.length} artifacts walked, journal remembers the attempt`));
116
- for (const k of kept)
117
- ctx.out(kv('kept', keptLine(k)));
117
+ ctx.out(kv('abandoned', `effort ${target} — ${items.length} artifacts walked · journal remembers the attempt`));
118
+ abandonLines(items, kept).forEach((l) => ctx.out(l));
118
119
  return EXIT.OK;
119
120
  }
120
121
  renderRefusal([v('target', 'unknown-target', target, 'a plan id or an effort slug')]).forEach((l) => ctx.err(l));
@@ -9,7 +9,7 @@ import { dirtyStatePaths, primaryRoot, stateCommit } from '../gitio.js';
9
9
  import { appendEntry, entryLine, journalRel } from '../journal.js';
10
10
  import { acquireLock } from '../lock.js';
11
11
  import { renderRefusal, v } from '../refusal.js';
12
- import { idRetired } from '../departure.js';
12
+ import { idRetired, pathRetired } from '../departure.js';
13
13
  import { findById, findCycle, loadCanon } from '../scan.js';
14
14
  import { validateDoc } from '../schemas.js';
15
15
  import { canonicalSha, short } from '../sha.js';
@@ -47,6 +47,11 @@ export async function run(ctx, argv) {
47
47
  const taken = idRetired(root, id, String(meta.type));
48
48
  if (taken)
49
49
  violations.push(taken);
50
+ // D167. Nor land on a path another id was last witnessed at — the same check `write` runs on a
51
+ // create. A doc witnessed where it sits passes on its own id, so any amendment stays adoptable.
52
+ const retired = pathRetired(root, canon, rel, id);
53
+ if (retired)
54
+ violations.push(retired);
50
55
  for (const dep of Array.isArray(meta.depends) ? meta.depends : []) {
51
56
  if (!findById(canon, dep))
52
57
  violations.push(v('depends', 'unknown-dep', dep, 'an existing doc id'));
@@ -4,7 +4,7 @@ import { parseArgs } from 'node:util';
4
4
  import { EXIT } from '../cli.js';
5
5
  import { loadConfig } from '../config.js';
6
6
  import { deferralMoves } from '../deferral.js';
7
- import { canonExit, idRetired, prepareDeparture } from '../departure.js';
7
+ import { canonExit, idRetired, pathRetired, prepareDeparture } from '../departure.js';
8
8
  import { designPending, designStamp } from '../design.js';
9
9
  import { ID_RE } from '../dsl.js';
10
10
  import { serializeDoc, splitDoc, writeDoc } from '../fm.js';
@@ -129,8 +129,9 @@ export async function run(ctx, argv) {
129
129
  renderRefusal(viol).forEach(ctx.err);
130
130
  return EXIT.REFUSED;
131
131
  };
132
- // D166. A create may not take an id that already names something (`idRetired` — the rule
133
- // `rename` applies to its target, with the one re-use of a spec its abandon unwound).
132
+ // D166. A create may not take an id that already names something (`idRetired`), with the one
133
+ // re-use of a spec its abandon unwound; a stray stream holds no id. `rename` is stricter on its
134
+ // target — no stream at all — because it moves its own stream onto that id.
134
135
  if (existing === undefined) {
135
136
  const taken = idRetired(root, id, String(manifest.type));
136
137
  if (taken)
@@ -213,6 +214,12 @@ export async function run(ctx, argv) {
213
214
  ? `a new id — ${rel} holds ${String(holder.meta.id)}`
214
215
  : `a new id — ${rel} holds a doc canon cannot read (fix or revert it first)`)]);
215
216
  }
217
+ // D167. Nor on the path a superseded spec comes back to (`pathRetired`).
218
+ if (existing === undefined) {
219
+ const retired = pathRetired(root, canon, rel, id);
220
+ if (retired)
221
+ return refuseWrite([retired]);
222
+ }
216
223
  const stream = journalRel(effort);
217
224
  const unrelated = dirtyStatePaths(root).filter((p) => p !== rel && p !== stream);
218
225
  if (unrelated.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popovych.co/witness",
3
- "version": "0.14.7",
3
+ "version": "0.14.8",
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.7",
4
+ "version": "0.14.8",
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.7}"
13
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.8}"
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.7} 2>/dev/null || true
6
+ ${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.8} 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.7}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.8}"
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.7}"
13
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.8}"
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.7}"
19
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.8}"
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.7}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.8}"
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.7}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.8}"
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.7}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.14.8}"
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.