@popovych.co/witness 0.19.0 → 0.19.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/abandon.js +217 -73
- package/dist/departure.js +90 -46
- package/dist/reviewed.js +3 -1
- package/dist/verbs/abandon.js +20 -10
- 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/abandon.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { prepareDeparture, prepareReturns } from './departure.js';
|
|
3
|
+
import { canonExit, prepareDeparture, prepareReturns, takeBack } from './departure.js';
|
|
4
4
|
import { designRel } from './design.js';
|
|
5
5
|
import { splitDoc } from './fm.js';
|
|
6
6
|
import { canonicalSha } from './sha.js';
|
|
@@ -9,33 +9,85 @@ import { acquireLock } from './lock.js';
|
|
|
9
9
|
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
|
-
import { effortOf, effortWrites } from './reviewed.js';
|
|
13
12
|
import { humanCall, ok, refuse, v } from './refusal.js';
|
|
14
13
|
import { prepareStamp, writeStamp } from './stamp.js';
|
|
15
14
|
import { kv } from './toon.js';
|
|
16
15
|
import { removeWorktree, worktreePath } from './worktree.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
const isWrite = (e, id, sha) => {
|
|
17
|
+
const w = e;
|
|
18
|
+
return w.t === 'write' && w.artifact === id && (sha === undefined || w.sha === sha);
|
|
19
|
+
};
|
|
20
|
+
const isAncestor = (root, a, b) => tryGit(root, 'merge-base', '--is-ancestor', a, b).ok;
|
|
21
|
+
// D168. Each line of a stream as history stood at `at`, with the commit that appended it — which
|
|
22
|
+
// `git blame` names because the journal is append-only. `--no-ignore-revs-file`: a blame config
|
|
23
|
+
// (a dotfile's `blame.ignoreRevsFile`) re-attributes lines, or makes blame fail outright in a repo
|
|
24
|
+
// that lacks the file. A blank line is skipped, as `readStream` skips it.
|
|
25
|
+
function journalActs(root, stream, at = 'HEAD') {
|
|
26
|
+
const blame = tryGit(root, 'blame', '--line-porcelain', '--no-ignore-revs-file', at, '--', journalRel(stream));
|
|
27
|
+
if (!blame.ok)
|
|
28
|
+
return [];
|
|
29
|
+
const out = [];
|
|
30
|
+
let commit = '';
|
|
31
|
+
for (const line of blame.out.split('\n')) {
|
|
32
|
+
if (/^[0-9a-f]{40} \d+ \d+/.test(line)) {
|
|
33
|
+
commit = line.slice(0, 40);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (line.startsWith('\t') && line.trim() !== '')
|
|
37
|
+
out.push({ entry: JSON.parse(line.slice(1)), commit });
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
// D168. The act this content of a spec is, as history stood at `at`: the newest write of it, by
|
|
42
|
+
// whichever effort made it. Rows 29 and 84 resolved a pin by walking the spec's path for matching
|
|
43
|
+
// content, and content is not an act: a return or another abandon's restore brings the same content
|
|
44
|
+
// back under a later commit, whose parent was never the write's — so an amendment read as a create
|
|
45
|
+
// and was deleted, and a create restored another effort's abandoned amendment. And the same bytes
|
|
46
|
+
// written again later, by another effort, are that effort's: who may take them back is the
|
|
47
|
+
// caller's question, asked of this answer.
|
|
48
|
+
export function writeAct(root, id, sha, at = 'HEAD') {
|
|
27
49
|
let found;
|
|
28
|
-
for (const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
for (const effort of effortStreams(root)) {
|
|
51
|
+
if (!readStream(root, effort).some((e) => isWrite(e, id, sha)))
|
|
52
|
+
continue;
|
|
53
|
+
for (const { entry, commit } of journalActs(root, effort, at)) {
|
|
54
|
+
if (!isWrite(entry, id, sha))
|
|
55
|
+
continue;
|
|
56
|
+
if (found === undefined || isAncestor(root, found.commit, commit)) {
|
|
57
|
+
found = { effort, commit, created: entry.created === true };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
36
60
|
}
|
|
37
61
|
return found;
|
|
38
62
|
}
|
|
63
|
+
const shaAt = (root, rev, rel) => {
|
|
64
|
+
const blob = gitBlob(root, rev, rel);
|
|
65
|
+
const doc = blob === undefined ? undefined : splitDoc(blob.toString('utf8'));
|
|
66
|
+
return doc?.ok ? canonicalSha(doc.value.meta, doc.value.body) : undefined;
|
|
67
|
+
};
|
|
68
|
+
// D168 (review). Whether this effort's abandon left its write standing — kept, because a plan
|
|
69
|
+
// stood on it or on a sibling slice, or the walk stopped at it. That content is canon of record:
|
|
70
|
+
// no later walk goes past it, or an amendment on top of it took the keep with it.
|
|
71
|
+
function stoodAtAbandon(root, effort, rel, sha) {
|
|
72
|
+
const at = journalActs(root, effort).find(({ entry }) => entry.t === 'human-decision' && entry.decision === 'abandon-effort');
|
|
73
|
+
return at !== undefined && shaAt(root, at.commit, rel) === sha;
|
|
74
|
+
}
|
|
75
|
+
// This effort's newest write of an artifact (of this content, when `sha` is given).
|
|
76
|
+
export const effortWrite = (root, effort, id, sha) => journalActs(root, effort).filter(({ entry }) => isWrite(entry, id, sha)).at(-1);
|
|
77
|
+
// D168 (review). Whether an earlier abandon already took this effort's newest write of a spec
|
|
78
|
+
// back — `abandon <plan>` does, before the effort's own abandon: a status entry the abandon
|
|
79
|
+
// journaled on the spec after that write, which is not a return, made where the spec stood at
|
|
80
|
+
// the write's content. Then the effort's abandon has nothing of its own left there to take back.
|
|
81
|
+
export function takenBackAlready(root, effort, id, rel, sha) {
|
|
82
|
+
const write = effortWrite(root, effort, id, sha);
|
|
83
|
+
if (write === undefined)
|
|
84
|
+
return false;
|
|
85
|
+
return journalActs(root, id).some(({ entry, commit }) => {
|
|
86
|
+
const s = entry;
|
|
87
|
+
const takes = s.t === 'status' && s.cause === 'abandon' && !(s.from === 'superseded' && s.to !== 'removed');
|
|
88
|
+
return takes && commit !== write.commit && isAncestor(root, write.commit, commit) && shaAt(root, `${commit}^`, rel) === sha;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
39
91
|
export const keptLine = (k) => `${String(k.doc.meta.id)} — not unwound · still standing on it: ${k.standing.join(' ')}`;
|
|
40
92
|
// D167. The abandon says everything it did to canon, on both paths, as `kept:` already did: what
|
|
41
93
|
// it reverted, what came back with it, and what the walk took back on the way.
|
|
@@ -43,28 +95,32 @@ export function abandonLines(items, kept) {
|
|
|
43
95
|
return [
|
|
44
96
|
...items.flatMap((item) => [
|
|
45
97
|
...(item.revert ? [kv('reverted', `${String(item.revert.doc.meta.id)} (${item.revert.action}) — journal untouched`)] : []),
|
|
46
|
-
...(item.revert?.returns ?? []).
|
|
47
|
-
|
|
48
|
-
...r.unwound.map((u) => kv('unwound', `${u.artifact} — stays out of canon · an abandoned effort created it`)),
|
|
49
|
-
]),
|
|
98
|
+
...(item.revert?.returns ?? []).map((r) => kv('returned', `${r.entry.artifact} — back in canon as ${r.entry.to} · the re-slice into ${r.by} is taken back`)),
|
|
99
|
+
...(item.revert?.unwound ?? []).map((u) => kv('unwound', `${u.artifact} — stays out of canon · an abandoned effort created it`)),
|
|
50
100
|
...(item.kept ? [kv('kept', keptLine(item.kept))] : []),
|
|
101
|
+
...(item.note ? [kv('note', item.note)] : []),
|
|
51
102
|
]),
|
|
52
103
|
...kept.map((k) => kv('kept', keptLine(k))),
|
|
53
104
|
];
|
|
54
105
|
}
|
|
55
|
-
// D166 (review). The writes the way back takes back besides this abandon's own
|
|
56
|
-
//
|
|
57
|
-
// departed — and
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
106
|
+
// D166 (review). The efforts whose writes the way back takes back besides this abandon's own
|
|
107
|
+
// targets: every effort already abandoned — whose abandon skipped a spec a later re-slice had
|
|
108
|
+
// departed — and the effort being abandoned now (`effort`, absent on the plan path).
|
|
109
|
+
const goneEfforts = (root, effort) => effortStreams(root).filter((e) => e === effort || effortAbandoned(readStream(root, e)));
|
|
110
|
+
// Content whose act is a write of one of `efforts` never comes back into canon on its own —
|
|
111
|
+
// unless a plan outside the abandon set stands on it (the keep rule, which never takes shipped
|
|
112
|
+
// content back), or that effort's own abandon left it standing.
|
|
113
|
+
function takenBackBy(root, canon, abandonSet, efforts) {
|
|
114
|
+
return (id, sha, rel, at) => {
|
|
115
|
+
if (plansOn(canon, id, { exclude: abandonSet, pin: sha }).length > 0)
|
|
116
|
+
return undefined;
|
|
117
|
+
const act = writeAct(root, id, sha, at);
|
|
118
|
+
return act !== undefined && efforts.includes(act.effort) && !stoodAtAbandon(root, act.effort, rel, sha) ? act : undefined;
|
|
119
|
+
};
|
|
66
120
|
}
|
|
67
|
-
|
|
121
|
+
// `act`: the write of `pin` this abandon takes back — the effort's own on the effort path, the
|
|
122
|
+
// write the plan is paired with on the plan path. `effort`: the effort being abandoned, if any.
|
|
123
|
+
export function specRevertFor(root, canon, spec, pin, abandonSet, act, effort) {
|
|
68
124
|
const current = canonicalSha(spec.meta, spec.body);
|
|
69
125
|
if (current !== pin) {
|
|
70
126
|
return refuse([v('spec', 'stacked-amendment', `${String(spec.meta.id)} is at ${current.slice(0, 7)}, pin is ${pin.slice(0, 7)}`, 'abandon newest-first: a later amendment touched this spec')]);
|
|
@@ -80,20 +136,50 @@ export function specRevertFor(root, canon, spec, pin, abandonSet, effort) {
|
|
|
80
136
|
shipped: standing.some((p) => String(p.meta.status) === 'done'),
|
|
81
137
|
});
|
|
82
138
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
139
|
+
const id = String(spec.meta.id);
|
|
140
|
+
// D168. The write being taken back is one act of its effort, and so is every earlier write of
|
|
141
|
+
// this spec it made: a revise round re-writes, and taking back only the last write left the
|
|
142
|
+
// effort's own first draft in canon. The walk goes back past those and past every abandoned
|
|
143
|
+
// effort's write, and stops at content a plan stands on.
|
|
144
|
+
const gone = goneEfforts(root, effort);
|
|
145
|
+
const walking = [...new Set([...gone, act.effort])];
|
|
146
|
+
const back = takeBack(root, spec.rel, id, act, takenBackBy(root, canon, abandonSet, walking));
|
|
147
|
+
if (back.end === 'lost') {
|
|
148
|
+
return refuse([humanCall(v('spec', 'pin-unresolvable', `${id}: no content at ${spec.rel} before ${back.act.effort}'s write ${back.act.commit.slice(0, 7)}`, 'that write to read as an amendment of what stood on this path — the spec was moved by hand or history was cut · a human\'s call'))]);
|
|
86
149
|
}
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// that
|
|
91
|
-
|
|
150
|
+
// D168 (review). The walk stopping at an earlier round of the effort being abandoned, because
|
|
151
|
+
// another plan still building stands on it, is a keep of that effort's content: the same keep
|
|
152
|
+
// the first round alone would have been, and refused for the same reason — this abandon is the
|
|
153
|
+
// last act that could take it back. Once a plan shipped it, it stands for good.
|
|
154
|
+
if (effort !== undefined && back.end === 'content') {
|
|
155
|
+
const sha = canonicalSha(back.doc.meta, back.doc.body);
|
|
156
|
+
const on = plansOn(canon, id, { exclude: abandonSet, pin: sha });
|
|
157
|
+
const owner = on.length > 0 ? writeAct(root, id, sha, back.src) : undefined;
|
|
158
|
+
if (owner !== undefined && walking.includes(owner.effort) && !on.some((p) => String(p.meta.status) === 'done')) {
|
|
159
|
+
return ok({ doc: spec, action: 'keep', standing: on.map((p) => String(p.meta.id)), shipped: false });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// D166. The way back: a write being taken back may have departed a predecessor (a re-slice),
|
|
163
|
+
// which then returns with it. A re-slice can cut one spec into several slices, and taking
|
|
164
|
+
// back one while another still stands would leave the predecessor half-described, so that
|
|
165
|
+
// write is kept until its siblings go too — together, through the effort that made them.
|
|
166
|
+
// D168: every write the walk takes back, not only the last — the create that re-sliced, or
|
|
167
|
+
// an amendment that folded another spec in, may lie under a revision.
|
|
168
|
+
const ways = back.passed.map((w) => prepareReturns(root, canon.paths, w.commit, id, takenBackBy(root, canon, abandonSet, gone)));
|
|
169
|
+
const returns = ways.flatMap((w) => w.returns);
|
|
170
|
+
const unwound = ways.flatMap((w) => w.unwound);
|
|
92
171
|
// D166 (review). A sibling slice stands when this abandon leaves it in canon: it is outside the
|
|
93
172
|
// abandon set, or inside it but kept because a plan outside the set stands on it. It stands for
|
|
94
173
|
// good when a plan shipped it — and then so does this keep, since nothing can take the
|
|
95
174
|
// sibling back. Every spec the walk passes through counts: each was a re-slice being taken back.
|
|
96
|
-
|
|
175
|
+
// D188: on the plan path, also a slice whose own write departed nothing — another slice's
|
|
176
|
+
// write took the predecessor out. Its effort is still live, and its own abandon is the act that
|
|
177
|
+
// takes the split back together; deleting the slice now left the split half described. The
|
|
178
|
+
// effort path is that last act, and there a slice another effort cut cannot go back together
|
|
179
|
+
// with this one: holding it would leave two efforts' abandons each refusing for the other's.
|
|
180
|
+
const from = spec.meta.supersedes === undefined ? undefined : String(spec.meta.supersedes);
|
|
181
|
+
const slice = effort === undefined && back.end === 'create' && from !== undefined && canonExit(readStream(root, from))?.to === 'superseded' ? [from] : [];
|
|
182
|
+
const cut = new Set([...returns.map((r) => r.entry.artifact), ...unwound.map((u) => u.artifact), ...slice]);
|
|
97
183
|
const slices = canon.docs
|
|
98
184
|
.filter((d) => d.meta.type === 'spec' && cut.has(String(d.meta.supersedes)) && String(d.meta.id) !== String(spec.meta.id))
|
|
99
185
|
.flatMap((d) => {
|
|
@@ -105,8 +191,7 @@ export function specRevertFor(root, canon, spec, pin, abandonSet, effort) {
|
|
|
105
191
|
if (slices.length > 0) {
|
|
106
192
|
return ok({ doc: spec, action: 'keep', standing: slices.map((s) => s.id), shipped: slices.some((s) => s.shipped) });
|
|
107
193
|
}
|
|
108
|
-
|
|
109
|
-
if (!prior.ok) {
|
|
194
|
+
if (back.end === 'create') {
|
|
110
195
|
const dependents = canon.docs
|
|
111
196
|
.filter((d) => !abandonSet.has(String(d.meta.id)))
|
|
112
197
|
.filter((d) => (d.meta.depends ?? []).includes(String(spec.meta.id)))
|
|
@@ -117,20 +202,19 @@ export function specRevertFor(root, canon, spec, pin, abandonSet, effort) {
|
|
|
117
202
|
// D166. The unwind departs the spec through the same door the re-slice uses, so
|
|
118
203
|
// everything that leaves canon with a spec — its design included — leaves here too.
|
|
119
204
|
return ok({
|
|
120
|
-
doc: spec, action: 'delete', returns,
|
|
205
|
+
doc: spec, action: 'delete', returns, unwound,
|
|
121
206
|
departure: prepareDeparture(root, canon.paths, spec, {
|
|
122
|
-
v: 1, t: 'status', artifact:
|
|
207
|
+
v: 1, t: 'status', artifact: id, from: String(spec.meta.status), to: 'removed', cause: 'abandon',
|
|
123
208
|
}),
|
|
124
209
|
});
|
|
125
210
|
}
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
const priorDesign = gitBlob(root, `${commit}^`, designR);
|
|
211
|
+
const toStatus = back.end === 'content' ? String(back.doc.meta.status) : 'draft';
|
|
212
|
+
// D166. The look goes back with the content it was drawn for: the design as it stood where the
|
|
213
|
+
// walk stopped, byte for byte — or none, if there was none then.
|
|
214
|
+
const designR = designRel(canon.paths, id);
|
|
215
|
+
const priorDesign = gitBlob(root, back.src, designR);
|
|
132
216
|
const design = priorDesign !== undefined || existsSync(join(root, designR)) ? { rel: designR, prior: priorDesign } : undefined;
|
|
133
|
-
return ok({ doc: spec, action: 'restore', content:
|
|
217
|
+
return ok({ doc: spec, action: 'restore', content: back.blob.toString('utf8'), toStatus, design, returns, unwound });
|
|
134
218
|
}
|
|
135
219
|
// D167. A return lands on the path its spec left from, and `rename` moves an id but leaves its
|
|
136
220
|
// file, so another doc may hold that path — or another spec this abandon brings back may have
|
|
@@ -154,6 +238,54 @@ export function returnCollisions(root, canon, items) {
|
|
|
154
238
|
...[...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
239
|
];
|
|
156
240
|
}
|
|
241
|
+
// D188. What an abandon lands — a restore's older content, a return's departed bytes — comes
|
|
242
|
+
// back only beside what it depends on: a dependency in canon that this abandon does not take
|
|
243
|
+
// out, or one it brings back. `write` refuses a dangling `depends`, and an abandon landing one
|
|
244
|
+
// left `check` red after it exited 0. Bringing the dependency back first is a human's call.
|
|
245
|
+
export function landingEdges(root, canon, items) {
|
|
246
|
+
const leaving = new Set(items.flatMap((i) => (i.revert?.action === 'delete' ? [String(i.revert.doc.meta.id)] : [])));
|
|
247
|
+
const coming = new Set(items.flatMap((i) => (i.revert?.returns ?? []).map((r) => r.entry.artifact)));
|
|
248
|
+
const lands = items.flatMap((i) => {
|
|
249
|
+
if (!i.revert)
|
|
250
|
+
return [];
|
|
251
|
+
const restored = i.revert.action === 'restore' ? splitDoc(i.revert.content) : undefined;
|
|
252
|
+
return [
|
|
253
|
+
...(restored?.ok ? [{ id: String(i.revert.doc.meta.id), depends: (restored.value.meta.depends ?? []).map(String) }] : []),
|
|
254
|
+
...i.revert.returns.map((r) => ({ id: r.entry.artifact, depends: r.depends })),
|
|
255
|
+
];
|
|
256
|
+
});
|
|
257
|
+
return lands.flatMap(({ id, depends }) => depends
|
|
258
|
+
.filter((dep) => !coming.has(dep) && (leaving.has(dep) || findById(canon, dep) === undefined))
|
|
259
|
+
.map((dep) => {
|
|
260
|
+
const exit = canonExit(readStream(root, dep));
|
|
261
|
+
const why = leaving.has(dep) ? 'it leaves with this abandon'
|
|
262
|
+
: exit?.to === 'superseded' ? `it left superseded by ${exit.by ?? 'a successor'}` : 'it left canon';
|
|
263
|
+
return humanCall(v('spec', 'dangling-depends', `${id} depends on ${dep}`, `${dep} in canon when ${id} comes back — ${why} · a human's call`));
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
// D188 (review). A spec kept for good stays in canon with what it depends on: a dependency this
|
|
267
|
+
// abandon would delete is kept beside it, and for good with it. `waiting-dependents` reads only
|
|
268
|
+
// docs outside the abandon set, and a kept one is inside it, so the delete left `check` red after
|
|
269
|
+
// the abandon exited 0; refusing instead would hold the effort for good behind a shipped plan.
|
|
270
|
+
export function keepDependencies(items, kept) {
|
|
271
|
+
let walked = items;
|
|
272
|
+
const held = [...kept];
|
|
273
|
+
for (let grew = true; grew;) {
|
|
274
|
+
grew = false;
|
|
275
|
+
walked = walked.filter((item) => {
|
|
276
|
+
if (item.revert?.action !== 'delete')
|
|
277
|
+
return true;
|
|
278
|
+
const id = String(item.revert.doc.meta.id);
|
|
279
|
+
const on = held.filter((k) => (k.doc.meta.depends ?? []).map(String).includes(id));
|
|
280
|
+
if (on.length === 0)
|
|
281
|
+
return true;
|
|
282
|
+
held.push({ doc: item.revert.doc, action: 'keep', standing: on.map((k) => String(k.doc.meta.id)), shipped: on.some((k) => k.shipped) });
|
|
283
|
+
grew = true;
|
|
284
|
+
return false;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return { items: walked, kept: held };
|
|
288
|
+
}
|
|
157
289
|
export function executeAbandon(root, ctx, items, effortEntry, subject) {
|
|
158
290
|
const files = new Set();
|
|
159
291
|
const journalMulti = [];
|
|
@@ -180,20 +312,21 @@ export function executeAbandon(root, ctx, items, effortEntry, subject) {
|
|
|
180
312
|
for (const f of touched)
|
|
181
313
|
files.add(f);
|
|
182
314
|
journalMulti.push({ stream: id, line: entryLine(entry) });
|
|
183
|
-
// D166. A spec this write had departed comes back in the same state commit
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
315
|
+
// D166. A spec this write had departed comes back in the same state commit — and, D168,
|
|
316
|
+
// a spec the walk took back past its create leaves canon's record in it too.
|
|
317
|
+
const asLine = (e) => ({ stream: e.artifact, entry: e });
|
|
318
|
+
const lines = [
|
|
319
|
+
...item.revert.returns.flatMap((r) => [asLine(r.entry), ...r.moves]),
|
|
320
|
+
...item.revert.unwound.map(asLine),
|
|
321
|
+
];
|
|
322
|
+
for (const r of item.revert.returns)
|
|
189
323
|
for (const f of r.files)
|
|
190
324
|
files.add(f);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
returnEntries.push(...lines);
|
|
325
|
+
for (const l of lines) {
|
|
326
|
+
files.add(journalRel(l.stream));
|
|
327
|
+
journalMulti.push({ stream: l.stream, line: entryLine(l.entry) });
|
|
196
328
|
}
|
|
329
|
+
returnEntries.push(...lines);
|
|
197
330
|
}
|
|
198
331
|
}
|
|
199
332
|
let effortLine;
|
|
@@ -270,13 +403,18 @@ export function planItems(root, canon, plan, abandonSet, pairing) {
|
|
|
270
403
|
};
|
|
271
404
|
if (pairing === 'stamp-only')
|
|
272
405
|
return ok(item);
|
|
273
|
-
|
|
406
|
+
// D168. Paired by the act: an effort that wrote this plan also made the write of the parent the
|
|
407
|
+
// plan derives from. Deriving the owner through effortOf tie-broke alphabetically across every
|
|
408
|
+
// effort that wrote the plan, and pairing on "wrote the parent at all" reverted content another
|
|
409
|
+
// effort wrote whenever the plan was derived from it.
|
|
274
410
|
const parentId = String(plan.meta.parent);
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
411
|
+
const parent = findById(canon, parentId);
|
|
412
|
+
if (parent) {
|
|
413
|
+
const pin = String(plan.meta['derives-from']);
|
|
414
|
+
const writers = effortStreams(root).filter((e) => readStream(root, e).some((x) => isWrite(x, planId)));
|
|
415
|
+
const act = writeAct(root, parentId, pin);
|
|
416
|
+
if (act !== undefined && writers.includes(act.effort)) {
|
|
417
|
+
const revertR = specRevertFor(root, canon, parent, pin, abandonSet, act);
|
|
280
418
|
if (!revertR.ok)
|
|
281
419
|
return revertR;
|
|
282
420
|
const outcome = revertR.value;
|
|
@@ -285,6 +423,12 @@ export function planItems(root, canon, plan, abandonSet, pairing) {
|
|
|
285
423
|
else
|
|
286
424
|
item.revert = outcome;
|
|
287
425
|
}
|
|
426
|
+
else if (act === undefined && writers.some((e) => readStream(root, e).some((x) => isWrite(x, parentId)))) {
|
|
427
|
+
// The plan's effort wrote this spec, but no journaled write made the content the plan
|
|
428
|
+
// derives from (a hand edit, an adopt): there is no act to pair with, and saying so is
|
|
429
|
+
// what keeps a spec left standing from looking unwound.
|
|
430
|
+
item.note = `${parentId} — not unwound · no journaled write made the content ${planId} derives from`;
|
|
431
|
+
}
|
|
288
432
|
}
|
|
289
433
|
return ok(item);
|
|
290
434
|
}
|
package/dist/departure.js
CHANGED
|
@@ -47,6 +47,55 @@ export function prepareDeparture(root, paths, spec, entry) {
|
|
|
47
47
|
unlinkSync(join(root, rel)); },
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
// The content a spec stood at when this write was made: none, when the entry says the write
|
|
51
|
+
// created it; lost, when the entry says amendment but the path held nothing then — moved by
|
|
52
|
+
// hand, or history cut — which is never read as a create.
|
|
53
|
+
function before(root, rel, act) {
|
|
54
|
+
if (act.created)
|
|
55
|
+
return 'create';
|
|
56
|
+
const src = `${act.commit}^`;
|
|
57
|
+
const blob = gitBlob(root, src, rel);
|
|
58
|
+
return blob === undefined ? 'lost' : { src, blob };
|
|
59
|
+
}
|
|
60
|
+
// D168. From content at `src`, back past every write `takenBack` names, each found by its own
|
|
61
|
+
// journal entry: to the content before the last of them, or to a create, past which the spec
|
|
62
|
+
// has no content to stand at. `passed` is every write taken back on the way, oldest last.
|
|
63
|
+
export function walkBack(root, rel, id, at, takenBack, seen) {
|
|
64
|
+
let { src, blob, doc } = at;
|
|
65
|
+
const passed = [];
|
|
66
|
+
for (;;) {
|
|
67
|
+
const back = takenBack(id, canonicalSha(doc.meta, doc.body), rel, src);
|
|
68
|
+
if (back === undefined || seen.has(back.commit))
|
|
69
|
+
return { end: 'content', src, blob, doc, passed };
|
|
70
|
+
seen.add(back.commit);
|
|
71
|
+
const prior = before(root, rel, back);
|
|
72
|
+
if (prior === 'create')
|
|
73
|
+
return { end: 'create', act: back, passed: [...passed, back] };
|
|
74
|
+
if (prior === 'lost')
|
|
75
|
+
return { end: 'lost', act: back, passed, at: { src, blob, doc } };
|
|
76
|
+
const parsed = splitDoc(prior.blob.toString('utf8'));
|
|
77
|
+
if (!parsed.ok)
|
|
78
|
+
return { end: 'content', src, blob, doc, passed }; // kept: nothing readable before it
|
|
79
|
+
passed.push(back);
|
|
80
|
+
({ src, blob } = prior);
|
|
81
|
+
doc = parsed.value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// D168. Taking back one write: the content before it, walked back past every write `takenBack`
|
|
85
|
+
// names — or the create, when the write or a write before it created the spec. An unreadable
|
|
86
|
+
// prior is restored as it stands, which is all the walk could do with it.
|
|
87
|
+
export function takeBack(root, rel, id, act, takenBack) {
|
|
88
|
+
const prior = before(root, rel, act);
|
|
89
|
+
if (prior === 'create')
|
|
90
|
+
return { end: 'create', act, passed: [act] };
|
|
91
|
+
if (prior === 'lost')
|
|
92
|
+
return { end: 'lost', act, passed: [] };
|
|
93
|
+
const parsed = splitDoc(prior.blob.toString('utf8'));
|
|
94
|
+
if (!parsed.ok)
|
|
95
|
+
return { end: 'unreadable', ...prior, passed: [act] };
|
|
96
|
+
const walked = walkBack(root, rel, id, { ...prior, doc: parsed.value }, takenBack, new Set([act.commit]));
|
|
97
|
+
return { ...walked, passed: [act, ...walked.passed] };
|
|
98
|
+
}
|
|
50
99
|
// The way back. Row 29 unwinds a write by selectively reverting its state commit, and a
|
|
51
100
|
// re-slice's commit did two things: it wrote the successor and departed its predecessor.
|
|
52
101
|
// Taking that write back therefore brings the predecessor back — its file and design as they
|
|
@@ -62,70 +111,65 @@ export function prepareDeparture(root, paths, spec, entry) {
|
|
|
62
111
|
// amendment, or, past a create, to whatever that create had departed in its turn — so canon
|
|
63
112
|
// ends where it stood before every abandoned write, whatever order the efforts went in.
|
|
64
113
|
export function prepareReturns(root, paths, commit, successor, takenBack = () => undefined) {
|
|
65
|
-
return returnsThrough(root, paths, commit, [successor],
|
|
114
|
+
return returnsThrough(root, paths, commit, [successor], takenBack, new Set([commit]));
|
|
66
115
|
}
|
|
67
|
-
function returnsThrough(root, paths, commit, chain,
|
|
116
|
+
function returnsThrough(root, paths, commit, chain, takenBack, seen) {
|
|
68
117
|
const successor = chain.at(-1);
|
|
69
118
|
const shown = tryGit(root, 'show', '--no-renames', '--name-status', '--format=', commit);
|
|
70
119
|
if (!shown.ok)
|
|
71
|
-
return [];
|
|
120
|
+
return { returns: [], unwound: [] };
|
|
72
121
|
const deleted = shown.out.split('\n').filter((l) => l.startsWith('D\t')).map((l) => l.slice(2));
|
|
73
|
-
|
|
122
|
+
const ways = deleted.map((rel) => {
|
|
123
|
+
const none = { returns: [], unwound: [] };
|
|
74
124
|
if (!rel.startsWith(`${paths.specs}/`) || !rel.endsWith('.md'))
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
|
|
125
|
+
return none;
|
|
126
|
+
const src = `${commit}^`;
|
|
127
|
+
const blob = gitBlob(root, src, rel);
|
|
78
128
|
if (blob === undefined)
|
|
79
|
-
return
|
|
80
|
-
|
|
129
|
+
return none;
|
|
130
|
+
const parsed = splitDoc(blob.toString('utf8'));
|
|
81
131
|
if (!parsed.ok)
|
|
82
|
-
return
|
|
132
|
+
return none;
|
|
83
133
|
const id = String(parsed.value.meta.id);
|
|
84
134
|
const exit = canonExit(readStream(root, id));
|
|
85
135
|
if (exit?.to !== 'superseded' || exit.by !== successor)
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const gone = { v: 1, t: 'status', artifact: id, from: 'superseded', to: 'removed', cause: 'abandon' };
|
|
97
|
-
return returnsThrough(root, paths, back, [...chain, id], [...unwound, gone], takenBack, seen);
|
|
98
|
-
}
|
|
99
|
-
const prior = splitDoc(before.toString('utf8'));
|
|
100
|
-
if (!prior.ok)
|
|
101
|
-
break;
|
|
102
|
-
src = `${back}^`;
|
|
103
|
-
blob = before;
|
|
104
|
-
parsed = prior;
|
|
105
|
-
status = String(prior.value.meta.status);
|
|
136
|
+
return none;
|
|
137
|
+
const reached = walkBack(root, rel, id, { src, blob, doc: parsed.value }, takenBack, seen);
|
|
138
|
+
// A write the walk cannot go past leaves the return where it stood before that write.
|
|
139
|
+
const walked = reached.end === 'lost' ? { ...reached, ...reached.at, end: 'content' } : reached;
|
|
140
|
+
if (walked.end === 'create') {
|
|
141
|
+
// That write created it: it stays out — journaled so whether or not anything lies under
|
|
142
|
+
// the create — and what it had departed comes back instead.
|
|
143
|
+
const gone = { v: 1, t: 'status', artifact: id, from: 'superseded', to: 'removed', cause: 'abandon' };
|
|
144
|
+
const under = returnsThrough(root, paths, walked.act.commit, [...chain, id], takenBack, seen);
|
|
145
|
+
return { returns: under.returns, unwound: [gone, ...under.unwound] };
|
|
106
146
|
}
|
|
107
|
-
const
|
|
147
|
+
const status = walked.passed.length > 0 ? String(walked.doc.meta.status) : exit.from;
|
|
108
148
|
const design = designRel(paths, id);
|
|
109
|
-
const designBlob = deleted.includes(design) ? gitBlob(root, src, design) : undefined;
|
|
149
|
+
const designBlob = deleted.includes(design) ? gitBlob(root, walked.src, design) : undefined;
|
|
110
150
|
const put = (r, b) => {
|
|
111
151
|
mkdirSync(dirname(join(root, r)), { recursive: true }); // git keeps no empty directory: a fresh clone may lack it
|
|
112
152
|
writeFileSync(join(root, r), b);
|
|
113
153
|
};
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
154
|
+
return {
|
|
155
|
+
returns: [{
|
|
156
|
+
entry: { v: 1, t: 'status', artifact: id, from: 'superseded', to: status, cause: 'abandon' },
|
|
157
|
+
files: [rel, ...(designBlob === undefined ? [] : [design]), journalRel(id)],
|
|
158
|
+
restore: () => {
|
|
159
|
+
put(rel, walked.blob);
|
|
160
|
+
if (designBlob !== undefined)
|
|
161
|
+
put(design, designBlob);
|
|
162
|
+
},
|
|
163
|
+
// Each link of the chain hands home the debts that came to it from the next one down.
|
|
164
|
+
moves: chain.flatMap((s, k) => deferralMoves(readStream(root, s), s, id, (d) => d.moved_from === (chain[k + 1] ?? id))),
|
|
165
|
+
rel,
|
|
166
|
+
by: successor,
|
|
167
|
+
depends: (walked.doc.meta.depends ?? []).map(String),
|
|
168
|
+
}],
|
|
169
|
+
unwound: [],
|
|
170
|
+
};
|
|
128
171
|
});
|
|
172
|
+
return { returns: ways.flatMap((w) => w.returns), unwound: ways.flatMap((w) => w.unwound) };
|
|
129
173
|
}
|
|
130
174
|
// Whether a NEW artifact may take this id. An id names one artifact for the life of the repo,
|
|
131
175
|
// because its stream IS that artifact's history. Three streams already belong to something
|
package/dist/reviewed.js
CHANGED
|
@@ -75,7 +75,9 @@ export function effortWrites(root, effort) {
|
|
|
75
75
|
if (e.t !== 'write')
|
|
76
76
|
continue;
|
|
77
77
|
const w = e;
|
|
78
|
-
|
|
78
|
+
// D168. Latest write wins for sha and covers, but a revision does not un-create: the effort
|
|
79
|
+
// created the artifact if any of its writes did.
|
|
80
|
+
out.set(w.artifact, { sha: w.sha, covers: w.covers, created: w.created ?? out.get(w.artifact)?.created });
|
|
79
81
|
}
|
|
80
82
|
return out;
|
|
81
83
|
}
|
package/dist/verbs/abandon.js
CHANGED
|
@@ -5,9 +5,9 @@ import { findById, loadCanon } from '../scan.js';
|
|
|
5
5
|
import { effortAbandoned, latestRecap, readStream, streamExists } from '../journal.js';
|
|
6
6
|
import { effortWrites } from '../reviewed.js';
|
|
7
7
|
import { canonicalSha } from '../sha.js';
|
|
8
|
-
import { renderRefusal, v } from '../refusal.js';
|
|
8
|
+
import { humanCall, renderRefusal, v } from '../refusal.js';
|
|
9
9
|
import { kv } from '../toon.js';
|
|
10
|
-
import { abandonLines, executeAbandon, planItems, returnCollisions, specRevertFor } from '../abandon.js';
|
|
10
|
+
import { abandonLines, executeAbandon, keepDependencies, landingEdges, planItems, returnCollisions, specRevertFor, takenBackAlready, writeAct } 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,7 +35,7 @@ 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]);
|
|
38
|
+
const taken = [...returnCollisions(root, canon, [itemR.value]), ...landingEdges(root, canon, [itemR.value])];
|
|
39
39
|
if (taken.length > 0) {
|
|
40
40
|
renderRefusal(taken).forEach((l) => ctx.err(l));
|
|
41
41
|
return EXIT.REFUSED;
|
|
@@ -80,11 +80,20 @@ export async function run(ctx, argv) {
|
|
|
80
80
|
const artifact = findById(canon, id);
|
|
81
81
|
if (!artifact || artifact.meta.type !== 'spec')
|
|
82
82
|
continue;
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
// D168. The effort's write is what this abandon takes back only while it is the act the
|
|
84
|
+
// spec's content is — the same bytes written again later are the later effort's. A write
|
|
85
|
+
// an earlier abandon already took back (`abandon <plan>`) leaves nothing here to take.
|
|
86
|
+
const current = canonicalSha(artifact.meta, artifact.body);
|
|
87
|
+
const act = writeAct(root, id, current);
|
|
88
|
+
if (current !== w.sha || act?.effort !== target) {
|
|
89
|
+
if (takenBackAlready(root, target, id, artifact.rel, w.sha))
|
|
90
|
+
continue;
|
|
91
|
+
problems.push(current === w.sha && act === undefined
|
|
92
|
+
? humanCall(v('spec', 'pin-unresolvable', id, `${target}'s journaled write of this content — the journal's history is unreadable here · a human's call`))
|
|
93
|
+
: v('spec', 'stacked-amendment', id, 'the effort\'s write is no longer the newest'));
|
|
85
94
|
continue;
|
|
86
95
|
}
|
|
87
|
-
const revertR = specRevertFor(root, canon, artifact, w.sha, abandonSet, target);
|
|
96
|
+
const revertR = specRevertFor(root, canon, artifact, w.sha, abandonSet, act, target);
|
|
88
97
|
if (!revertR.ok) {
|
|
89
98
|
problems.push(...revertR.violations);
|
|
90
99
|
continue;
|
|
@@ -104,18 +113,19 @@ export async function run(ctx, argv) {
|
|
|
104
113
|
else if (outcome)
|
|
105
114
|
items.push({ revert: outcome });
|
|
106
115
|
}
|
|
107
|
-
|
|
116
|
+
const held = keepDependencies(items, kept);
|
|
117
|
+
problems.push(...returnCollisions(root, canon, held.items), ...landingEdges(root, canon, held.items));
|
|
108
118
|
if (problems.length > 0) {
|
|
109
119
|
renderRefusal(problems).forEach((l) => ctx.err(l));
|
|
110
120
|
return EXIT.REFUSED;
|
|
111
121
|
}
|
|
112
|
-
const done = executeAbandon(root, ctx, items, { effort: target }, `abandon(${target})`);
|
|
122
|
+
const done = executeAbandon(root, ctx, held.items, { effort: target }, `abandon(${target})`);
|
|
113
123
|
if (!done.ok) {
|
|
114
124
|
renderRefusal(done.violations).forEach((l) => ctx.err(l));
|
|
115
125
|
return EXIT.REFUSED;
|
|
116
126
|
}
|
|
117
|
-
ctx.out(kv('abandoned', `effort ${target} — ${items.length} artifacts walked · journal remembers the attempt`));
|
|
118
|
-
abandonLines(items, kept).forEach((l) => ctx.out(l));
|
|
127
|
+
ctx.out(kv('abandoned', `effort ${target} — ${held.items.length} artifacts walked · journal remembers the attempt`));
|
|
128
|
+
abandonLines(held.items, held.kept).forEach((l) => ctx.out(l));
|
|
119
129
|
return EXIT.OK;
|
|
120
130
|
}
|
|
121
131
|
renderRefusal([v('target', 'unknown-target', target, 'a plan id or an effort slug')]).forEach((l) => ctx.err(l));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popovych.co/witness",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.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.19.
|
|
4
|
+
"version": "0.19.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Kostiantyn Popovych"
|
|
7
7
|
},
|
|
@@ -12,7 +12,7 @@ Any other words in `$ARGUMENTS` are the human's ask: run `$WITNESS ask "<those w
|
|
|
12
12
|
Resolve the CLI once:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.
|
|
15
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.2}"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
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.19.
|
|
6
|
+
${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.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.19.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.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.19.
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.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.19.
|
|
19
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.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.19.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.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.19.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.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.19.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.19.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.
|