@popovych.co/witness 0.19.1 → 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 +60 -3
- package/dist/departure.js +1 -0
- package/dist/verbs/abandon.js +7 -6
- 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, takeBack } 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';
|
|
@@ -72,12 +72,14 @@ function stoodAtAbandon(root, effort, rel, sha) {
|
|
|
72
72
|
const at = journalActs(root, effort).find(({ entry }) => entry.t === 'human-decision' && entry.decision === 'abandon-effort');
|
|
73
73
|
return at !== undefined && shaAt(root, at.commit, rel) === sha;
|
|
74
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);
|
|
75
77
|
// D168 (review). Whether an earlier abandon already took this effort's newest write of a spec
|
|
76
78
|
// back — `abandon <plan>` does, before the effort's own abandon: a status entry the abandon
|
|
77
79
|
// journaled on the spec after that write, which is not a return, made where the spec stood at
|
|
78
80
|
// the write's content. Then the effort's abandon has nothing of its own left there to take back.
|
|
79
81
|
export function takenBackAlready(root, effort, id, rel, sha) {
|
|
80
|
-
const write =
|
|
82
|
+
const write = effortWrite(root, effort, id, sha);
|
|
81
83
|
if (write === undefined)
|
|
82
84
|
return false;
|
|
83
85
|
return journalActs(root, id).some(({ entry, commit }) => {
|
|
@@ -170,7 +172,14 @@ export function specRevertFor(root, canon, spec, pin, abandonSet, act, effort) {
|
|
|
170
172
|
// abandon set, or inside it but kept because a plan outside the set stands on it. It stands for
|
|
171
173
|
// good when a plan shipped it — and then so does this keep, since nothing can take the
|
|
172
174
|
// sibling back. Every spec the walk passes through counts: each was a re-slice being taken back.
|
|
173
|
-
|
|
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]);
|
|
174
183
|
const slices = canon.docs
|
|
175
184
|
.filter((d) => d.meta.type === 'spec' && cut.has(String(d.meta.supersedes)) && String(d.meta.id) !== String(spec.meta.id))
|
|
176
185
|
.flatMap((d) => {
|
|
@@ -229,6 +238,54 @@ export function returnCollisions(root, canon, items) {
|
|
|
229
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`))),
|
|
230
239
|
];
|
|
231
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
|
+
}
|
|
232
289
|
export function executeAbandon(root, ctx, items, effortEntry, subject) {
|
|
233
290
|
const files = new Set();
|
|
234
291
|
const journalMulti = [];
|
package/dist/departure.js
CHANGED
|
@@ -164,6 +164,7 @@ function returnsThrough(root, paths, commit, chain, takenBack, seen) {
|
|
|
164
164
|
moves: chain.flatMap((s, k) => deferralMoves(readStream(root, s), s, id, (d) => d.moved_from === (chain[k + 1] ?? id))),
|
|
165
165
|
rel,
|
|
166
166
|
by: successor,
|
|
167
|
+
depends: (walked.doc.meta.depends ?? []).map(String),
|
|
167
168
|
}],
|
|
168
169
|
unwound: [],
|
|
169
170
|
};
|
package/dist/verbs/abandon.js
CHANGED
|
@@ -7,7 +7,7 @@ import { effortWrites } from '../reviewed.js';
|
|
|
7
7
|
import { canonicalSha } from '../sha.js';
|
|
8
8
|
import { humanCall, renderRefusal, v } from '../refusal.js';
|
|
9
9
|
import { kv } from '../toon.js';
|
|
10
|
-
import { abandonLines, executeAbandon, planItems, returnCollisions, specRevertFor, takenBackAlready, writeAct } 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;
|
|
@@ -113,18 +113,19 @@ export async function run(ctx, argv) {
|
|
|
113
113
|
else if (outcome)
|
|
114
114
|
items.push({ revert: outcome });
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
const held = keepDependencies(items, kept);
|
|
117
|
+
problems.push(...returnCollisions(root, canon, held.items), ...landingEdges(root, canon, held.items));
|
|
117
118
|
if (problems.length > 0) {
|
|
118
119
|
renderRefusal(problems).forEach((l) => ctx.err(l));
|
|
119
120
|
return EXIT.REFUSED;
|
|
120
121
|
}
|
|
121
|
-
const done = executeAbandon(root, ctx, items, { effort: target }, `abandon(${target})`);
|
|
122
|
+
const done = executeAbandon(root, ctx, held.items, { effort: target }, `abandon(${target})`);
|
|
122
123
|
if (!done.ok) {
|
|
123
124
|
renderRefusal(done.violations).forEach((l) => ctx.err(l));
|
|
124
125
|
return EXIT.REFUSED;
|
|
125
126
|
}
|
|
126
|
-
ctx.out(kv('abandoned', `effort ${target} — ${items.length} artifacts walked · journal remembers the attempt`));
|
|
127
|
-
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));
|
|
128
129
|
return EXIT.OK;
|
|
129
130
|
}
|
|
130
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.
|