@popovych.co/witness 0.19.2 → 0.19.3
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 +17 -27
- package/dist/gates/decompose.js +2 -1
- package/dist/journal.js +23 -0
- package/dist/verbs/abandon.js +2 -0
- package/dist/verbs/log.js +3 -14
- 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
|
@@ -7,7 +7,7 @@ import { canonicalSha } from './sha.js';
|
|
|
7
7
|
import { gitBlob, tryGit, stateCommit } from './gitio.js';
|
|
8
8
|
import { acquireLock } from './lock.js';
|
|
9
9
|
import { crashPoint, withTxn } from './txn.js';
|
|
10
|
-
import { appendEntry, effortAbandoned, effortStreams, entryLine, journalRel, readStream } from './journal.js';
|
|
10
|
+
import { appendEntry, effortAbandoned, effortStreams, entryLine, journalActs, journalRel, readStream } from './journal.js';
|
|
11
11
|
import { findById, plansOn } from './scan.js';
|
|
12
12
|
import { humanCall, ok, refuse, v } from './refusal.js';
|
|
13
13
|
import { prepareStamp, writeStamp } from './stamp.js';
|
|
@@ -18,25 +18,15 @@ const isWrite = (e, id, sha) => {
|
|
|
18
18
|
return w.t === 'write' && w.artifact === id && (sha === undefined || w.sha === sha);
|
|
19
19
|
};
|
|
20
20
|
const isAncestor = (root, a, b) => tryGit(root, 'merge-base', '--is-ancestor', a, b).ok;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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;
|
|
21
|
+
// D189. Of two commits, the later: by ancestry, and on parallel branches — where neither is an
|
|
22
|
+
// ancestor, as after a merge of two clones' journals — by commit time.
|
|
23
|
+
function later(root, a, b) {
|
|
24
|
+
if (isAncestor(root, a, b))
|
|
25
|
+
return b;
|
|
26
|
+
if (isAncestor(root, b, a))
|
|
27
|
+
return a;
|
|
28
|
+
const time = (c) => Number(tryGit(root, 'log', '-1', '--format=%ct', c).out) || 0;
|
|
29
|
+
return time(b) > time(a) ? b : a;
|
|
40
30
|
}
|
|
41
31
|
// D168. The act this content of a spec is, as history stood at `at`: the newest write of it, by
|
|
42
32
|
// whichever effort made it. Rows 29 and 84 resolved a pin by walking the spec's path for matching
|
|
@@ -53,7 +43,7 @@ export function writeAct(root, id, sha, at = 'HEAD') {
|
|
|
53
43
|
for (const { entry, commit } of journalActs(root, effort, at)) {
|
|
54
44
|
if (!isWrite(entry, id, sha))
|
|
55
45
|
continue;
|
|
56
|
-
if (found === undefined ||
|
|
46
|
+
if (found === undefined || later(root, found.commit, commit) === commit) {
|
|
57
47
|
found = { effort, commit, created: entry.created === true };
|
|
58
48
|
}
|
|
59
49
|
}
|
|
@@ -69,11 +59,11 @@ const shaAt = (root, rev, rel) => {
|
|
|
69
59
|
// stood on it or on a sibling slice, or the walk stopped at it. That content is canon of record:
|
|
70
60
|
// no later walk goes past it, or an amendment on top of it took the keep with it.
|
|
71
61
|
function stoodAtAbandon(root, effort, rel, sha) {
|
|
72
|
-
const at = journalActs(root, effort).find(({ entry }) => entry.t === 'human-decision' && entry.decision === 'abandon-effort');
|
|
62
|
+
const at = journalActs(root, effort, 'HEAD').find(({ entry }) => entry.t === 'human-decision' && entry.decision === 'abandon-effort');
|
|
73
63
|
return at !== undefined && shaAt(root, at.commit, rel) === sha;
|
|
74
64
|
}
|
|
75
65
|
// 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);
|
|
66
|
+
export const effortWrite = (root, effort, id, sha) => journalActs(root, effort, 'HEAD').filter(({ entry }) => isWrite(entry, id, sha)).at(-1);
|
|
77
67
|
// D168 (review). Whether an earlier abandon already took this effort's newest write of a spec
|
|
78
68
|
// back — `abandon <plan>` does, before the effort's own abandon: a status entry the abandon
|
|
79
69
|
// journaled on the spec after that write, which is not a return, made where the spec stood at
|
|
@@ -82,7 +72,7 @@ export function takenBackAlready(root, effort, id, rel, sha) {
|
|
|
82
72
|
const write = effortWrite(root, effort, id, sha);
|
|
83
73
|
if (write === undefined)
|
|
84
74
|
return false;
|
|
85
|
-
return journalActs(root, id).some(({ entry, commit }) => {
|
|
75
|
+
return journalActs(root, id, 'HEAD').some(({ entry, commit }) => {
|
|
86
76
|
const s = entry;
|
|
87
77
|
const takes = s.t === 'status' && s.cause === 'abandon' && !(s.from === 'superseded' && s.to !== 'removed');
|
|
88
78
|
return takes && commit !== write.commit && isAncestor(root, write.commit, commit) && shaAt(root, `${commit}^`, rel) === sha;
|
|
@@ -177,11 +167,11 @@ export function specRevertFor(root, canon, spec, pin, abandonSet, act, effort) {
|
|
|
177
167
|
// takes the split back together; deleting the slice now left the split half described. The
|
|
178
168
|
// effort path is that last act, and there a slice another effort cut cannot go back together
|
|
179
169
|
// with this one: holding it would leave two efforts' abandons each refusing for the other's.
|
|
180
|
-
const
|
|
181
|
-
const slice = effort === undefined && back.end === 'create' &&
|
|
170
|
+
const own = spec.meta.supersedes === undefined ? undefined : String(spec.meta.supersedes);
|
|
171
|
+
const slice = effort === undefined && back.end === 'create' && own !== undefined && canonExit(readStream(root, own))?.to === 'superseded' ? [own] : [];
|
|
182
172
|
const cut = new Set([...returns.map((r) => r.entry.artifact), ...unwound.map((u) => u.artifact), ...slice]);
|
|
183
173
|
const slices = canon.docs
|
|
184
|
-
.filter((d) => d.meta.type === 'spec' && cut.has(String(d.meta.supersedes)) && String(d.meta.id) !==
|
|
174
|
+
.filter((d) => d.meta.type === 'spec' && cut.has(String(d.meta.supersedes)) && String(d.meta.id) !== id)
|
|
185
175
|
.flatMap((d) => {
|
|
186
176
|
const id = String(d.meta.id);
|
|
187
177
|
const leaving = abandonSet.has(id);
|
package/dist/gates/decompose.js
CHANGED
|
@@ -59,7 +59,8 @@ registerGate({
|
|
|
59
59
|
detail: staleChildren.length
|
|
60
60
|
? `amended under in-progress plans (approve = explicit ack): ${staleChildren.join(' ')}`
|
|
61
61
|
: '' });
|
|
62
|
-
|
|
62
|
+
// D189. A spec the effort created — a plan it created is no misroute.
|
|
63
|
+
const created = docs.some((d) => writes.get(String(d.meta.id))?.created === true);
|
|
63
64
|
const standingStop = recap.class === 'feature' ? 'feature scope approval — scope is approved as its concrete slicing'
|
|
64
65
|
: recap.class === 'fix' && created
|
|
65
66
|
? 'fix created a new spec — misroute tripwire (young canon: expect new-spec stops at first)'
|
package/dist/journal.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { tryGit } from './gitio.js';
|
|
3
4
|
import { version } from './version.js';
|
|
4
5
|
const dir = (root) => join(root, '.witness', 'journal');
|
|
5
6
|
export const journalRel = (id) => `.witness/journal/${id}.jsonl`;
|
|
@@ -27,6 +28,28 @@ export function readStream(root, id) {
|
|
|
27
28
|
.filter(Boolean)
|
|
28
29
|
.map((l) => JSON.parse(l));
|
|
29
30
|
}
|
|
31
|
+
// D168, D189. Each line of a stream with the commit that appended it — `git blame` names it,
|
|
32
|
+
// since the journal is append-only — as history stood at `at`, or as the file stands now when
|
|
33
|
+
// `at` is absent (a line not yet committed carries git's all-zero id). `--no-ignore-revs-file`: a
|
|
34
|
+
// dotfile's `blame.ignoreRevsFile` re-attributes lines, or fails blame outright in a repo that
|
|
35
|
+
// lacks the file. A blank line is skipped, as `readStream` skips it. SHA-1 or SHA-256 ids.
|
|
36
|
+
export function journalActs(root, stream, at) {
|
|
37
|
+
const blame = tryGit(root, 'blame', '--line-porcelain', '--no-ignore-revs-file', ...(at === undefined ? [] : [at]), '--', journalRel(stream));
|
|
38
|
+
if (!blame.ok)
|
|
39
|
+
return [];
|
|
40
|
+
const out = [];
|
|
41
|
+
let commit = '';
|
|
42
|
+
for (const line of blame.out.split('\n')) {
|
|
43
|
+
const head = /^([0-9a-f]{40}(?:[0-9a-f]{24})?) \d+ \d+/.exec(line);
|
|
44
|
+
if (head) {
|
|
45
|
+
commit = head[1];
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (line.startsWith('\t') && line.trim() !== '')
|
|
49
|
+
out.push({ entry: JSON.parse(line.slice(1)), commit });
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
30
53
|
export function latestRecap(root, effort) {
|
|
31
54
|
const recaps = readStream(root, effort).filter((e) => e.t === 'recap');
|
|
32
55
|
return recaps.at(-1);
|
package/dist/verbs/abandon.js
CHANGED
|
@@ -66,6 +66,8 @@ export async function run(ctx, argv) {
|
|
|
66
66
|
continue; // already gone (re-slice, earlier abandon)
|
|
67
67
|
if (artifact.meta.type !== 'plan')
|
|
68
68
|
continue;
|
|
69
|
+
if (String(artifact.meta.status) === 'abandoned')
|
|
70
|
+
continue; // D189: an earlier abandon withdrew it
|
|
69
71
|
// 'stamp-only': the spec loop below is this path's sole spec revert, pinned on
|
|
70
72
|
// THIS effort's write sha. A paired revert here would re-derive the owner and
|
|
71
73
|
// could land on a sibling effort's amendment — see Pairing in abandon.ts.
|
package/dist/verbs/log.js
CHANGED
|
@@ -3,9 +3,9 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { EXIT } from '../cli.js';
|
|
5
5
|
import { askText } from '../ask.js';
|
|
6
|
-
import { git, primaryRoot
|
|
6
|
+
import { git, primaryRoot } from '../gitio.js';
|
|
7
7
|
import { canonExit } from '../departure.js';
|
|
8
|
-
import {
|
|
8
|
+
import { journalActs, readStream, streamExists } from '../journal.js';
|
|
9
9
|
import { renderRefusal, v } from '../refusal.js';
|
|
10
10
|
import { short } from '../sha.js';
|
|
11
11
|
import { rows } from '../toon.js';
|
|
@@ -93,23 +93,12 @@ function commitIndex(root) {
|
|
|
93
93
|
.forEach((sha, i) => map.set(sha, i));
|
|
94
94
|
return map;
|
|
95
95
|
}
|
|
96
|
-
function lineOrigins(root, rel) {
|
|
97
|
-
const blame = tryGit(root, 'blame', '--porcelain', '--', rel);
|
|
98
|
-
if (!blame.ok)
|
|
99
|
-
return [];
|
|
100
|
-
const shas = [];
|
|
101
|
-
for (const line of blame.out.split('\n')) {
|
|
102
|
-
if (/^[0-9a-f]{40} \d+ \d+/.test(line))
|
|
103
|
-
shas.push(line.slice(0, 40));
|
|
104
|
-
}
|
|
105
|
-
return shas;
|
|
106
|
-
}
|
|
107
96
|
export function mergedRows(root) {
|
|
108
97
|
const order = commitIndex(root);
|
|
109
98
|
const rowsOut = [];
|
|
110
99
|
for (const stream of streamIds(root)) {
|
|
111
100
|
const entries = readStream(root, stream);
|
|
112
|
-
const origins =
|
|
101
|
+
const origins = journalActs(root, stream).map((a) => a.commit);
|
|
113
102
|
entries.forEach((entry, lineNo) => {
|
|
114
103
|
const sha = origins[lineNo];
|
|
115
104
|
rowsOut.push({ stream, entry, lineNo, idx: sha !== undefined ? (order.get(sha) ?? Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popovych.co/witness",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
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.3",
|
|
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.3}"
|
|
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.3} 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.3}"
|
|
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.3}"
|
|
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.3}"
|
|
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.3}"
|
|
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.3}"
|
|
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.3}"
|
|
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.
|