@popovych.co/witness 0.21.0 → 0.21.1
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/config.js +2 -2
- package/dist/context.js +7 -4
- package/dist/drive-report.js +4 -9
- package/dist/journal.js +5 -2
- package/dist/ship.js +2 -5
- package/dist/slice.js +31 -18
- package/dist/steps.js +56 -33
- package/dist/verbs/decide.js +2 -5
- package/dist/verbs/evidence.js +16 -12
- package/dist/verbs/log.js +1 -12
- package/dist/verbs/next.js +57 -33
- package/dist/verbs/step-close.js +4 -5
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/commands/witness.md +1 -1
- package/plugin/hooks/context-core.mjs +20 -10
- 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 +3 -3
- package/plugin/skills/witness-plan/SKILL.md +1 -1
- package/plugin/skills/witness-ship/SKILL.md +1 -1
package/dist/config.js
CHANGED
|
@@ -17,8 +17,8 @@ export const DEFAULT_PATHS = {
|
|
|
17
17
|
export const DOC_KEYS = ['conventions', 'design'];
|
|
18
18
|
export const DEFAULT_STEPS_PER_DISPATCH = listDefault('implement.stepsPerDispatch');
|
|
19
19
|
// Row 191 §4.2. The default 20 is the row's premise (§1): once a session's context is more
|
|
20
|
-
// than about 20% full, its output gets worse. A repo decision, so its
|
|
21
|
-
// witness.config.yaml
|
|
20
|
+
// than about 20% full, its output gets worse. A repo decision, so its home is
|
|
21
|
+
// witness.config.yaml; row 191 deferred a machine-local override (config.local.yaml, row 90).
|
|
22
22
|
export const DEFAULT_CONTEXT_LIMIT = listDefault('session.contextLimit');
|
|
23
23
|
// D145. One hour. An implement slice is ~25 minutes at the default budget of 3 steps,
|
|
24
24
|
// and the cost of the two errors is not symmetric: a generous ceiling wastes an hour
|
package/dist/context.js
CHANGED
|
@@ -27,6 +27,8 @@ const isInt = (x) => typeof x === 'number' && Number.isInteger(x);
|
|
|
27
27
|
// harmless: a drive launched from a chat session's shell carries the chat's reading, and its
|
|
28
28
|
// child runs as a session of its own. The order is the reasons' precedence: a broken value is
|
|
29
29
|
// malformed whoever wrote it, and only this session's own value can say it has no reading yet.
|
|
30
|
+
// A value with no window is not a broken one (D192): Pi's adapter writes the session alone when
|
|
31
|
+
// the harness gives no usage at all.
|
|
30
32
|
export function readFill(env) {
|
|
31
33
|
const raw = env[CONTEXT_ENV];
|
|
32
34
|
if (raw === undefined || raw === '')
|
|
@@ -43,16 +45,17 @@ export function readFill(env) {
|
|
|
43
45
|
const { session, tokens, window } = parsed;
|
|
44
46
|
if (typeof session !== 'string' || session === '')
|
|
45
47
|
return unmeasured('malformed');
|
|
46
|
-
if (!isInt(window) || window < 1)
|
|
48
|
+
if (window !== undefined && (!isInt(window) || window < 1))
|
|
47
49
|
return unmeasured('malformed');
|
|
48
|
-
if (tokens !== undefined && tokens !== null && (!isInt(tokens) || tokens < 0 || tokens > window)) {
|
|
50
|
+
if (tokens !== undefined && tokens !== null && (!isInt(tokens) || tokens < 0 || (isInt(window) && tokens > window))) {
|
|
49
51
|
return unmeasured('malformed');
|
|
50
52
|
}
|
|
51
53
|
if (session !== sessionIdOf(env))
|
|
52
54
|
return unmeasured('stale');
|
|
53
55
|
// P7: Claude Code sends no `tokens` before the session's first response, Pi sends 0 then,
|
|
54
|
-
// and Pi sends null right after a compaction. Each is a reading not taken yet
|
|
55
|
-
|
|
56
|
+
// and Pi sends null right after a compaction. Each is a reading not taken yet, and so is Pi's
|
|
57
|
+
// session alone, with no window.
|
|
58
|
+
if (!isInt(window) || !isInt(tokens) || tokens === 0)
|
|
56
59
|
return unmeasured('no reading yet');
|
|
57
60
|
return { measured: true, reading: { session, tokens, window } };
|
|
58
61
|
}
|
package/dist/drive-report.js
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
1
|
+
import { basename } from 'node:path';
|
|
3
2
|
import { handoffLine, resolveDriver } from './harness.js';
|
|
4
|
-
import { readStream } from './journal.js';
|
|
3
|
+
import { readStream, streamIds } from './journal.js';
|
|
5
4
|
import { unmetNeeds } from './needs.js';
|
|
6
5
|
import { findById, loadCanon } from './scan.js';
|
|
7
6
|
import { pendingTxn } from './txn.js';
|
|
8
7
|
import { owedDecisions, parkedGates } from './verbs/next.js';
|
|
9
8
|
import { recoverChoice } from './verbs/recover.js';
|
|
10
|
-
const journalDir = (root) => join(root, '.witness', 'journal');
|
|
11
|
-
const streams = (root) => existsSync(journalDir(root))
|
|
12
|
-
? readdirSync(journalDir(root)).filter((f) => f.endsWith('.jsonl')).map((f) => f.slice(0, -'.jsonl'.length)).sort()
|
|
13
|
-
: [];
|
|
14
9
|
export function runStart(root) {
|
|
15
|
-
return { lines: new Map(
|
|
10
|
+
return { lines: new Map(streamIds(root).map((s) => [s, readStream(root, s).length])) };
|
|
16
11
|
}
|
|
17
12
|
export function entriesSince(root, start) {
|
|
18
|
-
return
|
|
13
|
+
return streamIds(root).flatMap((stream) => readStream(root, stream).slice(start.lines.get(stream) ?? 0).map((entry) => ({ stream, entry })));
|
|
19
14
|
}
|
|
20
15
|
const prOf = (line) => /PR #(\d+)/.exec(line)?.[1];
|
|
21
16
|
// Every `run:` and `then:` line is raw: a command is never kv-escaped (D120).
|
package/dist/journal.js
CHANGED
|
@@ -73,15 +73,18 @@ export function latestRecap(root, effort) {
|
|
|
73
73
|
const recaps = readStream(root, effort).filter((e) => e.t === 'recap');
|
|
74
74
|
return recaps.at(-1);
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
// Every stream the journal holds, by id, sorted.
|
|
77
|
+
export function streamIds(root) {
|
|
77
78
|
if (!existsSync(dir(root)))
|
|
78
79
|
return [];
|
|
79
80
|
return readdirSync(dir(root))
|
|
80
81
|
.filter((f) => f.endsWith('.jsonl'))
|
|
81
82
|
.map((f) => f.slice(0, -'.jsonl'.length))
|
|
82
|
-
.filter((id) => readStream(root, id)[0]?.t === 'recap')
|
|
83
83
|
.sort();
|
|
84
84
|
}
|
|
85
|
+
export function effortStreams(root) {
|
|
86
|
+
return streamIds(root).filter((id) => readStream(root, id)[0]?.t === 'recap');
|
|
87
|
+
}
|
|
85
88
|
export function effortAbandoned(entries) {
|
|
86
89
|
return entries.some((e) => e.t === 'human-decision' && e.decision === 'abandon-effort');
|
|
87
90
|
}
|
package/dist/ship.js
CHANGED
|
@@ -18,7 +18,7 @@ import { branchName, worktreePath } from './worktree.js';
|
|
|
18
18
|
import { authoringOwed, gateSettled } from './verbs/next.js';
|
|
19
19
|
import { diffReviewedSha } from './reviewed.js';
|
|
20
20
|
import { diffBase } from './evidence.js';
|
|
21
|
-
import {
|
|
21
|
+
import { handOverIfDue } from './slice.js';
|
|
22
22
|
// Static, unlike gate.ts and next.ts: this module lies outside gate.ts's static import graph,
|
|
23
23
|
// so reaching decide.ts from here closes no registration cycle.
|
|
24
24
|
import { settleStop, settledCode } from './settle.js';
|
|
@@ -364,10 +364,7 @@ export async function runShip(ctx, planId, opts = {}) {
|
|
|
364
364
|
const code = await shipPass(ctx, planId, opts);
|
|
365
365
|
if (code !== EXIT.OK && code !== EXIT.FINDINGS)
|
|
366
366
|
return code;
|
|
367
|
-
|
|
368
|
-
const cfgR = rootR.ok ? loadConfig(rootR.value) : undefined;
|
|
369
|
-
if (rootR.ok && cfgR?.ok)
|
|
370
|
-
reportHandover(ctx, rootR.value, cfgR.value, { guard: true });
|
|
367
|
+
handOverIfDue(ctx, { guard: true });
|
|
371
368
|
return code;
|
|
372
369
|
}
|
|
373
370
|
async function shipPass(ctx, planId, opts) {
|
package/dist/slice.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import { loadConfig } from './config.js';
|
|
4
2
|
import { pctOf, reached, readFill, sessionHash, sessionIdOf, windowLabel } from './context.js';
|
|
3
|
+
import { primaryRoot } from './gitio.js';
|
|
5
4
|
import { relayLine, resolveDriver } from './harness.js';
|
|
6
|
-
import { readStream } from './journal.js';
|
|
5
|
+
import { readStream, streamIds } from './journal.js';
|
|
7
6
|
import { implementGated, redInFlight, stepProgress, stepsLine, vacuousEvidence } from './steps.js';
|
|
8
7
|
import { kv } from './toon.js';
|
|
9
8
|
// Plan's call 8 (row 191 §4.8). An act is the session's own stage work. What the CLI writes on a
|
|
@@ -21,11 +20,7 @@ const stampOf = (e) => typeof e.ctx === 'object' && e.ctx !== null ? e.ctx : {};
|
|
|
21
20
|
// review I3): it observed the behavior already there, so a session opened over the limit that only
|
|
22
21
|
// re-ran one would hand over, and so would every fresh session after it.
|
|
23
22
|
export function sessionActed(root, hash) {
|
|
24
|
-
|
|
25
|
-
if (!existsSync(dir))
|
|
26
|
-
return false;
|
|
27
|
-
return readdirSync(dir).filter((f) => f.endsWith('.jsonl')).some((f) => readStream(root, f.slice(0, -'.jsonl'.length))
|
|
28
|
-
.some((e) => ACT_KINDS.has(e.t) && !vacuousEvidence(e) && stampOf(e).session === hash));
|
|
23
|
+
return streamIds(root).some((id) => readStream(root, id).some((e) => ACT_KINDS.has(e.t) && !vacuousEvidence(e) && stampOf(e).session === hash));
|
|
29
24
|
}
|
|
30
25
|
// The entries that closed a step, in stream order. A legacy plan's closes are not the CLI's to
|
|
31
26
|
// see, so it has none.
|
|
@@ -61,10 +56,10 @@ export function closedThisSession(entries, p, hash) {
|
|
|
61
56
|
// session's closes against the step budget where it can see both; where it cannot, the skill
|
|
62
57
|
// keeps its own count, as before row 191.
|
|
63
58
|
//
|
|
64
|
-
// `redOpen` (final review I1): a red in flight (steps.ts `redInFlight`: the latest
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
59
|
+
// `redOpen` (final review I1, D192): a red in flight (steps.ts `redInFlight`: the latest non-vacuous
|
|
60
|
+
// run saw a failure, whatever step it names or none) is work the session is in the middle
|
|
61
|
+
// of, and a session never relays mid-red, so the slice goes on until its green. A closing green
|
|
62
|
+
// clears it, so this answers a close made while a red stands: a `step-close`.
|
|
68
63
|
// It outranks rule 1 too: a red re-opened once every step has closed would send the session to a
|
|
69
64
|
// gate whose evidence check fails on it, spending a round on a tree mid-red.
|
|
70
65
|
export function stepCloseVerdict(i) {
|
|
@@ -89,13 +84,17 @@ export function stepCloseVerdict(i) {
|
|
|
89
84
|
}
|
|
90
85
|
return { kind: 'continue' };
|
|
91
86
|
}
|
|
92
|
-
// Row 191 §4.6 and §4.7. The fill is floored, as `reached` decides (Plan's call 2). The next step
|
|
93
|
-
//
|
|
87
|
+
// Row 191 §4.6 and §4.7. The fill is floored, as `reached` decides (Plan's call 2). The next step is
|
|
88
|
+
// the difference of the two floored shares, so it adds up to the projection the verdict prints
|
|
89
|
+
// beside it (D192: rounded up on its own, 30,001 tokens read +4% beside 17% + next step ≈ 20%), and
|
|
90
|
+
// a step that grew the context at all never reads +0%.
|
|
94
91
|
export function contextLine(fill, limit, budget, growth) {
|
|
95
92
|
if (!fill.measured)
|
|
96
93
|
return `unmeasured (${fill.reason}) — step budget ${budget}`;
|
|
97
94
|
const { tokens, window } = fill.reading;
|
|
98
|
-
const next = growth === undefined
|
|
95
|
+
const next = growth === undefined
|
|
96
|
+
? ''
|
|
97
|
+
: ` · next step ≈ +${Math.max(pctOf(tokens + growth, window) - pctOf(tokens, window), 1)}%`;
|
|
99
98
|
return `${pctOf(tokens, window)}% of ${windowLabel(window)} · limit ${limit}%${next}`;
|
|
100
99
|
}
|
|
101
100
|
export function sliceLine(v) {
|
|
@@ -111,16 +110,20 @@ export function sliceLine(v) {
|
|
|
111
110
|
// green that closes no step (a re-record, a fix round) only reports the fill. A legacy plan's
|
|
112
111
|
// closes are not the CLI's to see (§4.3), so until its implement gate every passing green
|
|
113
112
|
// decides. The close is journaled before this runs, so a config that will not load costs the
|
|
114
|
-
// report and never the verb's exit: dispatch-report's relay line keeps the same doctrine.
|
|
113
|
+
// report and never the verb's exit: dispatch-report's relay line keeps the same doctrine. It says
|
|
114
|
+
// so on stderr (D192), so the missing report is never silent. `entries` is the stream as the
|
|
115
|
+
// caller read it to name the step closed, so the report decides from that same read (D192).
|
|
115
116
|
export function reportStepClose(ctx, root, plan, opts) {
|
|
116
117
|
if (opts.closed === undefined && !opts.passingGreen)
|
|
117
118
|
return;
|
|
118
119
|
const cfg = loadConfig(root);
|
|
119
|
-
if (!cfg.ok)
|
|
120
|
+
if (!cfg.ok) {
|
|
121
|
+
ctx.err(`warn: no context line — witness.config.yaml did not load (${cfg.violations[0].rule})`);
|
|
120
122
|
return;
|
|
123
|
+
}
|
|
121
124
|
const limit = cfg.value.session.contextLimit;
|
|
122
125
|
const budget = cfg.value.implement.stepsPerDispatch;
|
|
123
|
-
const entries = readStream(root, String(plan.meta.id));
|
|
126
|
+
const entries = opts.entries ?? readStream(root, String(plan.meta.id));
|
|
124
127
|
const progress = stepProgress(plan, entries);
|
|
125
128
|
const fill = readFill(ctx.env);
|
|
126
129
|
// §4.5's `g` is the size of the next step, so once every step is closed there is none to project.
|
|
@@ -169,6 +172,16 @@ export function reportHandover(ctx, root, cfg, opts) {
|
|
|
169
172
|
ctx.out(kv('relay', relayLine(driver.value.harness)));
|
|
170
173
|
return true;
|
|
171
174
|
}
|
|
175
|
+
// Row 191 §4.4. `reportHandover` at the exit of a verb that holds no root or config there (a
|
|
176
|
+
// `decide --revise`, a ship run): both are read again from the cwd. One that will not resolve
|
|
177
|
+
// hands nothing over, and the verb's own exit stands.
|
|
178
|
+
export function handOverIfDue(ctx, opts) {
|
|
179
|
+
const rootR = primaryRoot(ctx.cwd);
|
|
180
|
+
if (!rootR.ok)
|
|
181
|
+
return false;
|
|
182
|
+
const cfgR = loadConfig(rootR.value);
|
|
183
|
+
return cfgR.ok && reportHandover(ctx, rootR.value, cfgR.value, opts);
|
|
184
|
+
}
|
|
172
185
|
// Row 191 §4.5 "Before any work". A session already at the limit when it opens cannot hand over
|
|
173
186
|
// (it has done nothing yet), and with no first-step exemption at a step close each of its slices
|
|
174
187
|
// will be one step, so the CLI says so where the session begins. Deterministic, so a fresh
|
package/dist/steps.js
CHANGED
|
@@ -2,10 +2,11 @@ import { v } from './refusal.js';
|
|
|
2
2
|
const manifest = (plan) => (Array.isArray(plan.meta.steps) ? plan.meta.steps : [])
|
|
3
3
|
.filter((s) => typeof s === 'object' && s !== null)
|
|
4
4
|
.map((s) => ({ id: String(s.id), kind: s.scaffolding === true ? 'scaffolding' : 'criteria' }));
|
|
5
|
-
// A cycle can be a plan step's only where the plan has a criteria step for it to name.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
5
|
+
// A cycle can be a plan step's only where the plan has a criteria step for it to name. Every chore
|
|
6
|
+
// plan has none, since its parent, the principles, carries no criteria; nor does a plan whose
|
|
7
|
+
// spec's criteria an earlier plan realized (an empty criteria delta). None of such a plan's evidence
|
|
8
|
+
// is a step's: it owes no `--step`, and its untagged evidence hides no step's close.
|
|
9
|
+
export const hasCriteriaStep = (plan) => manifest(plan).some((s) => s.kind === 'criteria');
|
|
9
10
|
const outcomes = (e) => (Array.isArray(e.tests) ? e.tests : []);
|
|
10
11
|
// A red that passed observed nothing missing: the behavior it asserts was already there. It opens
|
|
11
12
|
// no cycle (Plan's call 3), it is no act of the session's, and drive counts it as no progress
|
|
@@ -68,40 +69,62 @@ export function stepProgress(plan, entries) {
|
|
|
68
69
|
export function closedAt(p, index) {
|
|
69
70
|
return p.known ? p.steps.find((s) => s.closedAt === index)?.id : undefined;
|
|
70
71
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
72
|
+
// Whether an evidence run counts toward the red in flight. Every run executes the parent tag's
|
|
73
|
+
// whole suite, so one that counts is the suite's state at that run, whatever step it names or none.
|
|
74
|
+
// A vacuous red does not count, though a plain one ran this tree's whole suite and saw it pass (only
|
|
75
|
+
// verify-red's ran against the base): a vacuous red is no cycle (Plan's call 3), so the genuine red
|
|
76
|
+
// before it stays open, and the green routed for it closes the step. An entry with no outcome saw
|
|
77
|
+
// nothing at all.
|
|
78
|
+
const countsInFlight = (e) => e.t === 'test-evidence' && e.vacuous !== true && outcomes(e).length > 0;
|
|
79
|
+
// Row 191 §1–§2, D192. The red in flight, the one reading the step row, the evidence row, `next`'s
|
|
80
|
+
// hand-over and the verdict at a step close all share: the plan's latest run that counts (a
|
|
81
|
+
// non-vacuous one) saw a failure, a red with a failing test or a green that did not pass. A passing
|
|
82
|
+
// green ends it whatever step it names, or none: read per step, the red the skill tags on a legacy
|
|
83
|
+
// plan, whose routed green names no step, stayed open for good (D191's re-review). A green that
|
|
84
|
+
// fails after a passing one re-opens it: read from the latest red alone, `next` routed that green
|
|
85
|
+
// while the verdict let the slice end (D192). Whether a red is open, whatever step it names or none
|
|
86
|
+
// (a fix round's, a legacy cycle's): what keeps `next` from handing over, and a step close from
|
|
87
|
+
// ending the slice, mid-red.
|
|
88
|
+
export function redInFlight(entries) {
|
|
89
|
+
const last = entries.filter(countsInFlight).at(-1);
|
|
90
|
+
return last !== undefined && outcomes(last).some((t) => !t.ok);
|
|
91
|
+
}
|
|
92
|
+
// The latest genuine red and where it stands in the stream.
|
|
93
|
+
function latestRed(entries) {
|
|
94
|
+
let at = -1;
|
|
95
|
+
entries.forEach((e, i) => { if (opensCycle(e))
|
|
96
|
+
at = i; });
|
|
97
|
+
const step = entries[at]?.step;
|
|
98
|
+
return at === -1 ? undefined : { at, ...(typeof step === 'string' ? { step } : {}) };
|
|
99
|
+
}
|
|
100
|
+
// The step of the red in flight: the latest genuine red's, when it names one. One red is open at a
|
|
101
|
+
// time, so its step is the open step, the one the green owed names. Which steps are closed is
|
|
77
102
|
// another question (closingIndex): a criteria step closes only at a green tagged with it after its
|
|
78
103
|
// own red.
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
for (const e of entries) {
|
|
82
|
-
if (opensCycle(e))
|
|
83
|
-
open = e;
|
|
84
|
-
else if (closesCycle(e))
|
|
85
|
-
open = undefined;
|
|
86
|
-
}
|
|
87
|
-
return open;
|
|
104
|
+
function stepInFlight(entries) {
|
|
105
|
+
return redInFlight(entries) ? latestRed(entries)?.step : undefined;
|
|
88
106
|
}
|
|
89
|
-
// Whether
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
// Whether the green owed names this step: it is the red in flight's. That is not whether the step
|
|
108
|
+
// holds an open red of its own (D192): once its cycle closed, a later green that failed still owes
|
|
109
|
+
// the green here (redCycleClosed).
|
|
110
|
+
export function greenOwedOn(entries, stepId) {
|
|
111
|
+
return stepInFlight(entries) === stepId;
|
|
93
112
|
}
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
// D192. Whether the latest genuine red's cycle has closed: a green tagged with its step passed after
|
|
114
|
+
// it. While a red is in flight that means a later run failed (a green naming another step, say):
|
|
115
|
+
// the green owed still names that red's step, but the step holds no open red of its own.
|
|
116
|
+
export function redCycleClosed(entries) {
|
|
117
|
+
const red = latestRed(entries);
|
|
118
|
+
return red?.step !== undefined && closingIndex({ id: red.step, kind: 'criteria' }, entries.slice(red.at)) !== undefined;
|
|
97
119
|
}
|
|
98
|
-
// Row 191 §1–§2 (final review I1).
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
// Row 191 §1–§2 (final review I1), D192. Which step the green owed names: the red in flight's, when
|
|
121
|
+
// it is a criteria step of the plan. That green comes before anything else, wherever the red stands:
|
|
122
|
+
// recorded out of order ahead of an unclosed step, re-opened on a closed one, or on a step whose own
|
|
123
|
+
// cycle has since closed and a later green failed (D192). Read whether or not the plan's progress is
|
|
124
|
+
// known. A criteria step alone, because `test-evidence` refuses a green naming any other: after a red
|
|
125
|
+
// on a step an amend removed or turned into scaffolding, the green owed names none.
|
|
126
|
+
export function greenOwedStep(plan, entries) {
|
|
127
|
+
const step = stepInFlight(entries);
|
|
105
128
|
return manifest(plan).find((s) => s.kind === 'criteria' && s.id === step)?.id;
|
|
106
129
|
}
|
|
107
130
|
// The step's latest red passed: the behavior it asserts already exists, as when the
|
package/dist/verbs/decide.js
CHANGED
|
@@ -23,7 +23,7 @@ import { short } from '../sha.js';
|
|
|
23
23
|
import { cmd, kv, rows } from '../toon.js';
|
|
24
24
|
import { ROUND_BOUND, anchorRecurrence, boundReached, lastGateRun, openReopen, pendingDecision, repairGranted, roundsSinceApprove, } from '../rounds.js';
|
|
25
25
|
import { prepareStamp, writeStamp } from '../stamp.js';
|
|
26
|
-
import {
|
|
26
|
+
import { handOverIfDue } from '../slice.js';
|
|
27
27
|
// D154. The untrusted `cmd:` list for a block, resolved from the ctx alone — `renderBound`
|
|
28
28
|
// and `--show` both render without a `root` in scope, and gate.ts's renderChoices took the
|
|
29
29
|
// same route for the same reason. Best-effort: a root that will not resolve costs the trust
|
|
@@ -128,10 +128,7 @@ export async function run(ctx, argv) {
|
|
|
128
128
|
// `next` settle through that door too, and each hands over at its own point.
|
|
129
129
|
if (code !== EXIT.OK || act.value.decision !== 'revise')
|
|
130
130
|
return code;
|
|
131
|
-
|
|
132
|
-
const cfgR = rootR.ok ? loadConfig(rootR.value) : undefined;
|
|
133
|
-
if (rootR.ok && cfgR?.ok)
|
|
134
|
-
reportHandover(ctx, rootR.value, cfgR.value, { guard: false });
|
|
131
|
+
handOverIfDue(ctx, { guard: false });
|
|
135
132
|
return code;
|
|
136
133
|
}
|
|
137
134
|
function show(ctx, spec, gate, target) {
|
package/dist/verbs/evidence.js
CHANGED
|
@@ -15,25 +15,29 @@ const exitOf = (violations) => (violations.some((x) => x.rule === 'locked') ? EX
|
|
|
15
15
|
const EVIDENCE_USAGE = 'witness test-evidence <plan-id> --phase red|green [--step <id>]';
|
|
16
16
|
const VERIFY_USAGE = 'witness verify-red <plan-id> [--base <ref>] [--step <id>]';
|
|
17
17
|
// The plan id is the first token that is neither a flag nor a flag's value, so `--step s1`
|
|
18
|
-
// written before the id never reads as the plan.
|
|
18
|
+
// written before the id never reads as the plan. `step-close` reads its id here too (D192).
|
|
19
19
|
const VALUED = new Set(['--phase', '--step', '--base']);
|
|
20
|
-
const planIdOf = (argv) => argv.find((a, i) => !a.startsWith('--') && !VALUED.has(argv[i - 1] ?? ''));
|
|
21
|
-
// Row 191 §4.3. `--step`
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
function stepArg(argv, usage) {
|
|
20
|
+
export const planIdOf = (argv) => argv.find((a, i) => !a.startsWith('--') && !VALUED.has(argv[i - 1] ?? ''));
|
|
21
|
+
// Row 191 §4.3. The step `--step` names, if it names one: a flag where the value belongs is no
|
|
22
|
+
// value. Each verb says what no step means, so `step-close` reads it here too (D192).
|
|
23
|
+
export function stepOf(argv) {
|
|
25
24
|
const at = argv.indexOf('--step');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const value = at === -1 ? undefined : argv[at + 1];
|
|
26
|
+
return value === undefined || value.startsWith('--') ? undefined : value;
|
|
27
|
+
}
|
|
28
|
+
// `--step` named with no value is refused rather than read as no step: an untagged cycle is
|
|
29
|
+
// another claim (a fix round), not a typo's default.
|
|
30
|
+
function stepArg(argv, usage) {
|
|
31
|
+
const step = stepOf(argv);
|
|
32
|
+
return argv.includes('--step') && step === undefined ? refuse([v('--step', 'required', 'absent', usage)]) : ok(step);
|
|
30
33
|
}
|
|
31
34
|
// Row 191 §4.4. The green just journaled is the stream's last line, and whether it closed a step
|
|
32
35
|
// is the plan's progress to say: a step's first passing green after its red does, a re-record or
|
|
33
|
-
// a fix round does not.
|
|
36
|
+
// a fix round does not. The report reads the same entries (D192): one read names the step closed
|
|
37
|
+
// and decides the slice.
|
|
34
38
|
function reportGreen(ctx, stateRoot, plan, passingGreen) {
|
|
35
39
|
const entries = readStream(stateRoot, String(plan.meta.id));
|
|
36
|
-
reportStepClose(ctx, stateRoot, plan, { closed: closedAt(stepProgress(plan, entries), entries.length - 1), passingGreen });
|
|
40
|
+
reportStepClose(ctx, stateRoot, plan, { closed: closedAt(stepProgress(plan, entries), entries.length - 1), passingGreen, entries });
|
|
37
41
|
}
|
|
38
42
|
export async function run(ctx, argv) {
|
|
39
43
|
const planId = planIdOf(argv);
|
package/dist/verbs/log.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import { parseArgs } from 'node:util';
|
|
4
2
|
import { EXIT } from '../cli.js';
|
|
5
3
|
import { askText } from '../ask.js';
|
|
6
4
|
import { git, primaryRoot } from '../gitio.js';
|
|
7
5
|
import { canonExit } from '../departure.js';
|
|
8
|
-
import { journalActs, readStream, streamExists } from '../journal.js';
|
|
6
|
+
import { journalActs, readStream, streamExists, streamIds } from '../journal.js';
|
|
9
7
|
import { renderRefusal, v } from '../refusal.js';
|
|
10
8
|
import { findById, loadCanon } from '../scan.js';
|
|
11
9
|
import { short } from '../sha.js';
|
|
@@ -63,15 +61,6 @@ function detail(e) {
|
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
|
-
const streamIds = (root) => {
|
|
67
|
-
const dir = join(root, '.witness', 'journal');
|
|
68
|
-
if (!existsSync(dir))
|
|
69
|
-
return [];
|
|
70
|
-
return readdirSync(dir)
|
|
71
|
-
.filter((f) => f.endsWith('.jsonl'))
|
|
72
|
-
.map((f) => f.slice(0, -'.jsonl'.length))
|
|
73
|
-
.sort();
|
|
74
|
-
};
|
|
75
64
|
// D166. Read through `canonExit`, the one reading of a departure: a re-slice that was taken
|
|
76
65
|
// back is no longer a lineage step.
|
|
77
66
|
function successorOf(root, id) {
|
package/dist/verbs/next.js
CHANGED
|
@@ -13,7 +13,7 @@ import { diffReviewedSha, effortOf, effortReviewedSha, effortSpecs, effortWrites
|
|
|
13
13
|
import { baseForSpec } from '../history.js';
|
|
14
14
|
import { canonicalSha } from '../sha.js';
|
|
15
15
|
import { changedFiles, diffBase, evidenceForDiff, isTestPath } from '../evidence.js';
|
|
16
|
-
import { implementGated, lastRedVacuous,
|
|
16
|
+
import { greenOwedOn, greenOwedStep, hasCriteriaStep, implementGated, lastRedVacuous, redCycleClosed, redInFlight, stepProgress, stepsLine, } from '../steps.js';
|
|
17
17
|
import { SESSION_DEFAULT, stagePin } from '../model.js';
|
|
18
18
|
import { handoffLine, relayLine, resolveDriver, resolveJudge } from '../harness.js';
|
|
19
19
|
import { atHome, worktreeFlow, worktreePath } from '../worktree.js';
|
|
@@ -165,49 +165,59 @@ function judgeNote(entries, gate, judge) {
|
|
|
165
165
|
//
|
|
166
166
|
// `step` is set where `test-evidence` refuses a cycle that names none (row 191): before the
|
|
167
167
|
// first implement gate, on a plan whose progress is known and that has a criteria step.
|
|
168
|
-
|
|
168
|
+
//
|
|
169
|
+
// D192. With anything changed, the phase is the one reading of a red in flight (steps.ts
|
|
170
|
+
// `redInFlight`, `midRed` here): its green while one is, a red otherwise. An empty diff routes a red
|
|
171
|
+
// whatever is in flight. Read from the parent tag's latest red, as it was, a vacuous red after a
|
|
172
|
+
// genuine one routed a second red here while the step row routed the green. The gate's own
|
|
173
|
+
// semantics stay `evidenceForDiff`'s, so the owed note can still name a red the gate wants after
|
|
174
|
+
// that green.
|
|
175
|
+
//
|
|
176
|
+
// D192. Every step of a plan with no criteria step (every chore plan, and one whose spec's criteria
|
|
177
|
+
// an earlier plan realized) is scaffolding, so with nothing changed yet its work is the change
|
|
178
|
+
// itself, never a failing test first. The note claims no more (final review): such a plan need not
|
|
179
|
+
// be a chore, and once tagged tests land in its diff the gate can still want their red.
|
|
180
|
+
function evidenceRow(id, files, report, seat, opts) {
|
|
169
181
|
const base = { stage: 'implement', target: id, ...seat };
|
|
170
|
-
const stepFlag = step === undefined ? '' : ` --step ${step}`;
|
|
182
|
+
const stepFlag = opts.step === undefined ? '' : ` --step ${opts.step}`;
|
|
171
183
|
if (report === undefined) {
|
|
172
184
|
return {
|
|
173
185
|
line: `witness test-evidence ${id} --phase red${stepFlag}`, ...base,
|
|
174
|
-
note:
|
|
186
|
+
note: opts.criteria
|
|
187
|
+
? 'nothing changed yet — write the failing test first'
|
|
188
|
+
: 'nothing changed yet — make the change · every step of this plan is scaffolding',
|
|
175
189
|
};
|
|
176
190
|
}
|
|
177
191
|
const owed = report.required
|
|
178
192
|
.filter((r) => !(r.red && r.green && !r.vacuous))
|
|
179
193
|
.map((r) => `${r.tag} ${!r.red || r.vacuous ? 'red' : 'green'}`)
|
|
180
194
|
.join(' · ');
|
|
181
|
-
const
|
|
182
|
-
const liveRed = parent !== undefined && parent.red && !parent.vacuous;
|
|
183
|
-
const line = !liveRed && files.some((f) => !isTestPath(f))
|
|
195
|
+
const line = !opts.midRed && files.some((f) => !isTestPath(f))
|
|
184
196
|
? `witness verify-red ${id}${stepFlag}`
|
|
185
|
-
: `witness test-evidence ${id} --phase ${
|
|
186
|
-
|
|
187
|
-
// routed mid-red.
|
|
188
|
-
return {
|
|
189
|
-
line, ...base, ...noteOf(owed === '' ? undefined : `evidence owed: ${owed}`),
|
|
190
|
-
...(liveRed ? { midRed: true } : {}),
|
|
191
|
-
};
|
|
197
|
+
: `witness test-evidence ${id} --phase ${opts.midRed ? 'green' : 'red'}${stepFlag}`;
|
|
198
|
+
return { line, ...base, ...noteOf(owed === '' ? undefined : `evidence owed: ${owed}`) };
|
|
192
199
|
}
|
|
193
200
|
// The run that works a plan's next unfinished step. A step whose latest red came back vacuous
|
|
194
201
|
// gets `verify-red`, the one run that can witness its red: a plain red would come back vacuous
|
|
195
|
-
// again and `next` would answer with the same line twice.
|
|
196
|
-
//
|
|
202
|
+
// again and `next` would answer with the same line twice. A green owed on the step (`greenOwedOn`)
|
|
203
|
+
// outranks that: a green, not another red, is what the step is owed.
|
|
197
204
|
function stepLine(id, s, entries) {
|
|
198
205
|
if (s.kind === 'scaffolding')
|
|
199
206
|
return { line: `witness step-close ${id} --step ${s.id}` };
|
|
200
|
-
if (
|
|
207
|
+
if (greenOwedOn(entries, s.id))
|
|
201
208
|
return { line: `witness test-evidence ${id} --phase green --step ${s.id}` };
|
|
202
209
|
if (lastRedVacuous(entries, s.id)) {
|
|
203
210
|
return { line: `witness verify-red ${id} --step ${s.id}`, why: 'its red was vacuous — verify-red reconstructs it against the base' };
|
|
204
211
|
}
|
|
205
212
|
return { line: `witness test-evidence ${id} --phase red --step ${s.id}` };
|
|
206
213
|
}
|
|
207
|
-
// The step an evidence row names once every step is closed
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
214
|
+
// The step an evidence row names once every step is closed, unless the green owed names one (D192:
|
|
215
|
+
// that step comes first): that of the latest red naming a criteria step of the plan as it stands.
|
|
216
|
+
// So it answers when no red is in flight, and also while one is that names no criteria step of the
|
|
217
|
+
// plan: a failing green with no genuine red before it, or a red on a step an amend removed or turned
|
|
218
|
+
// into scaffolding. Never a scaffolding step or one an amend removed, both of which `test-evidence`
|
|
219
|
+
// refuses. One exists whenever the plan has a criteria step, since none closes without a red tagged
|
|
220
|
+
// with it, and a plan with none is owed no step.
|
|
211
221
|
function evidenceStep(steps, entries) {
|
|
212
222
|
const criteria = new Set(steps.filter((s) => s.kind === 'criteria').map((s) => s.id));
|
|
213
223
|
const red = entries
|
|
@@ -277,18 +287,27 @@ export function flowAction(root, cfg, plan, judge) {
|
|
|
277
287
|
// step is the work. Legacy plans and fix rounds after a gate keep the rows below.
|
|
278
288
|
const progress = stepProgress(plan, entries);
|
|
279
289
|
const stepped = progress.known && !implementGated(entries) ? progress : undefined;
|
|
280
|
-
// Final review I1
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
290
|
+
// Final review I1, its re-review and D192. A red in flight (steps.ts: the latest non-vacuous run
|
|
291
|
+
// saw a failure) is one whatever step it names, or none: recorded out of order, ahead of a
|
|
292
|
+
// scaffolding step left unclosed, re-opened on a closed step, or a fix round's. Whatever row
|
|
293
|
+
// follows, the session is mid-red, and the evidence row's phase reads the same answer unless the
|
|
294
|
+
// diff is empty.
|
|
295
|
+
const midRed = redInFlight(entries);
|
|
296
|
+
const inFlight = midRed ? { midRed: true } : {};
|
|
285
297
|
if (stepped?.next !== undefined) {
|
|
286
|
-
//
|
|
287
|
-
// leave the
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
298
|
+
// The green owed comes before the first unclosed step: routing that step's red or its close
|
|
299
|
+
// would leave the green owed unrouted. D192: once the latest genuine red's cycle has closed
|
|
300
|
+
// (`redCycleClosed`), what failed is a later green (a failing red would be the latest genuine red,
|
|
301
|
+
// its cycle still open), and the note says so rather than naming a red its step no longer holds.
|
|
302
|
+
// Not "the last run": a vacuous red after that green may have passed.
|
|
303
|
+
const owed = greenOwedStep(plan, entries);
|
|
304
|
+
const owedStep = stepped.steps.find((s) => s.id === owed);
|
|
305
|
+
const { line, why } = stepLine(id, owedStep ?? stepped.next, entries);
|
|
306
|
+
const ahead = owedStep === undefined || owedStep.id === stepped.next.id
|
|
307
|
+
? why
|
|
308
|
+
: redCycleClosed(entries)
|
|
309
|
+
? 'the last green failed — a passing green comes first'
|
|
310
|
+
: `${owedStep.id} has an open red — its green comes first`;
|
|
292
311
|
return {
|
|
293
312
|
line, stage: 'implement', target: id, ...inWorktree, ...noteOf(`steps ${stepsLine(stepped)}`, ahead), ...inFlight,
|
|
294
313
|
};
|
|
@@ -299,7 +318,12 @@ export function flowAction(root, cfg, plan, judge) {
|
|
|
299
318
|
const report = baseR.ok && files.length > 0 ? evidenceForDiff(wt, root, plan, baseR.value) : undefined;
|
|
300
319
|
if (report === undefined || !report.satisfied) {
|
|
301
320
|
return {
|
|
302
|
-
...evidenceRow(id,
|
|
321
|
+
...evidenceRow(id, files, report, inWorktree, {
|
|
322
|
+
midRed, criteria: hasCriteriaStep(plan),
|
|
323
|
+
// D192. While a red is in flight its green is owed, so the row names the red in flight's step,
|
|
324
|
+
// as the step row does. Read from the latest red of any kind, it named a vacuous red's step.
|
|
325
|
+
step: stepped !== undefined ? greenOwedStep(plan, entries) ?? evidenceStep(stepped.steps, entries) : undefined,
|
|
326
|
+
}),
|
|
303
327
|
...inFlight,
|
|
304
328
|
};
|
|
305
329
|
}
|
package/dist/verbs/step-close.js
CHANGED
|
@@ -8,15 +8,14 @@ import { reportStepClose } from '../slice.js';
|
|
|
8
8
|
import { checkStep, closingIndex } from '../steps.js';
|
|
9
9
|
import { renderRefusal, v } from '../refusal.js';
|
|
10
10
|
import { kv } from '../toon.js';
|
|
11
|
+
import { planIdOf, stepOf } from './evidence.js';
|
|
11
12
|
// Row 191 §4.3. A scaffolding step has no red→green to witness, so its close is journaled on
|
|
12
13
|
// its own and the entry is the whole act. Like `test-evidence` it checks no plan status, and it
|
|
13
14
|
// runs from the primary root or the plan's worktree alike: the journal lives at the primary root.
|
|
14
15
|
export async function run(ctx, argv) {
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const step = value === undefined || value.startsWith('--') ? undefined : value;
|
|
19
|
-
const planId = argv.find((a, i) => !a.startsWith('--') && argv[i - 1] !== '--step');
|
|
16
|
+
// `--step` with no value names no step, and `checkStep` refuses the close that names none.
|
|
17
|
+
const step = stepOf(argv);
|
|
18
|
+
const planId = planIdOf(argv);
|
|
20
19
|
if (!planId) {
|
|
21
20
|
renderRefusal([v('plan-id', 'required', 'absent', 'witness step-close <plan-id> --step <id>')]).forEach(ctx.err);
|
|
22
21
|
return EXIT.REFUSED;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popovych.co/witness",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
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.21.
|
|
4
|
+
"version": "0.21.1",
|
|
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.21.
|
|
15
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.21.1}"
|
|
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.
|
|
@@ -39,6 +39,11 @@ const SESSION = /^(["']?)session\1[ \t]*:(?=\s|$)/;
|
|
|
39
39
|
// A plain key needs whitespace after its colon; a quoted one may take its value straight after,
|
|
40
40
|
// the JSON-like flow form (`{"contextLimit":15}`).
|
|
41
41
|
const LIMIT = /^(?:(["'])contextLimit\1\s*:|contextLimit\s*:(?=\s|$))\s*([\s\S]*)$/;
|
|
42
|
+
// Only in a flow map, though (D192). In a block map a value straight after the colon is a YAML
|
|
43
|
+
// error for a quoted key. A plain one is no key there at all: with no `key: value` entry beside it,
|
|
44
|
+
// the whole of `session:` is one string, and beside one, before or after it, it is a YAML error.
|
|
45
|
+
// The CLI refuses the config either way.
|
|
46
|
+
const ADJACENT = /^(["']?)contextLimit\1\s*:(?=\S)/;
|
|
42
47
|
|
|
43
48
|
// A comment opens at a `#` that starts the line or follows whitespace; inside a word it is text.
|
|
44
49
|
const uncomment = (line) => line.replace(/(^|\s)#.*$/, '');
|
|
@@ -78,13 +83,16 @@ function flowEntries(text) {
|
|
|
78
83
|
return undefined;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
// The `session:` map's own entries, one `key: value` text each
|
|
82
|
-
// no map — nothing under it is null, and a scalar or a
|
|
83
|
-
// CLI refuses. A block map's entries are its lines at the
|
|
84
|
-
// `contextLimit` nested deeper is some other key's.
|
|
86
|
+
// The `session:` map's own entries, one `key: value` text each, and whether the map is a flow
|
|
87
|
+
// map. Undefined when `session:` holds no map — nothing under it is null, and a scalar or a
|
|
88
|
+
// sequence is not a map — all of which the CLI refuses. A block map's entries are its lines at the
|
|
89
|
+
// first child's indentation, so a `contextLimit` nested deeper is some other key's.
|
|
85
90
|
function sessionEntries(lines, at) {
|
|
86
91
|
const inline = uncomment(lines[at].replace(SESSION, '')).trim();
|
|
87
|
-
if (inline.startsWith('{'))
|
|
92
|
+
if (inline.startsWith('{')) {
|
|
93
|
+
const entries = flowEntries([inline, ...lines.slice(at + 1).map(uncomment)].join('\n'));
|
|
94
|
+
return entries === undefined ? undefined : { flow: true, entries };
|
|
95
|
+
}
|
|
88
96
|
if (inline !== '') return undefined;
|
|
89
97
|
const entries = [];
|
|
90
98
|
let indent;
|
|
@@ -97,7 +105,7 @@ function sessionEntries(lines, at) {
|
|
|
97
105
|
if (lead === indent) entries.push(text);
|
|
98
106
|
}
|
|
99
107
|
if (indent === undefined || /^-(\s|$)/.test(entries[0])) return undefined;
|
|
100
|
-
return entries;
|
|
108
|
+
return { flow: false, entries };
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
/**
|
|
@@ -109,10 +117,12 @@ export function contextLimitOf(text) {
|
|
|
109
117
|
const lines = text.replace(/^\uFEFF/, '').split(/\r?\n/);
|
|
110
118
|
const at = lines.findIndex((line) => SESSION.test(line));
|
|
111
119
|
if (at === -1) return DEFAULT_LIMIT;
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
for (const entry of entries) {
|
|
115
|
-
const
|
|
120
|
+
const session = sessionEntries(lines, at);
|
|
121
|
+
if (session === undefined) return undefined;
|
|
122
|
+
for (const entry of session.entries) {
|
|
123
|
+
const text = entry.trim();
|
|
124
|
+
if (!session.flow && ADJACENT.test(text)) return undefined;
|
|
125
|
+
const m = LIMIT.exec(text);
|
|
116
126
|
if (!m) continue;
|
|
117
127
|
const n = numberOf((m[2] ?? '').trim());
|
|
118
128
|
return Number.isInteger(n) && n >= 1 && n <= 100 ? n : undefined;
|
|
@@ -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.21.
|
|
6
|
+
${WITNESS_BIN:-npx -y @popovych.co/witness@0.21.1} 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.21.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.21.1}"
|
|
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.21.
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.21.1}"
|
|
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.21.
|
|
19
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.21.1}"
|
|
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.21.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.21.1}"
|
|
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.
|
|
@@ -74,11 +74,11 @@ slice: end — 17% + next step ≈ 22% passes the 20% limit
|
|
|
74
74
|
- `slice: continue` → take the next step.
|
|
75
75
|
- `slice: end — <why>` → the slice is over: `dispatch-report`, then the relay (below).
|
|
76
76
|
- `slice: gate — no steps remain` → skip the relay and go to the gate.
|
|
77
|
-
- `slice: own count —
|
|
77
|
+
- `slice: own count — the CLI cannot count this session's steps · end after 3 step(s) by your own count` → so you count them, from this session's own closes. Once this session has closed that many, the slice is over, as at `slice: end`.
|
|
78
78
|
|
|
79
79
|
`steps: unknown (evidence predates step tags)` marks a plan begun before step tags: the CLI cannot see which of its steps are done. Keep your own count from `$WITNESS log <plan-id>` (steps with a green `test-evidence` are done), still name the step in every cycle, still obey `slice: end`, and go to the gate when your count says no steps remain — the CLI cannot print `slice: gate` for this plan.
|
|
80
80
|
|
|
81
|
-
Every passing green prints `context:`, and so does every `step-close` that closes its step; a passing green that closes no step — a re-record, a fix round's cycle — prints it alone. A vacuous `verify-red` prints none: its green only confirms the restore and journals nothing. When a passing green that closes a step, or a `step-close` that closes one, prints no `context:` line at all, the CLI could not read the config:
|
|
81
|
+
Every passing green prints `context:`, and so does every `step-close` that closes its step; on a known plan, a passing green that closes no step — a re-record, a fix round's cycle — prints it alone. A vacuous `verify-red` prints none: its green only confirms the restore and journals nothing. When a passing green that closes a step, or a `step-close` that closes one, prints no `context:` line at all, the CLI could not read the config, and says so on stderr: `warn: no context line — witness.config.yaml did not load (<rule>)`. Say so, show the human `witness check`, and end your turn — the same stop as a `dispatch-report` that prints no `relay:` (below).
|
|
82
82
|
|
|
83
83
|
**Act on the context warning.** Once per session, when the fill has reached the limit, a `witness: context …` line follows a command's result: `witness: context 21% of 1M has reached the 20% limit — finish what you are in (implement: the red→green), then hand over`. It is addressed to you — never background to read past. Finish the red→green you are in — the step you are working, through its green; never stop mid-red — then run `dispatch-report` and relay, even if an earlier `slice:` said continue. Between steps, relay at once. Two things outrank it: a `slice: gate` still goes to the gate, and a session that has closed no step yet works its first one through its close. A fix round after a gate has no step to close: finish the round and re-gate — the gate is where it hands over.
|
|
84
84
|
|
|
@@ -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.21.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.21.1}"
|
|
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.21.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.21.1}"
|
|
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.
|