@popovych.co/witness 0.23.0 → 0.24.0
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/NOTICE.md +1 -1
- package/README.md +7 -3
- package/dist/ask.js +8 -2
- package/dist/cli.js +4 -1
- package/dist/drafts.js +168 -0
- package/dist/gitio.js +6 -1
- package/dist/idlists.js +31 -0
- package/dist/slice.js +5 -1
- package/dist/verbs/decide.js +8 -1
- package/dist/verbs/design.js +36 -3
- package/dist/verbs/draft.js +107 -0
- package/dist/verbs/init.js +1 -0
- package/dist/verbs/next.js +35 -4
- package/dist/verbs/recap.js +13 -29
- package/dist/worktree.js +8 -5
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/commands/witness.md +4 -4
- package/plugin/hooks/session-dashboard.sh +1 -1
- package/plugin/skills/witness-brainstorm/SKILL.md +73 -28
- package/plugin/skills/witness-decompose/SKILL.md +4 -3
- package/plugin/skills/witness-design/SKILL.md +34 -9
- package/plugin/skills/witness-implement/SKILL.md +2 -2
- package/plugin/skills/witness-plan/SKILL.md +2 -2
- package/plugin/skills/witness-ship/SKILL.md +2 -2
package/NOTICE.md
CHANGED
|
@@ -28,7 +28,7 @@ From **superpowers** (obra) — `LICENSES/superpowers-MIT.txt`:
|
|
|
28
28
|
|
|
29
29
|
From **mattpocock/skills** — `LICENSES/mattpocock-skills-MIT.txt`:
|
|
30
30
|
|
|
31
|
-
- `plugin/skills/witness-brainstorm/SKILL.md` (← grill-me: one
|
|
31
|
+
- `plugin/skills/witness-brainstorm/SKILL.md` (← grill-me: the whole decision tree, one question at a time with a recommended answer, the codebase explored instead of asking)
|
|
32
32
|
- `plugin/skills/witness-implement/SKILL.md` (← tdd: red/green loop discipline)
|
|
33
33
|
|
|
34
34
|
From **no-mistakes / axi family** (kunchenguid) — `LICENSES/no-mistakes-MIT.txt`:
|
package/README.md
CHANGED
|
@@ -235,7 +235,10 @@ check set has held still for `ship.ciSettleMs`: gh's watch ends the moment nothi
|
|
|
235
235
|
a check that registers late would otherwise be missed. Green means at least one check passed and
|
|
236
236
|
none failed, was cancelled or is pending. Brainstorm and design stay conversations with a human in
|
|
237
237
|
every mode: `/witness` starts drive for the machine work and keeps the human's own items —
|
|
238
|
-
decisions left for them, asks to grill, looks to design — in the chat.
|
|
238
|
+
decisions left for them, asks to grill, looks to design — in the chat. Brainstorm is a grill: one
|
|
239
|
+
plain-words question per turn with up to three ranked answers, the repo explored first, and a local
|
|
240
|
+
draft after every settled answer, so a dead session resumes. Design shows up to three rendered,
|
|
241
|
+
checked directions on one page before it asks which.
|
|
239
242
|
|
|
240
243
|
**`drive`.** `drive.parallelSuites: false` makes test runs in two worktrees take turns, because
|
|
241
244
|
suites that boot servers or browsers collide. A target that reaches any `drive.caps.*` bound is
|
|
@@ -275,6 +278,7 @@ the wrong file refuses. There are no `WITNESS_*` env vars for configuration.
|
|
|
275
278
|
| --- | --- |
|
|
276
279
|
| `init [--agent claude-code\|pi\|auto]` | scaffold config, principles, journal — one trailer commit; `--agent` also installs that harness's engine, guard and dashboard (idempotent) |
|
|
277
280
|
| `recap [--amend] --file <json>` | validate + persist a scope recap — class, goals, non-goals, constraints and numbered decisions; births the effort journal |
|
|
281
|
+
| `draft <key> [--file <json>]` | a grill's or design session's local draft — saved after every settled answer, read back to resume; never committed or journaled |
|
|
278
282
|
| `write <id> --effort <e> --meta <json> --body <md>` | validated manifest → spec/plan on disk + journal entry; a spec's `covers` and `carries` map it to the recap's goals and decisions |
|
|
279
283
|
| `diff <spec-id>` | delta since the last realized state (plan pin → empty), and the decisions that moved since that plan |
|
|
280
284
|
| `read <id> [--design] [--outline] [--lines <a>-<b>] [--decisions]` | print a spec/plan/design from the primary root — the read route, since a worktree carries no canon copy; `--outline` names each anchor's line range for fat artifacts; `--decisions` prints the decisions a spec carries with its effort's constraints and non-goals, or an effort's whole recap |
|
|
@@ -284,7 +288,7 @@ the wrong file refuses. There are no `WITNESS_*` env vars for configuration.
|
|
|
284
288
|
| `log <id>` | render a journal stream |
|
|
285
289
|
| `gate <decompose\|plan\|implement\|ship\|design> <id>` | run the reviewer gate; journals the round, stamps on pass |
|
|
286
290
|
| `decide <gate> <id> --approve\|--revise [--pin <policy>]` | record the human decision on a stopped gate; `--pin` adds a standing content policy |
|
|
287
|
-
| `design <spec-id> --file <html>\|--open\|--reconfirm
|
|
291
|
+
| `design <spec-id> --file <html>\|--open\|--reconfirm\|--preview <html>` | register/show a `ui` spec's approved look; `--preview` opens a page and records nothing |
|
|
288
292
|
| `start <plan-id>` | create/re-attach the plan's worktree (`.witness/worktrees/<id>`) — code only: the canon dirs are excluded from it, so a plan cannot go stale in there |
|
|
289
293
|
| `next` | the one next action across every effort |
|
|
290
294
|
| `drive [--flow <plan-id>] [--max-spawns <n>] [--manual] [--detach]` | runs every session itself · under autonomy it needs no terminal and reports when nothing else can move. Up to `drive.parallel` targets at once, one session per target; usage limits are waited out; a target past a cap is parked. `--detach` (autonomy only) runs it in the background into `.witness/drive.log`; `--manual` runs serially and asks you at every stop. One drive per repo. A session drive spawned is refused — agents are what drive spawns, never what spawns drive |
|
|
@@ -312,7 +316,7 @@ the wrong file refuses. There are no `WITNESS_*` env vars for configuration.
|
|
|
312
316
|
- **The journal is history** — `.witness/journal/<id>.jsonl`, append-only, committed, never compacted.
|
|
313
317
|
- **Every state commit carries `Witness-State: 1`** — `witness check` audits that spec/plan diffs appear only in trailer-bearing commits.
|
|
314
318
|
- **Canon roots are configurable** — `paths: { specs: docs/specs, plans: docs/plans }` in `witness.config.yaml` (defaults: `specs/`, `plans/`); scan, commit scoping, the guard hook, and criteria excludes all follow it. `git mv` existing docs when changing it.
|
|
315
|
-
- Local, never committed: `.witness/{lock,txn.json,allow.json,calibration.local.yaml}`.
|
|
319
|
+
- Local, never committed: `.witness/{lock,txn.json,allow.json,calibration.local.yaml,drafts/}`.
|
|
316
320
|
|
|
317
321
|
## Development
|
|
318
322
|
|
package/dist/ask.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { draftLine, dropDraft } from './drafts.js';
|
|
1
2
|
import { FLOOR_STREAM } from './floor.js';
|
|
2
3
|
import { stateCommit } from './gitio.js';
|
|
3
4
|
import { appendEntry, effortStreams, entryLine, journalRel, readStream } from './journal.js';
|
|
@@ -127,15 +128,20 @@ export function withdrawAsk(ctx, root, id, note) {
|
|
|
127
128
|
});
|
|
128
129
|
if (!res.ok)
|
|
129
130
|
return res;
|
|
131
|
+
// Row 199. The conversation that would have answered it is over; its draft goes with it.
|
|
132
|
+
dropDraft(root, { kind: 'ask', id });
|
|
130
133
|
return ok({ already: false });
|
|
131
134
|
}
|
|
132
135
|
finally {
|
|
133
136
|
lock.value();
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
|
-
// The one derivation of which ask is routed next: the oldest still open
|
|
139
|
+
// The one derivation of which ask is routed next: the oldest still open — except that an ask whose
|
|
140
|
+
// grill has a draft comes first, the oldest drafted ask among several (row 199: finish what you
|
|
141
|
+
// started). A draft that does not read gives no priority.
|
|
137
142
|
export function nextAsk(root) {
|
|
138
|
-
|
|
143
|
+
const open = askQueue(root).filter((a) => a.state === 'unanswered');
|
|
144
|
+
return open.find((a) => draftLine(root, { kind: 'ask', id: a.id }) !== undefined) ?? open[0];
|
|
139
145
|
}
|
|
140
146
|
// The ask an effort answers: what the decompose arbiter judges a slicing against beside the
|
|
141
147
|
// recap itself (row 177). Named by the recap that birthed the effort — an amend carries no
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,8 @@ const VERBS = {
|
|
|
15
15
|
recap: () => import('./verbs/recap.js'),
|
|
16
16
|
// Row 176. The human's ask, queued as state so `next` routes it and no session can lose it.
|
|
17
17
|
ask: () => import('./verbs/ask.js'),
|
|
18
|
+
// Row 199. A conversation's local draft: saved after every settled answer, never committed.
|
|
19
|
+
draft: () => import('./verbs/draft.js'),
|
|
18
20
|
write: () => import('./verbs/write.js'),
|
|
19
21
|
// Row 132's read route. Canon lives at the primary root and nowhere else, so reading it
|
|
20
22
|
// is a verb rather than a path — which is also what lets a worktree carry no copy.
|
|
@@ -63,9 +65,10 @@ const VERB_USAGE = {
|
|
|
63
65
|
calibrate: 'witness calibrate <exact-model-id> [--suite all|reviewers|skills] [--only <name>] [--samples <n>] [--publish]',
|
|
64
66
|
check: 'witness check',
|
|
65
67
|
clean: 'witness clean',
|
|
68
|
+
draft: 'witness draft <ask-<id>|amend-<effort>|design-<spec-id>> [--file <json>]',
|
|
66
69
|
decide: 'witness decide <gate> <target> --approve|--revise|--stop [--override] [--repair] [--note <t>] [--upstream <artifact|effort>] [--pin <policy>]… [--via affirmation] [--show]',
|
|
67
70
|
floor: 'witness floor --show | --set <triple> --note <why>',
|
|
68
|
-
design: 'witness design <spec-id> --file <html> | --reconfirm | --open',
|
|
71
|
+
design: 'witness design <spec-id> --file <html> | --reconfirm | --open | --preview <html>',
|
|
69
72
|
diff: 'witness diff <spec-id>',
|
|
70
73
|
drive: 'witness drive [--flow <plan-id>] [--max-spawns <n>] [--manual] [--detach] — runs every session itself · under autonomy it needs no terminal and reports when nothing else can move',
|
|
71
74
|
'dispatch-report': 'witness dispatch-report <plan-id> --steps-assigned <n> --steps-completed <n> [--tokens <n>] [--tool-uses <n>] [--duration-ms <n>]',
|
package/dist/drafts.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ID_RE } from './dsl.js';
|
|
4
|
+
import { idTextList } from './idlists.js';
|
|
5
|
+
import { ok, refuse, v } from './refusal.js';
|
|
6
|
+
import { ensureExcluded } from './worktree.js';
|
|
7
|
+
// Row 199. A conversation's local draft: the grill's settled answers, or the design session's
|
|
8
|
+
// direction, answers and page, saved after every settled answer so a session that dies, hits its
|
|
9
|
+
// limit or relays loses nothing. Local, never committed and never journaled: the recap or the
|
|
10
|
+
// stamped look is the record, and a draft is only the memory of the conversation that reaches it.
|
|
11
|
+
// Keyed by what the conversation is about. This module reaches no ask.ts, scan.ts or verb, so
|
|
12
|
+
// each of those may import it.
|
|
13
|
+
export const DRAFTS_DIR = '.witness/drafts';
|
|
14
|
+
export function parseKey(key) {
|
|
15
|
+
const ask = /^ask-(a[0-9]+)$/.exec(key);
|
|
16
|
+
if (ask)
|
|
17
|
+
return { kind: 'ask', id: ask[1] };
|
|
18
|
+
for (const kind of ['amend', 'design']) {
|
|
19
|
+
const rest = key.startsWith(`${kind}-`) ? key.slice(kind.length + 1) : undefined;
|
|
20
|
+
if (rest === undefined || !ID_RE.test(rest))
|
|
21
|
+
continue;
|
|
22
|
+
return kind === 'amend' ? { kind, effort: rest } : { kind, spec: rest };
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
export function keyName(k) {
|
|
27
|
+
return k.kind === 'ask' ? `ask-${k.id}` : k.kind === 'amend' ? `amend-${k.effort}` : `design-${k.spec}`;
|
|
28
|
+
}
|
|
29
|
+
export const draftRel = (k) => `${DRAFTS_DIR}/${keyName(k)}.json`;
|
|
30
|
+
const isObject = (x) => typeof x === 'object' && x !== null && !Array.isArray(x);
|
|
31
|
+
const unreadable = (k) => refuse([v('key', 'draft-unreadable', draftRel(k), 'a draft witness wrote — save over it with --file')]);
|
|
32
|
+
// ok(undefined) is "none saved". A file that does not parse as a stored draft is refused, never
|
|
33
|
+
// read as empty: the draft is a resumed session's only memory.
|
|
34
|
+
export function readDraft(root, k) {
|
|
35
|
+
const path = join(root, draftRel(k));
|
|
36
|
+
if (!existsSync(path))
|
|
37
|
+
return ok(undefined);
|
|
38
|
+
let raw;
|
|
39
|
+
try {
|
|
40
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return unreadable(k);
|
|
44
|
+
}
|
|
45
|
+
if (!isObject(raw) || raw.v !== 1 || !isObject(raw.draft))
|
|
46
|
+
return unreadable(k);
|
|
47
|
+
const read = Array.isArray(raw.read) ? raw.read.filter((x) => typeof x === 'string') : [];
|
|
48
|
+
return ok({
|
|
49
|
+
v: 1, key: keyName(k), draft: raw.draft,
|
|
50
|
+
...(typeof raw.session === 'string' ? { session: raw.session } : {}),
|
|
51
|
+
...(read.length ? { read } : {}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const writeStored = (root, k, stored) => {
|
|
55
|
+
const path = join(root, draftRel(k));
|
|
56
|
+
writeFileSync(`${path}.tmp`, `${JSON.stringify(stored, null, 2)}\n`);
|
|
57
|
+
renameSync(`${path}.tmp`, path);
|
|
58
|
+
};
|
|
59
|
+
// Written aside and renamed into place, so a crash never leaves half a draft. The exclusion
|
|
60
|
+
// first: a repo whose .gitignore predates drafts must never show one to `git status`.
|
|
61
|
+
export function saveDraft(root, k, draft, session) {
|
|
62
|
+
ensureExcluded(root);
|
|
63
|
+
mkdirSync(join(root, DRAFTS_DIR), { recursive: true });
|
|
64
|
+
writeStored(root, k, { v: 1, key: keyName(k), ...(session !== undefined ? { session } : {}), draft });
|
|
65
|
+
}
|
|
66
|
+
// A read-back by a session other than the saver records it: that session has now seen every answer
|
|
67
|
+
// in this content, so its next save may drop or reword one. Not an act (row 191 reads `session`
|
|
68
|
+
// only), and a new save clears the list, because its content is unseen again.
|
|
69
|
+
export function markRead(root, k, stored, session) {
|
|
70
|
+
if (stored.session === undefined || stored.session === session || stored.read?.includes(session))
|
|
71
|
+
return;
|
|
72
|
+
writeStored(root, k, { ...stored, read: [...(stored.read ?? []), session] });
|
|
73
|
+
}
|
|
74
|
+
export function dropDraft(root, k) {
|
|
75
|
+
rmSync(join(root, draftRel(k)), { force: true });
|
|
76
|
+
}
|
|
77
|
+
// Row 191 reads these beside the journal: a settled answer saved is the session's stage work.
|
|
78
|
+
export function draftSessions(root) {
|
|
79
|
+
const dir = join(root, DRAFTS_DIR);
|
|
80
|
+
if (!existsSync(dir))
|
|
81
|
+
return [];
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const f of readdirSync(dir).filter((n) => n.endsWith('.json'))) {
|
|
84
|
+
const k = parseKey(f.slice(0, -'.json'.length));
|
|
85
|
+
if (k === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
const d = readDraft(root, k);
|
|
88
|
+
if (d.ok && d.value?.session !== undefined)
|
|
89
|
+
out.push(d.value.session);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
const GRILL_FIELDS = ['effort', 'class', 'goals', 'non_goals', 'constraints', 'decisions', 'slices', 'open'];
|
|
94
|
+
const DESIGN_FIELDS = ['direction', 'answers', 'open', 'page'];
|
|
95
|
+
const ID_LISTS = [['goals', 'g'], ['non_goals', 'n'], ['constraints', 'c'], ['decisions', 'd']];
|
|
96
|
+
const stringList = (raw, field) => raw === undefined || (Array.isArray(raw) && raw.every((s) => typeof s === 'string' && s.trim() !== ''))
|
|
97
|
+
? []
|
|
98
|
+
: [v(field, 'shape', JSON.stringify(raw), 'list of non-empty strings')];
|
|
99
|
+
// The shape, never completeness: a draft is the recap so far. An unknown field is refused, because
|
|
100
|
+
// a field dropped without a word would be a lost answer.
|
|
101
|
+
export function validateDraft(k, raw) {
|
|
102
|
+
if (!isObject(raw)) {
|
|
103
|
+
return [v('draft', 'shape', Array.isArray(raw) ? 'array' : raw === null ? 'null' : typeof raw, 'JSON object')];
|
|
104
|
+
}
|
|
105
|
+
const allowed = k.kind === 'design' ? DESIGN_FIELDS : GRILL_FIELDS;
|
|
106
|
+
const out = Object.keys(raw).filter((f) => !allowed.includes(f))
|
|
107
|
+
.map((f) => v(f, 'unknown-field', f, `one of ${allowed.join(' ')}`));
|
|
108
|
+
if (k.kind === 'design') {
|
|
109
|
+
if (raw.direction !== undefined && (typeof raw.direction !== 'string' || raw.direction.trim() === '')) {
|
|
110
|
+
out.push(v('direction', 'shape', JSON.stringify(raw.direction), 'non-empty string'));
|
|
111
|
+
}
|
|
112
|
+
out.push(...stringList(raw.answers, 'answers'), ...stringList(raw.open, 'open'));
|
|
113
|
+
if (raw.page !== undefined && typeof raw.page !== 'string')
|
|
114
|
+
out.push(v('page', 'shape', typeof raw.page, 'string'));
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
if (raw.effort !== undefined) {
|
|
118
|
+
if (typeof raw.effort !== 'string' || !ID_RE.test(raw.effort)) {
|
|
119
|
+
out.push(v('effort', 'id-charset', String(raw.effort), '[a-z0-9-]+'));
|
|
120
|
+
}
|
|
121
|
+
else if (k.kind === 'amend' && raw.effort !== k.effort) {
|
|
122
|
+
out.push(v('effort', 'effort-mismatch', raw.effort, `${k.effort} — the effort this draft amends`));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (raw.class !== undefined && raw.class !== 'feature' && raw.class !== 'fix' && raw.class !== 'chore') {
|
|
126
|
+
out.push(v('class', 'enum', String(raw.class), 'feature | fix | chore'));
|
|
127
|
+
}
|
|
128
|
+
for (const [field, prefix] of ID_LISTS)
|
|
129
|
+
out.push(...idTextList(raw[field], field, prefix, false));
|
|
130
|
+
out.push(...stringList(raw.slices, 'slices'), ...stringList(raw.open, 'open'));
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
const list = (x) => (Array.isArray(x) ? x : []);
|
|
134
|
+
// What a save by a session that has not seen this draft must keep of it (Builder's call 11), by
|
|
135
|
+
// content: two chats both number their first answer g1, so an id alone cannot tell their answers
|
|
136
|
+
// apart. A grill's items are its four lists' `id: text`; a look's are its chosen direction, each
|
|
137
|
+
// settled answer, and whether a page is saved (the page itself changes with every save).
|
|
138
|
+
export function settledItems(k, draft) {
|
|
139
|
+
if (k.kind === 'design') {
|
|
140
|
+
return [
|
|
141
|
+
...(typeof draft.direction === 'string' && draft.direction !== '' ? [`direction: ${draft.direction}`] : []),
|
|
142
|
+
...list(draft.answers).filter((a) => typeof a === 'string').map((a) => `answer: ${a}`),
|
|
143
|
+
...(typeof draft.page === 'string' && draft.page !== '' ? ['page'] : []),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
return ID_LISTS.flatMap(([field]) => list(draft[field]))
|
|
147
|
+
.map((e) => (isObject(e) && typeof e.id === 'string' ? `${e.id}: ${String(e.text)}` : undefined))
|
|
148
|
+
.filter((item) => item !== undefined);
|
|
149
|
+
}
|
|
150
|
+
const count = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
151
|
+
export function draftSummary(k, draft) {
|
|
152
|
+
if (k.kind === 'design') {
|
|
153
|
+
const chosen = typeof draft.direction === 'string' && draft.direction !== '';
|
|
154
|
+
const page = typeof draft.page === 'string' && draft.page !== '';
|
|
155
|
+
return `direction ${chosen ? 'chosen' : 'open'} · ${count(list(draft.answers).length, 'answer')} settled · page ${page ? 'saved' : 'none'}`;
|
|
156
|
+
}
|
|
157
|
+
const items = ID_LISTS.reduce((n, [field]) => n + list(draft[field]).length, 0);
|
|
158
|
+
const open = list(draft.open).length;
|
|
159
|
+
return `${count(items, 'item')} settled${open > 0 ? ` · ${open} open` : ''}`;
|
|
160
|
+
}
|
|
161
|
+
// `next`'s line for a conversation that has a draft. None, or one that does not read, prints
|
|
162
|
+
// nothing: an unreadable draft is refused by name where it is read back, never here.
|
|
163
|
+
export function draftLine(root, k) {
|
|
164
|
+
const d = readDraft(root, k);
|
|
165
|
+
if (!d.ok || d.value === undefined)
|
|
166
|
+
return undefined;
|
|
167
|
+
return `${draftSummary(k, d.value.draft)} · resume with witness draft ${keyName(k)}`;
|
|
168
|
+
}
|
package/dist/gitio.js
CHANGED
|
@@ -110,6 +110,11 @@ const LOCAL_STATE_FILES = new Set([
|
|
|
110
110
|
// declared extensions exist to unblock.
|
|
111
111
|
'.witness/config.local.yaml',
|
|
112
112
|
]);
|
|
113
|
+
// Row 199. A conversation's drafts are local by directory, as row 90's machine config is by file,
|
|
114
|
+
// and the exemption is load-bearing for the same reason: init writes its .gitignore block only
|
|
115
|
+
// when scaffolding, so on every repo older than the row a draft is untracked-but-visible under a
|
|
116
|
+
// state dir, and an unrelated-dirty refusal there would block the very recap the grill lands.
|
|
117
|
+
const LOCAL_STATE_DIRS = ['.witness/drafts/'];
|
|
113
118
|
export function dirtyStatePaths(root) {
|
|
114
119
|
const res = tryGit(root, 'status', '--porcelain', '--untracked-files=all', '--', ...stateDirs(root));
|
|
115
120
|
if (!res.ok || res.out === '')
|
|
@@ -119,7 +124,7 @@ export function dirtyStatePaths(root) {
|
|
|
119
124
|
// a rename (e.g. from `git mv`) porcelain-formats as "old -> new" on one line —
|
|
120
125
|
// split it into its two paths so each is checked against the caller's planned set
|
|
121
126
|
.flatMap((l) => l.slice(3).replace(/^"|"$/g, '').split(' -> '))
|
|
122
|
-
.filter((p) => !LOCAL_STATE_FILES.has(p));
|
|
127
|
+
.filter((p) => !LOCAL_STATE_FILES.has(p) && !LOCAL_STATE_DIRS.some((d) => p.startsWith(d)));
|
|
123
128
|
}
|
|
124
129
|
export const TRAILER = 'Witness-State: 1';
|
|
125
130
|
// Row 87's rule: nothing to COMMIT is the same legal silent success as nothing to write.
|
package/dist/idlists.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { v } from './refusal.js';
|
|
2
|
+
// A recap's `{id, text}` lists, checked one way wherever one is read: the recap itself and a
|
|
3
|
+
// conversation's draft (row 199). Its own module so drafts.ts never imports a verb — through
|
|
4
|
+
// recap.ts that would close drafts → recap → ask → drafts.
|
|
5
|
+
export function idTextList(raw, field, prefix, required) {
|
|
6
|
+
if (raw === undefined)
|
|
7
|
+
return required ? [v(field, 'required', 'absent', `>=1 {id, text} with ids ${prefix}1..`)] : [];
|
|
8
|
+
if (!Array.isArray(raw))
|
|
9
|
+
return [v(field, 'shape', typeof raw, 'list of {id, text}')];
|
|
10
|
+
if (required && raw.length === 0)
|
|
11
|
+
return [v(field, 'required', 'empty', `>=1 {id, text} with ids ${prefix}1..`)];
|
|
12
|
+
const out = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
raw.forEach((item, i) => {
|
|
15
|
+
const at = `${field}[${i}]`;
|
|
16
|
+
const e = (typeof item === 'object' && item !== null ? item : {});
|
|
17
|
+
if (typeof e.id !== 'string' || !new RegExp(`^${prefix}[0-9]+$`).test(e.id)) {
|
|
18
|
+
out.push(v(`${at}.id`, 'id-prefix', String(e.id ?? 'absent'), `${prefix}<number>`));
|
|
19
|
+
}
|
|
20
|
+
else if (seen.has(e.id)) {
|
|
21
|
+
out.push(v(`${at}.id`, 'id-unique', e.id, `unique within ${field}`));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
seen.add(e.id);
|
|
25
|
+
}
|
|
26
|
+
if (typeof e.text !== 'string' || e.text.trim() === '') {
|
|
27
|
+
out.push(v(`${at}.text`, 'required', String(e.text ?? 'absent'), 'non-empty text'));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return out;
|
|
31
|
+
}
|
package/dist/slice.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from './config.js';
|
|
2
2
|
import { pctOf, reached, readFill, sessionHash, sessionIdOf, windowLabel } from './context.js';
|
|
3
|
+
import { draftSessions } from './drafts.js';
|
|
3
4
|
import { primaryRoot } from './gitio.js';
|
|
4
5
|
import { relayLine, resolveDriver } from './harness.js';
|
|
5
6
|
import { readStream, streamIds } from './journal.js';
|
|
@@ -19,8 +20,11 @@ const stampOf = (e) => typeof e.ctx === 'object' && e.ctx !== null ? e.ctx : {};
|
|
|
19
20
|
// a fresh session from handing over before it has done anything. A vacuous red is no act (final
|
|
20
21
|
// review I3): it observed the behavior already there, so a session opened over the limit that only
|
|
21
22
|
// re-ran one would hand over, and so would every fresh session after it.
|
|
23
|
+
// Row 199: a settled answer saved to a draft is stage work too — the grill's and the design
|
|
24
|
+
// session's boundary — so it counts beside the journal. Reading a draft back is not: a relayed
|
|
25
|
+
// session that has only resumed has done nothing yet.
|
|
22
26
|
export function sessionActed(root, hash) {
|
|
23
|
-
return streamIds(root).some((id) => readStream(root, id).some((e) => ACT_KINDS.has(e.t) && !vacuousEvidence(e) && stampOf(e).session === hash));
|
|
27
|
+
return draftSessions(root).includes(hash) || streamIds(root).some((id) => readStream(root, id).some((e) => ACT_KINDS.has(e.t) && !vacuousEvidence(e) && stampOf(e).session === hash));
|
|
24
28
|
}
|
|
25
29
|
// The entries that closed a step, in stream order. A legacy plan's closes are not the CLI's to
|
|
26
30
|
// see, so it has none.
|
package/dist/verbs/decide.js
CHANGED
|
@@ -3,6 +3,7 @@ import { EXIT } from '../cli.js';
|
|
|
3
3
|
import { grantCommands, untrustedCmdsFor } from '../allowlist.js';
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { designUnseen } from '../design.js';
|
|
6
|
+
import { dropDraft } from '../drafts.js';
|
|
6
7
|
import '../gates/index.js';
|
|
7
8
|
import { gateSpec, liveExits, renderGateRun } from '../gate.js';
|
|
8
9
|
import { newDeferralId } from '../deferral.js';
|
|
@@ -642,6 +643,12 @@ export async function applyDecision(ctx, act) {
|
|
|
642
643
|
finally {
|
|
643
644
|
lockR.value();
|
|
644
645
|
}
|
|
646
|
+
// Row 200 (Builder's call 5). The look is stamped, so the design session that chose it is over:
|
|
647
|
+
// its draft goes now and not when the look registered, because a behavior note at the approval
|
|
648
|
+
// stop re-opens the look and must find the direction and answers still there. D154's rule below:
|
|
649
|
+
// after the decision is durable, never before.
|
|
650
|
+
if (gate === 'design' && entry.decision === 'approve')
|
|
651
|
+
dropDraft(root, { kind: 'design', spec: target });
|
|
645
652
|
// D154. After the decision is durable, never before: a refused or crashed decide must
|
|
646
653
|
// not leave a grant behind that nobody chose.
|
|
647
654
|
if (trustCmds) {
|
|
@@ -686,7 +693,7 @@ export async function applyDecision(ctx, act) {
|
|
|
686
693
|
ctx.out(kv('upstream', `${upstream} is owned by effort ${entry.upstream.artifact} — the decompose reopen is booked there`));
|
|
687
694
|
}
|
|
688
695
|
if (gate === 'decompose')
|
|
689
|
-
ctx.out(`help: scope itself implicated → witness recap --amend
|
|
696
|
+
ctx.out(`help: scope itself implicated → witness draft amend-${target} seeds the amendment from the latest recap · witness recap --amend --file <recap.json> lands it`);
|
|
690
697
|
else if (entry.upstream)
|
|
691
698
|
ctx.out(`help: reopened ${entry.upstream.artifact} (${entry.upstream.gate} stage) — linked via caused_by`);
|
|
692
699
|
}
|
package/dist/verbs/design.js
CHANGED
|
@@ -5,6 +5,7 @@ import { parseArgs } from 'node:util';
|
|
|
5
5
|
import { EXIT } from '../cli.js';
|
|
6
6
|
import { loadConfig, loadLocalConfig } from '../config.js';
|
|
7
7
|
import { designPending, designRel, designStamp, htmlSha, validateDesignArtifact } from '../design.js';
|
|
8
|
+
import { dropDraft } from '../drafts.js';
|
|
8
9
|
import { writeDoc } from '../fm.js';
|
|
9
10
|
import { dirtyStatePaths, primaryRoot, stateCommit } from '../gitio.js';
|
|
10
11
|
import { appendEntry, entryLine, journalRel, latestRecap } from '../journal.js';
|
|
@@ -19,11 +20,11 @@ import { crashPoint, guardTxn, withTxn } from '../txn.js';
|
|
|
19
20
|
export async function run(ctx, argv) {
|
|
20
21
|
const { values, positionals } = parseArgs({
|
|
21
22
|
args: argv, allowPositionals: true,
|
|
22
|
-
options: { file: { type: 'string' }, reconfirm: { type: 'boolean' }, open: { type: 'boolean' } },
|
|
23
|
+
options: { file: { type: 'string' }, reconfirm: { type: 'boolean' }, open: { type: 'boolean' }, preview: { type: 'string' } },
|
|
23
24
|
});
|
|
24
25
|
const specId = positionals[0];
|
|
25
26
|
if (!specId) {
|
|
26
|
-
ctx.err('usage: witness design <spec-id> --file <html> | --reconfirm | --open');
|
|
27
|
+
ctx.err('usage: witness design <spec-id> --file <html> | --reconfirm | --open | --preview <html>');
|
|
27
28
|
return EXIT.REFUSED;
|
|
28
29
|
}
|
|
29
30
|
const rootR = primaryRoot(ctx.cwd);
|
|
@@ -48,6 +49,8 @@ export async function run(ctx, argv) {
|
|
|
48
49
|
}
|
|
49
50
|
if (values.open)
|
|
50
51
|
return openOnly(ctx, root, designRel(cfgR.value.paths, specId), specId);
|
|
52
|
+
if (values.preview !== undefined)
|
|
53
|
+
return previewOnly(ctx, root, specId, values.preview);
|
|
51
54
|
if (spec.meta.ui !== true) {
|
|
52
55
|
renderRefusal([v('spec', 'not-ui', specId, 'a ui-flagged spec — set ui: true at decompose to earn a design stage')]).forEach(ctx.err);
|
|
53
56
|
return EXIT.REFUSED;
|
|
@@ -74,7 +77,34 @@ export async function run(ctx, argv) {
|
|
|
74
77
|
lock.value();
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
|
-
// The
|
|
80
|
+
// Row 200. The design session shows before it asks: its directions page, and the full look before
|
|
81
|
+
// it registers, open through the same opener as --open (`opener:`, row 90). A preview takes no
|
|
82
|
+
// lock and records nothing: `design-shown` is the witness that registered bytes were seen (row 72),
|
|
83
|
+
// and these bytes are not registered.
|
|
84
|
+
function previewOnly(ctx, root, id, file) {
|
|
85
|
+
const abs = resolve(ctx.cwd, file);
|
|
86
|
+
if (!existsSync(abs)) {
|
|
87
|
+
renderRefusal([v('--preview', 'input-unreadable', abs, 'a readable HTML file')]).forEach(ctx.err);
|
|
88
|
+
return EXIT.REFUSED;
|
|
89
|
+
}
|
|
90
|
+
const localR = loadLocalConfig(root);
|
|
91
|
+
if (!localR.ok) {
|
|
92
|
+
renderRefusal(localR.violations).forEach(ctx.err);
|
|
93
|
+
return EXIT.REFUSED;
|
|
94
|
+
}
|
|
95
|
+
const { outcome, command } = openArtifact(localR.value.opener, abs);
|
|
96
|
+
if (outcome === 'failed') {
|
|
97
|
+
renderRefusal([v('design', 'opener-failed', `${command} did not resolve`, `a working platform opener, or opener: in .witness/config.local.yaml — meanwhile open it yourself: file://${abs}`)]).forEach(ctx.err);
|
|
98
|
+
return EXIT.REFUSED;
|
|
99
|
+
}
|
|
100
|
+
ctx.out(kv('previewed', id));
|
|
101
|
+
ctx.out(kv('path', abs));
|
|
102
|
+
ctx.out(kv('opener', command));
|
|
103
|
+
ctx.out(kv('note', `a preview registers nothing — witness design ${id} --file <html> registers the look`));
|
|
104
|
+
return EXIT.OK;
|
|
105
|
+
}
|
|
106
|
+
// The one place a registered look is ever spawned at a human (a preview, above, shows bytes that
|
|
107
|
+
// are not registered and witnesses nothing). Journals a sha-keyed witness that
|
|
78
108
|
// `gate design`, `decide --approve` and `next` all consult. Deliberately reachable for
|
|
79
109
|
// any registered artifact — re-showing a file that exists needs no ui/feature eligibility,
|
|
80
110
|
// and gating it on those would dead-end the very refusals that point here.
|
|
@@ -202,6 +232,9 @@ function reconfirm(ctx, root, spec, specSha) {
|
|
|
202
232
|
renderRefusal(res.violations).forEach(ctx.err);
|
|
203
233
|
return EXIT.REFUSED;
|
|
204
234
|
}
|
|
235
|
+
// Row 200. Reconfirming stamps the look as an approve does, so the amend-mode session that chose
|
|
236
|
+
// it is over and its draft goes too — once the stamp is committed.
|
|
237
|
+
dropDraft(root, { kind: 'design', spec: id });
|
|
205
238
|
ctx.out(kv('reconfirmed', id));
|
|
206
239
|
ctx.out(kv('design', `re-pinned to ${short(specSha)} (artifact unchanged)`));
|
|
207
240
|
return EXIT.OK;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { EXIT } from '../cli.js';
|
|
5
|
+
import { journalStamp } from '../context.js';
|
|
6
|
+
import { draftSummary, dropDraft, keyName, markRead, parseKey, readDraft, saveDraft, settledItems, validateDraft, } from '../drafts.js';
|
|
7
|
+
import { primaryRoot } from '../gitio.js';
|
|
8
|
+
import { latestRecap } from '../journal.js';
|
|
9
|
+
import { renderRefusal, v } from '../refusal.js';
|
|
10
|
+
import { findById, loadCanon } from '../scan.js';
|
|
11
|
+
import { kv } from '../toon.js';
|
|
12
|
+
import { askRefusal, effortRefusal } from './recap.js';
|
|
13
|
+
// Row 199. A grill's or design session's local draft, saved after every settled answer and read
|
|
14
|
+
// back to resume. Drafts are not state: no lock, no txn guard, no commit, no journal line. The
|
|
15
|
+
// saver's session rides in the file, which is what lets row 191 count a save as an act.
|
|
16
|
+
export async function run(ctx, argv) {
|
|
17
|
+
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { file: { type: 'string' } } });
|
|
18
|
+
const raw = positionals[0];
|
|
19
|
+
if (!raw) {
|
|
20
|
+
ctx.err('usage: witness draft <ask-<id>|amend-<effort>|design-<spec-id>> [--file <json>]');
|
|
21
|
+
return EXIT.REFUSED;
|
|
22
|
+
}
|
|
23
|
+
const refused = (violations) => { renderRefusal(violations).forEach(ctx.err); return EXIT.REFUSED; };
|
|
24
|
+
const rootR = primaryRoot(ctx.cwd);
|
|
25
|
+
if (!rootR.ok)
|
|
26
|
+
return refused(rootR.violations);
|
|
27
|
+
const root = rootR.value;
|
|
28
|
+
const k = parseKey(raw);
|
|
29
|
+
if (k === undefined)
|
|
30
|
+
return refused([v('key', 'bad-key', raw, 'ask-<id> | amend-<effort> | design-<spec-id>')]);
|
|
31
|
+
const referent = referentRefusal(root, k);
|
|
32
|
+
if (referent !== undefined)
|
|
33
|
+
return refused([referent]);
|
|
34
|
+
const stored = readDraft(root, k);
|
|
35
|
+
const session = journalStamp(ctx.env)?.session;
|
|
36
|
+
if (values.file === undefined) {
|
|
37
|
+
if (!stored.ok)
|
|
38
|
+
return refused(stored.violations);
|
|
39
|
+
if (stored.value === undefined) {
|
|
40
|
+
if (k.kind !== 'amend')
|
|
41
|
+
return refused([v('key', 'no-draft', keyName(k), `a saved draft — witness draft ${keyName(k)} --file <json>`)]);
|
|
42
|
+
// Builder's call 8: an amendment starts from the latest recap, every text exact, because a
|
|
43
|
+
// decision's text is part of the pin of every plan that carries it.
|
|
44
|
+
const r = latestRecap(root, k.effort);
|
|
45
|
+
ctx.out(kv('draft', `${keyName(k)} · seeded from the latest recap`));
|
|
46
|
+
const seed = {
|
|
47
|
+
effort: r.effort, class: r.class, goals: r.goals, non_goals: r.non_goals, constraints: r.constraints,
|
|
48
|
+
...(r.decisions !== undefined ? { decisions: r.decisions } : {}), slices: r.slices,
|
|
49
|
+
};
|
|
50
|
+
JSON.stringify(seed, null, 2).split('\n').forEach((l) => ctx.out(l));
|
|
51
|
+
return EXIT.OK;
|
|
52
|
+
}
|
|
53
|
+
ctx.out(kv('draft', `${keyName(k)} · ${draftSummary(k, stored.value.draft)}`));
|
|
54
|
+
JSON.stringify(stored.value.draft, null, 2).split('\n').forEach((l) => ctx.out(l));
|
|
55
|
+
if (session !== undefined)
|
|
56
|
+
markRead(root, k, stored.value, session);
|
|
57
|
+
return EXIT.OK;
|
|
58
|
+
}
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(readFileSync(resolve(ctx.cwd, values.file), 'utf8'));
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return refused([v('--file', 'input-json', String(e.message).slice(0, 120), 'readable JSON file')]);
|
|
65
|
+
}
|
|
66
|
+
const violations = validateDraft(k, parsed);
|
|
67
|
+
if (violations.length)
|
|
68
|
+
return refused(violations);
|
|
69
|
+
const draft = parsed;
|
|
70
|
+
// Builder's call 11. Two chats routed into one conversation would otherwise overwrite each
|
|
71
|
+
// other's answers unseen. A session that saved this draft, or read it back since, has seen every
|
|
72
|
+
// answer in it, so it may drop or reword one — that is the human's call in its chat.
|
|
73
|
+
const seen = stored.ok && (stored.value?.session === session || stored.value?.read?.includes(session ?? '') === true);
|
|
74
|
+
if (stored.ok && stored.value?.session !== undefined && session !== undefined && !seen) {
|
|
75
|
+
const kept = new Set(settledItems(k, draft));
|
|
76
|
+
const lost = settledItems(k, stored.value.draft).filter((i) => !kept.has(i));
|
|
77
|
+
if (lost.length) {
|
|
78
|
+
return refused([v('key', 'draft-moved', `${keyName(k)} would drop ${lost.join(' · ')}`, `this draft holds answers another session saved — read it back (witness draft ${keyName(k)}) and continue from it`)]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
saveDraft(root, k, draft, session);
|
|
82
|
+
ctx.out(kv('draft', `${keyName(k)} — saved · ${draftSummary(k, draft)}`));
|
|
83
|
+
return EXIT.OK;
|
|
84
|
+
}
|
|
85
|
+
// Builder's call 13. A draft's key refuses what its recap would, by the recap's own rules: a draft
|
|
86
|
+
// for an ask no recap may answer, or an effort no amendment may land on, is a conversation with no
|
|
87
|
+
// way to end. An answered or withdrawn ask's stale draft is removed on the way.
|
|
88
|
+
function referentRefusal(root, k) {
|
|
89
|
+
const asKey = (x) => ({ ...x, field: 'key' });
|
|
90
|
+
if (k.kind === 'ask') {
|
|
91
|
+
const refusal = askRefusal(root, k.id);
|
|
92
|
+
if (refusal !== undefined && refusal.rule !== 'unknown-ask')
|
|
93
|
+
dropDraft(root, k);
|
|
94
|
+
return refusal === undefined ? undefined : asKey(refusal);
|
|
95
|
+
}
|
|
96
|
+
if (k.kind === 'amend') {
|
|
97
|
+
const refusal = effortRefusal(root, k.effort, true);
|
|
98
|
+
if (refusal !== undefined)
|
|
99
|
+
return asKey(refusal);
|
|
100
|
+
return latestRecap(root, k.effort) === undefined
|
|
101
|
+
? v('key', 'unknown-effort', k.effort, 'an effort with a recap to amend')
|
|
102
|
+
: undefined;
|
|
103
|
+
}
|
|
104
|
+
return findById(loadCanon(root), k.spec)?.meta.type === 'spec'
|
|
105
|
+
? undefined
|
|
106
|
+
: v('key', 'unknown-spec', k.spec, 'a specs/ doc id');
|
|
107
|
+
}
|
package/dist/verbs/init.js
CHANGED
package/dist/verbs/next.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { EXIT } from '../cli.js';
|
|
4
4
|
import { askText, nextAsk } from '../ask.js';
|
|
5
|
+
import { draftLine } from '../drafts.js';
|
|
5
6
|
import { unmetNeeds } from '../needs.js';
|
|
6
7
|
import { loadConfig } from '../config.js';
|
|
7
8
|
import { pendingTxn } from '../txn.js';
|
|
@@ -705,7 +706,12 @@ export function computeNext(root, ctx, canon, cfg, opts = {}) {
|
|
|
705
706
|
// the endgame decision itself is the next action — decisions outrank motion, jammed
|
|
706
707
|
// targets must not be silently skipped. A parked gate is excluded: the human already made
|
|
707
708
|
// the endgame decision.
|
|
708
|
-
|
|
709
|
+
// Row 200 (§7). A behavior note at the look's approval stop starts an amendment of the spec's
|
|
710
|
+
// effort while its design decision is still pending. The drafted amendment comes first: the look
|
|
711
|
+
// is being re-specified, so approving it now would stamp the old one and end its draft. Only
|
|
712
|
+
// where the walk admits the effort, so the amendment the rung below returns is reachable.
|
|
713
|
+
const amendingLook = (o) => o.gate === 'design' && efforts.some((e) => admits(e.slug) && effortWrites(root, e.slug).has(o.target) && draftLine(root, { kind: 'amend', effort: e.slug }) !== undefined);
|
|
714
|
+
const owed = owedFrom(root, efforts, specs, plans).find((o) => admits(o.target) && !amendingLook(o));
|
|
709
715
|
if (owed !== undefined) {
|
|
710
716
|
if (!owed.bound)
|
|
711
717
|
return { line: `witness decide ${owed.gate} ${owed.target} --show`, target: owed.target };
|
|
@@ -718,10 +724,19 @@ export function computeNext(root, ctx, canon, cfg, opts = {}) {
|
|
|
718
724
|
// TIER 3 — start new work. Row 176: the oldest unanswered ask comes first, so a new ask
|
|
719
725
|
// never waits for other efforts' decompose, design, plan and start rungs. No `target`: it
|
|
720
726
|
// would have withDeferralNote read a stream named like the ask id.
|
|
727
|
+
// Row 199: finish what you started. A grill with a draft comes before one without: a drafted
|
|
728
|
+
// ask, then a drafted amendment, then the oldest undrafted ask. The amendment has a target, so a
|
|
729
|
+
// walk scoped to its effort (drive's fill, `humanAction`) still meets it, and its line is
|
|
730
|
+
// `witness recap` on stage brainstorm, which `conversationStage` already reads as a conversation.
|
|
721
731
|
const ask = admits(undefined) ? nextAsk(root) : undefined;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
732
|
+
const grill = (a) => ({ line: `witness recap --file <recap.json> --ask ${a.id}`, stage: 'brainstorm', ask: { id: a.id, text: a.text } });
|
|
733
|
+
if (ask !== undefined && draftLine(root, { kind: 'ask', id: ask.id }) !== undefined)
|
|
734
|
+
return grill(ask);
|
|
735
|
+
const amending = efforts.find((e) => admits(e.slug) && draftLine(root, { kind: 'amend', effort: e.slug }) !== undefined);
|
|
736
|
+
if (amending !== undefined)
|
|
737
|
+
return { line: 'witness recap --amend --file <recap.json>', stage: 'brainstorm', target: amending.slug };
|
|
738
|
+
if (ask !== undefined)
|
|
739
|
+
return grill(ask);
|
|
725
740
|
// Row 194. With no ask and no effort the grill opens, in both modes: a chat starts it, and
|
|
726
741
|
// drive sets it aside as a conversation and reports the queue empty.
|
|
727
742
|
if (efforts.length === 0 && admits(undefined))
|
|
@@ -927,6 +942,19 @@ export function conversationStage(action) {
|
|
|
927
942
|
return action.line.startsWith('witness design ')
|
|
928
943
|
&& (action.line.includes('--open') || action.line.includes('--file'));
|
|
929
944
|
}
|
|
945
|
+
// Row 199. The draft of the conversation an action routes into, as `next` prints it: the grill of
|
|
946
|
+
// an ask or of an amendment, or the design session at any of its steps (author, show, gate), so a
|
|
947
|
+
// session relayed mid-look finds its direction and answers whichever step it lands on. Read once,
|
|
948
|
+
// here at the print site, so the ladder's action and the autonomous chat's walk print it alike.
|
|
949
|
+
export function conversationDraft(root, action) {
|
|
950
|
+
if (action.stage === 'brainstorm' && action.ask !== undefined)
|
|
951
|
+
return draftLine(root, { kind: 'ask', id: action.ask.id });
|
|
952
|
+
if (action.stage === 'brainstorm' && action.target !== undefined)
|
|
953
|
+
return draftLine(root, { kind: 'amend', effort: action.target });
|
|
954
|
+
if (action.stage === 'design' && action.target !== undefined)
|
|
955
|
+
return draftLine(root, { kind: 'design', spec: action.target });
|
|
956
|
+
return undefined;
|
|
957
|
+
}
|
|
930
958
|
// A stop that waits for the human: a decision the CLI could not settle, a conversation, or a
|
|
931
959
|
// start an unmet manual need blocks. Drive sets each aside, so none is work to launch it for.
|
|
932
960
|
function humanStop(canon, a) {
|
|
@@ -1144,6 +1172,9 @@ export async function run(ctx, argv) {
|
|
|
1144
1172
|
ctx.out(kv('target', action.target));
|
|
1145
1173
|
if (action.ask)
|
|
1146
1174
|
ctx.out(kv('ask', `${action.ask.id} — ${askText(action.ask.text)}`));
|
|
1175
|
+
const draft = conversationDraft(root, action);
|
|
1176
|
+
if (draft !== undefined)
|
|
1177
|
+
ctx.out(kv('draft', draft));
|
|
1147
1178
|
if (action.note)
|
|
1148
1179
|
ctx.out(kv('note', action.note));
|
|
1149
1180
|
if (action.block)
|
package/dist/verbs/recap.js
CHANGED
|
@@ -4,40 +4,15 @@ import { parseArgs } from 'node:util';
|
|
|
4
4
|
import { EXIT } from '../cli.js';
|
|
5
5
|
import { askQueue, reservedId } from '../ask.js';
|
|
6
6
|
import { loadConfig } from '../config.js';
|
|
7
|
+
import { dropDraft } from '../drafts.js';
|
|
7
8
|
import { ID_RE } from '../dsl.js';
|
|
9
|
+
import { idTextList } from '../idlists.js';
|
|
8
10
|
import { primaryRoot, stateCommit } from '../gitio.js';
|
|
9
11
|
import { appendEntry, effortAbandoned, entryLine, journalRel, readStream, streamExists } from '../journal.js';
|
|
10
12
|
import { acquireLock } from '../lock.js';
|
|
11
13
|
import { renderRefusal, v } from '../refusal.js';
|
|
12
14
|
import { kv } from '../toon.js';
|
|
13
15
|
import { crashPoint, guardTxn, withTxn } from '../txn.js';
|
|
14
|
-
function idTextList(raw, field, prefix, required) {
|
|
15
|
-
if (raw === undefined)
|
|
16
|
-
return required ? [v(field, 'required', 'absent', `>=1 {id, text} with ids ${prefix}1..`)] : [];
|
|
17
|
-
if (!Array.isArray(raw))
|
|
18
|
-
return [v(field, 'shape', typeof raw, 'list of {id, text}')];
|
|
19
|
-
if (required && raw.length === 0)
|
|
20
|
-
return [v(field, 'required', 'empty', `>=1 {id, text} with ids ${prefix}1..`)];
|
|
21
|
-
const out = [];
|
|
22
|
-
const seen = new Set();
|
|
23
|
-
raw.forEach((item, i) => {
|
|
24
|
-
const at = `${field}[${i}]`;
|
|
25
|
-
const e = (typeof item === 'object' && item !== null ? item : {});
|
|
26
|
-
if (typeof e.id !== 'string' || !new RegExp(`^${prefix}[0-9]+$`).test(e.id)) {
|
|
27
|
-
out.push(v(`${at}.id`, 'id-prefix', String(e.id ?? 'absent'), `${prefix}<number>`));
|
|
28
|
-
}
|
|
29
|
-
else if (seen.has(e.id)) {
|
|
30
|
-
out.push(v(`${at}.id`, 'id-unique', e.id, `unique within ${field}`));
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
seen.add(e.id);
|
|
34
|
-
}
|
|
35
|
-
if (typeof e.text !== 'string' || e.text.trim() === '') {
|
|
36
|
-
out.push(v(`${at}.text`, 'required', String(e.text ?? 'absent'), 'non-empty text'));
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
16
|
export function validateRecap(raw) {
|
|
42
17
|
if (typeof raw !== 'object' || raw === null)
|
|
43
18
|
return [v('recap', 'shape', String(raw), 'JSON object')];
|
|
@@ -61,7 +36,10 @@ export function validateRecap(raw) {
|
|
|
61
36
|
// Whether this slug may take this recap: a new effort needs a fresh stream, an amend a live
|
|
62
37
|
// one. Row 176: a reserved stream is refused before slug-reuse and for --amend too, whether
|
|
63
38
|
// or not it exists yet — an amend would append a recap to witness's own history.
|
|
64
|
-
|
|
39
|
+
// Exported, with askRefusal below, for `draft` (row 199), whose key names the ask or effort a
|
|
40
|
+
// recap will land on: a second copy would be a second answer to "may this recap land", and the
|
|
41
|
+
// two would part company on the first change.
|
|
42
|
+
export function effortRefusal(root, effort, amend) {
|
|
65
43
|
const reserved = reservedId(effort);
|
|
66
44
|
if (reserved !== undefined)
|
|
67
45
|
return v('effort', 'id-taken', effort, `a new id — ${effort} is ${reserved}`);
|
|
@@ -75,7 +53,7 @@ function effortRefusal(root, effort, amend) {
|
|
|
75
53
|
return undefined;
|
|
76
54
|
}
|
|
77
55
|
// Whether this ask may be answered by a new effort, read from one queue derivation.
|
|
78
|
-
function askRefusal(root, id) {
|
|
56
|
+
export function askRefusal(root, id) {
|
|
79
57
|
const ask = askQueue(root).find((a) => a.id === id);
|
|
80
58
|
if (!ask)
|
|
81
59
|
return v('--ask', 'unknown-ask', id, 'an ask id — witness status lists the queue');
|
|
@@ -185,6 +163,12 @@ export async function run(ctx, argv) {
|
|
|
185
163
|
renderRefusal(res.violations).forEach(ctx.err);
|
|
186
164
|
return EXIT.REFUSED;
|
|
187
165
|
}
|
|
166
|
+
// Row 199. The recap is the record now, so the grill's draft is done — only once the commit
|
|
167
|
+
// landed: a refused recap leaves the conversation where it was.
|
|
168
|
+
if (recap.ask !== undefined)
|
|
169
|
+
dropDraft(root, { kind: 'ask', id: recap.ask });
|
|
170
|
+
if (values.amend)
|
|
171
|
+
dropDraft(root, { kind: 'amend', effort: recap.effort });
|
|
188
172
|
ctx.out(kv('effort', recap.effort));
|
|
189
173
|
ctx.out(kv('class', recap.class));
|
|
190
174
|
ctx.out(kv('goals', recap.goals.length));
|
package/dist/worktree.js
CHANGED
|
@@ -56,11 +56,14 @@ export function ensureExcluded(root) {
|
|
|
56
56
|
const current = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
|
|
57
57
|
const lines = current.split('\n');
|
|
58
58
|
// screens are witnessed evidence, regenerable, never committed — ignored in every
|
|
59
|
-
// worktree so they stay out of changedFiles() and the reviewed tree-sha
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
// worktree so they stay out of changedFiles() and the reviewed tree-sha. Drafts (row 199)
|
|
60
|
+
// are a conversation's local memory, never committed: a repo whose .gitignore predates
|
|
61
|
+
// them would otherwise show each one to `git status` and to a human's `git add -A`
|
|
62
|
+
const missing = ['.witness/worktrees/', '.witness/screens/', '.witness/drafts/'].filter((l) => !lines.includes(l));
|
|
63
|
+
// A file a human left without a final newline keeps its last line whole.
|
|
64
|
+
const sep = current !== '' && !current.endsWith('\n') ? '\n' : '';
|
|
65
|
+
if (missing.length)
|
|
66
|
+
appendFileSync(excludePath, `${sep}${missing.map((l) => `${l}\n`).join('')}`);
|
|
64
67
|
}
|
|
65
68
|
// Row 132. Canon has ONE home, and it is the primary root. The exclusion set is
|
|
66
69
|
// `stateDirs(root)` — the same predicate `isStatePath` and `stateCommit` already use, never
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popovych.co/witness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
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.
|
|
4
|
+
"version": "0.24.0",
|
|
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.
|
|
15
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.24.0}"
|
|
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.
|
|
@@ -33,9 +33,9 @@ Every turn is `witness next`, read, act on the first matching row, repeat:
|
|
|
33
33
|
| `drive: done` is printed | `witness drive` started this session for one target and that target needs nothing more from it. **END YOUR TURN**. |
|
|
34
34
|
| a `relay:` line is printed with no `run:` (by `next`, `gate`, `decide` or `ship`) | This session reached its context limit where the journal carries the position. Print the `relay:` line verbatim, say the work continues in a fresh session in this directory, and **END YOUR TURN**. Leave the `next:` action to that session: it re-derives position from the CLI. |
|
|
35
35
|
| A `choose:` block is printed (`note:` says `N ready — ranked below`) | Several specs are ready and the CLI has ranked them. Render the whole block verbatim and in full — the recommendation, its why, each alternative's when and tradeoff, and the `judge-first:` line — then ask the human which to plan. Then use the `witness-plan` skill with the id they name. Never collapse the block to a bare list of ids: the ranking and its costs are the part they are deciding with. |
|
|
36
|
-
| `next:` names `witness decide` | A gate is stopped and the decision is the human's. Run `$WITNESS decide <gate> <target> --show`, render everything it emits verbatim and in full — the checks, the findings, and its ranked options — and **END YOUR TURN**. Never substitute a command set you remember: it is wrong at the round bound, in the reopened and stale states, and wherever a repair grant is unspent. Never run `--approve`, `--revise` or `--stop` on your own judgment. |
|
|
36
|
+
| `next:` names `witness decide` | A gate is stopped and the decision is the human's. Run `$WITNESS decide <gate> <target> --show`, render everything it emits verbatim and in full — the checks, the findings, and its ranked options — and **END YOUR TURN**. Never substitute a command set you remember: it is wrong at the round bound, in the reopened and stale states, and wherever a repair grant is unspent. Never run `--approve`, `--revise` or `--stop` on your own judgment. If the human answers a `design look` stop with a note that changes what the screen does, use the `witness-design` skill's **A behavior answer**. |
|
|
37
37
|
| a `run:` line is printed | This stage belongs to a different session; the CLI already compared `home:` against your cwd. Print the `run:` line verbatim for the human (if this session is `--manual`-armed, change the argument to `'/witness --manual'`), say that work continues in the fresh session, and **END YOUR TURN**. Never use a stage skill or run the `next:` command from the wrong `home:` — a fresh session is the execution model, and it is the only one. A `home:` line with **no** `run:` under it means you are already there: read on and act, unless a `relay:` printed alone (its row is above). |
|
|
38
|
-
| `stage: brainstorm` | Use the `witness-brainstorm` skill. |
|
|
38
|
+
| `stage: brainstorm` | Use the `witness-brainstorm` skill — with `target` (the effort) when `next` printed one: that is an amendment. |
|
|
39
39
|
| `stage: decompose` | Use the `witness-decompose` skill with `target` (the effort slug). |
|
|
40
40
|
| `stage: design` | Use the `witness-design` skill with `target` (the spec id). |
|
|
41
41
|
| `stage: plan` | Use the `witness-plan` skill with `target` (the spec id). |
|
|
@@ -47,7 +47,7 @@ Every turn is `witness next`, read, act on the first matching row, repeat:
|
|
|
47
47
|
|
|
48
48
|
`next`, `start`, `gate` and `decide --show` print `autonomy: on` and the `kept-stops:` set when the repo has opted in. Unless this session is `--manual`-armed, that changes exactly these rules:
|
|
49
49
|
|
|
50
|
-
- **Decisions are the CLI's.** It settles each stop itself — through its policy, or the arbiter it launches — and prints who decided (`decided: … · policy` or `· arbiter`). Never run `witness decide`. Outside the
|
|
50
|
+
- **Decisions are the CLI's.** It settles each stop itself — through its policy, or the arbiter it launches — and prints who decided (`decided: … · policy` or `· arbiter`). Never run `witness decide`. Outside the grill and the design session, which stay conversations, never end your turn to ask and never put options of your own to the human: when a stage owes a call of yours, make it, state it in one line, and continue.
|
|
51
51
|
- **A `witness decide` line from `next` belongs to a human.** The CLI left that stop for a person, and the `kept-stop:` line or the refusal above it says why. Run `$WITNESS decide <gate> <target> --show`, render it verbatim, name the human's act, and END YOUR TURN.
|
|
52
52
|
- **The merge is the CLI's.** `witness ship` merges its own PR on green CI and prints `merged:`. You still never merge.
|
|
53
53
|
|
|
@@ -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.
|
|
6
|
+
${WITNESS_BIN:-npx -y @popovych.co/witness@0.24.0} 2>/dev/null || true
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: witness-brainstorm
|
|
3
|
-
description:
|
|
3
|
+
description: The grill for a witness effort — walks the whole decision tree one plain-words question per turn, each with up to three ranked answers, explores the repo before asking, saves a local draft after every settled answer, and ends in a critique pass, a playback and a confirmed recap persisted via witness recap. Normally invoked by /witness.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# witness-brainstorm —
|
|
6
|
+
# witness-brainstorm — the grill → confirmed recap
|
|
7
7
|
|
|
8
|
-
<!-- Derived (MIT):
|
|
9
|
-
question at a time
|
|
10
|
-
|
|
11
|
-
witness: ends in a confirmed
|
|
8
|
+
<!-- Derived (MIT): the grill from mattpocock/skills grill-me (the whole
|
|
9
|
+
decision tree, one question at a time with a recommended answer, the codebase
|
|
10
|
+
explored instead of asking); intent-before-design exploration from
|
|
11
|
+
obra/superpowers brainstorming. Rewritten for witness: ends in a confirmed
|
|
12
|
+
recap, keeps a local draft, writes no document. See NOTICE.md. -->
|
|
12
13
|
|
|
13
14
|
## Ground rules (every witness skill)
|
|
14
15
|
|
|
15
16
|
Resolve the CLI once per session:
|
|
16
17
|
|
|
17
18
|
```bash
|
|
18
|
-
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.
|
|
19
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.24.0}"
|
|
19
20
|
```
|
|
20
21
|
|
|
21
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.
|
|
22
|
-
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the
|
|
23
|
+
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the grill, the design session's questions) are conversation, not a block: an affirmation there accepts your stated recommendation, with no selection entry, no journal claim and no flag; a number there (1, 2, 3) picks that answer — never a `witness decide` option. A selection does not survive session death: killed and re-run, render the block again and ask again.
|
|
23
24
|
- **Never edit `specs/**`, `plans/**`, or `designs/**`** (the canon dirs — `paths:` in witness.config.yaml may relocate them) — not with an edit tool, not with a write tool, not with Bash redirection. The CLI is the sole writer of state; you author in scratch files under `$(mktemp -d)` and hand them to the CLI. (The canon guard blocks you; the trailer audit catches what it can't.)
|
|
24
25
|
- **Read canon with `witness read <id>`, never by path.** Canon lives at the primary root; inside a worktree the files are **absent by design**, so a path read finds nothing and a stale copy cannot be mistaken for the contract. Fat artifacts: `witness read <spec-id> --design --outline`, then `--lines <a>-<b>`.
|
|
25
26
|
- **Read a file before your first edit of it in this session.** Relay boundaries, `verify-red`'s stash cycle, and worktree re-attach all change files under you — an edit against a remembered copy is how "modified since read" and partial applies happen. The CLI now prints `stale-reads:` when it churns the tree; treat that list as unread.
|
|
@@ -27,32 +28,78 @@ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.23.0}"
|
|
|
27
28
|
- **Refusal repair loop:** a `witness` verb exiting 2 prints structured violations (`field · rule · got · want`). Fix your input and retry — **3 total attempts** per artifact, then stop, show the human the violation list verbatim, and end your turn.
|
|
28
29
|
- **A refused or hook-blocked command is a stop, not a step to drop.** Re-issue it on its own; if it still refuses, tell the human what was blocked and why. Never proceed by deleting the refused half of a compound command — a dropped step is silent, and silence is how a skipped check becomes a shipped defect.
|
|
29
30
|
- **Re-entrancy:** derive position from CLI output (`$WITNESS next`, the dashboard, `log`, `index`) — never from conversation memory. Killed and re-run, you must converge.
|
|
30
|
-
- **A `witness: context …` line is addressed to you.** Once per session, when this session's context has filled to the configured limit, it follows a command's result. Finish the act you are in (here: the
|
|
31
|
+
- **A `witness: context …` line is addressed to you.** Once per session, when this session's context has filled to the configured limit, it follows a command's result. Finish the act you are in (here: the answer you are on, through its `witness draft` save) — a session that has done none yet does the one it was routed to — then hand back to /witness, whose `next` hands over to a fresh session.
|
|
31
32
|
|
|
32
33
|
## What you produce
|
|
33
34
|
|
|
34
|
-
No canon, only evidence: a **confirmed scope recap** persisted by `witness recap` as the birth entry of the effort journal. This skill writes no spec, no plan, no brief — decompose consumes the recap after you, including after session death.
|
|
35
|
+
No canon, only evidence: a **confirmed scope recap** persisted by `witness recap` as the birth entry of the effort journal. This skill writes no spec, no plan, no brief — decompose consumes the recap after you, including after session death. On the way it keeps a **local draft** through `witness draft`: never committed, never journaled, deleted when the recap lands.
|
|
35
36
|
|
|
36
|
-
##
|
|
37
|
+
## Start
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
```bash
|
|
40
|
+
DIR=$(mktemp -d)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **An amendment queues no ask.** When `next` printed a `target:` with `stage: brainstorm`, or you were sent to amend (a decompose scope stop, a design session's behavior answer), skip the ask: the key is `amend-<effort>`, and the amendment bullet below is where you start.
|
|
44
|
+
- **The ask.** Every new effort's grill starts by queuing an ask. Use the `ask:` line from `$WITNESS next` when it is there. When it is not, open with "what do you want to build?", run `$WITNESS ask "<their words>"` on the answer, then run `$WITNESS next` again — its `ask:` line names the id.
|
|
45
|
+
- **The key.** Your draft's key is `ask-<id>`, or `amend-<effort>` for an amendment.
|
|
46
|
+
- **Resuming.** A `draft:` line means resume: read the draft back with the command it names, continue after the last settled answer, and tell the human in one line where you resume. `next` names a drafted amendment the same way, with `target:` its effort.
|
|
47
|
+
- **An amendment** (a decompose stop sent back to re-interview, or a design session's behavior answer) starts from `$WITNESS draft amend-<effort>`, which prints the latest recap as the seed. Edit that seed, and never retype a text you are not changing: a decision's text is part of its plans' pin. Walk the branches the change touches. Once its first answer is saved, `next` resumes it ahead of the effort's other work.
|
|
48
|
+
|
|
49
|
+
## The question
|
|
50
|
+
|
|
51
|
+
- **One question per turn**, in plain words — no witness jargon the human has to decode.
|
|
52
|
+
- **Up to three ranked answers, in the CLI's field words** (a human meets one shape everywhere in this pipeline). Answer 1 is the pick, with its **why**. Answers 2 and 3 each carry **when** (better if…) and **tradeoff** (the cost against the pick). Give fewer when fewer real answers exist — never padded.
|
|
53
|
+
- **How the human replies:** `y` or `1` takes the pick, `2` or `3` takes those, or they answer in their own words. Lock each answer before the next.
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
2. **Problem & outcome goals** — what observably changes, for whom. Number them `g1, g2, …`. Each goal is an outcome someone can observe — "Guests can pay by card", never "Integrate Stripe".
|
|
42
|
-
3. **Non-goals** — what this effort deliberately will not do: `n1, n2, …`. Push for at least one; scope without a "no" is unbounded.
|
|
43
|
-
4. **Constraints** — standing limits (compliance, performance budgets, compatibility): `c1, …`. Empty is fine.
|
|
44
|
-
5. **Slice candidates** — non-binding thin vertical slices you would expect decompose to cut. Plain strings.
|
|
55
|
+
## Explore before asking, check before showing
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
- Anything the repo can answer — code, `witness index`, `witness read <id>`, `witness read <id> --decisions`, product docs, journals — you answer yourself, and say what you found with `file:line`. Only intent, priorities, taste and facts outside the repo reach the human.
|
|
58
|
+
- Every factual claim in a question is verified by a file read or a probe before the human sees it.
|
|
47
59
|
|
|
48
|
-
##
|
|
60
|
+
## The walk
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
- **The floor.** The recap's fields are **the floor, not the count**: keep asking while any open branch would change what gets built. A recap that would surprise its reviewer is under-asked.
|
|
63
|
+
- **The order, by dependency:** the problem and who has it → scope edges → behavior, including edge cases and failures → data and approach → rollout and compatibility. Skip branches the repo already answers.
|
|
64
|
+
- **Class** — `feature | fix | chore` — is inferred: `fix` restores promised behavior; `chore` is motion with no state change (deps, tooling); everything else is `feature`. It shows in the playback, and you ask only when it is unclear.
|
|
65
|
+
- **No short forms.** A fix's or a chore's tree is small, so its grill ends fast; it is still walked whole.
|
|
66
|
+
- **Out of bounds:** the look (that is the design session) and the order of steps (that is the plan).
|
|
67
|
+
|
|
68
|
+
## Where each answer goes
|
|
69
|
+
|
|
70
|
+
- **Goals** are outcomes someone can observe, numbered `g1, g2, …` — "Guests can pay by card", never "Integrate Stripe".
|
|
71
|
+
- **Non-goals** are deliberate noes, `n1, …`. Push for at least one; scope without a "no" is unbounded.
|
|
72
|
+
- **Constraints** are standing limits on the whole effort (compliance, performance budgets, compatibility), `c1, …`. Empty is fine.
|
|
73
|
+
- **Decisions** are settled answers that shape at least one spec, in behavior or approach, `d1, …`. An answer that shapes no single spec is a constraint or a non-goal.
|
|
74
|
+
- **Slices** are non-binding thin vertical slices you expect decompose to cut — plain strings.
|
|
75
|
+
|
|
76
|
+
## Save as you go
|
|
77
|
+
|
|
78
|
+
After each settled answer, write the growing draft — the recap fields settled so far, plus `open`, the branches still open — and save it:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cat > "$DIR/draft.json" <<'EOF'
|
|
82
|
+
{
|
|
83
|
+
"class": "feature",
|
|
84
|
+
"goals": [{ "id": "g1", "text": "Guests can pay by card at checkout" }],
|
|
85
|
+
"decisions": [{ "id": "d1", "text": "A declined card keeps the cart and says why" }],
|
|
86
|
+
"open": ["which card brands", "refunds"]
|
|
87
|
+
}
|
|
88
|
+
EOF
|
|
89
|
+
$WITNESS draft <key> --file "$DIR/draft.json"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The CLI checks the draft's shape, never its completeness. A `draft-moved` refusal means another session saved answers since: read the draft back and continue from it.
|
|
93
|
+
|
|
94
|
+
## The end
|
|
95
|
+
|
|
96
|
+
1. Once no open branch would change what gets built, run **one critique pass** over the whole set from the human's seat — what goes stale, what they meet on upgrade, which outside readers break — and probe every finding.
|
|
97
|
+
2. Then the **playback** table: class · goals · non-goals · constraints · decisions · slices.
|
|
98
|
+
3. Then an explicit yes. Adjust and re-render until confirmed: the recap is decompose's input contract, and a fuzzy recap poisons every stage after it.
|
|
51
99
|
|
|
52
100
|
## Persist
|
|
53
101
|
|
|
54
102
|
```bash
|
|
55
|
-
DIR=$(mktemp -d)
|
|
56
103
|
cat > "$DIR/recap.json" <<'EOF'
|
|
57
104
|
{
|
|
58
105
|
"effort": "auth-hardening",
|
|
@@ -60,17 +107,15 @@ cat > "$DIR/recap.json" <<'EOF'
|
|
|
60
107
|
"goals": [{ "id": "g1", "text": "Refresh tokens rotate before expiry" }],
|
|
61
108
|
"non_goals": [{ "id": "n1", "text": "No SSO provider changes" }],
|
|
62
109
|
"constraints": [],
|
|
110
|
+
"decisions": [{ "id": "d1", "text": "Rotation happens server-side, invisible to the client" }],
|
|
63
111
|
"slices": ["token rotation"]
|
|
64
112
|
}
|
|
65
113
|
EOF
|
|
66
|
-
$WITNESS recap --file "$DIR/recap.json"
|
|
114
|
+
$WITNESS recap --file "$DIR/recap.json" --ask <id>
|
|
67
115
|
```
|
|
68
116
|
|
|
69
|
-
-
|
|
117
|
+
- `--ask <id>` names the ask the recap answers, and `next` stops offering it.
|
|
118
|
+
- **An amendment** runs `$WITNESS recap --amend --file "$DIR/recap.json"`, the recap naming its effort. It re-arms the decompose gate's feature-class stop: scope changed, scope gets re-approved.
|
|
119
|
+
- **A confirmed recap deletes the draft.** Nothing is left to resume.
|
|
70
120
|
- Effort slug: kebab-case `[a-z0-9-]+`, minted from the scope title. A `slug-reuse` refusal means that history exists — mint a different slug; histories never merge, and `--amend` is never the dodge.
|
|
71
|
-
- **Amending scope** — mid-effort scope change, or the decompose gate's stop screen handed off to re-interview: run the interview shortened to what changed, confirm, then `$WITNESS recap --amend <effort> --file "$DIR/recap.json"`. An amendment re-arms the decompose gate's feature-class stop: scope changed, scope gets re-approved.
|
|
72
121
|
- The refusal repair loop applies (3 attempts, then stop with the violation list).
|
|
73
|
-
|
|
74
|
-
## Stateless by design
|
|
75
|
-
|
|
76
|
-
Session died mid-interview? Start over — nothing was persisted; that is the accepted cost. If the dashboard's efforts table already shows the effort (the recap landed), do not re-interview — hand back to /witness.
|
|
@@ -10,11 +10,11 @@ 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.
|
|
13
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.24.0}"
|
|
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.
|
|
17
|
-
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the
|
|
17
|
+
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the grill, the design session's questions) are conversation, not a block: an affirmation there accepts your stated recommendation, with no selection entry, no journal claim and no flag; a number there (1, 2, 3) picks that answer — never a `witness decide` option. A selection does not survive session death: killed and re-run, render the block again and ask again.
|
|
18
18
|
- **Never edit `specs/**`, `plans/**`, or `designs/**`** (the canon dirs — `paths:` in witness.config.yaml may relocate them) — not with an edit tool, not with a write tool, not with Bash redirection. The CLI is the sole writer of state; you author in scratch files under `$(mktemp -d)` and hand them to the CLI. (The canon guard blocks you; the trailer audit catches what it can't.)
|
|
19
19
|
- **Read canon with `witness read <id>`, never by path.** Canon lives at the primary root; inside a worktree the files are **absent by design**, so a path read finds nothing and a stale copy cannot be mistaken for the contract. Fat artifacts: `witness read <spec-id> --design --outline`, then `--lines <a>-<b>`.
|
|
20
20
|
- **Read a file before your first edit of it in this session.** Relay boundaries, `verify-red`'s stash cycle, and worktree re-attach all change files under you — an edit against a remembered copy is how "modified since read" and partial applies happen. The CLI now prints `stale-reads:` when it churns the tree; treat that list as unread.
|
|
@@ -103,5 +103,6 @@ $WITNESS gate decompose --effort <effort> # append --manual when the run aske
|
|
|
103
103
|
- **Auto-pass** (green path) → done; hand back to /witness.
|
|
104
104
|
- **Stop** (standing stop, blocking findings, fix-created-spec tripwire) → render the gate output verbatim and in full, including its ranked options and `run:` line, and END YOUR TURN. You never decide.
|
|
105
105
|
- **Re-entered after `--revise`** → `decide --show` reconstructs the verdict + human note; findings anchor to spec headings. Fix via new `witness write` calls (same ids amend in place), self-check totality, re-gate. The 3-round bound is the CLI's — surface it, never fight it. `--show` also emits `state:` and `exits:` — a `reopened` or `settled` state means the verdict above it is history, so act on the `exits:` line, not on remembered findings.
|
|
106
|
-
-
|
|
106
|
+
- **Re-entered after the stop screen's re-interview option** (`--revise --upstream <effort>` naming this effort: the scope itself; `decide decompose <effort> --show` carries the human's note) → never re-slice against the recap the human rejected. Use the `witness-brainstorm` skill to amend it, starting from `$WITNESS draft amend-<effort>`; once its first answer is saved `next` resumes the amendment, and the re-armed gate judges the new slicing after it lands.
|
|
107
|
+
- Findings implicate the **scope itself** (goals wrong, not slicing wrong)? Tell the human that `--revise --upstream` on the stop screen sends it back to re-interview: the `witness-brainstorm` skill amends the recap, starting from `$WITNESS draft amend-<effort>`, and `next` resumes the amendment once its first answer is saved.
|
|
107
108
|
- **Under `autonomy: on`** (printed by `gate`; not on a `--manual` run): the CLI settles this stop itself — the scope stop and a created spec go to its arbiter, findings to its policy — and prints `decided:`. If a `relay:` line printed under it, this session has reached its context limit: print that line verbatim and END YOUR TURN — a fresh session takes the revise. Otherwise, on `→ revise`, re-author from the revise-context it printed and re-gate; on `→ approve` or `→ stop`, hand back to /witness. Never run `witness decide`. End your turn only at a `kept-stop:` line, a `relay:` line, or a refusal you cannot repair.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: witness-design
|
|
3
|
-
description: Produce a ui spec's living approved look —
|
|
3
|
+
description: Produce a ui spec's living approved look — a design session that shows up to three rendered, checked directions, converges in the grill's question shape, saves a local draft, and ends in a self-contained designs/<spec-id>.html handed to witness design, then the design gate. Normally invoked by /witness with the spec id.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# witness-design — spec → approved look → gate
|
|
@@ -16,11 +16,11 @@ 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
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.24.0}"
|
|
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.
|
|
23
|
-
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the
|
|
23
|
+
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the grill, the design session's questions) are conversation, not a block: an affirmation there accepts your stated recommendation, with no selection entry, no journal claim and no flag; a number there (1, 2, 3) picks that answer — never a `witness decide` option. A selection does not survive session death: killed and re-run, render the block again and ask again.
|
|
24
24
|
- **Never edit `specs/**`, `plans/**`, or `designs/**`.** The CLI is the sole writer of state. Author HTML in `$(mktemp -d)` and hand it to `witness design`. (The canon guard blocks direct edits; the trailer audit catches end-runs.)
|
|
25
25
|
- **Read canon with `witness read <id>`, never by path.** Canon lives at the primary root; inside a worktree the files are **absent by design**, so a path read finds nothing and a stale copy cannot be mistaken for the contract. Fat artifacts: `witness read <spec-id> --design --outline`, then `--lines <a>-<b>`.
|
|
26
26
|
- **Read a file before your first edit of it in this session.** Relay boundaries, `verify-red`'s stash cycle, and worktree re-attach all change files under you — an edit against a remembered copy is how "modified since read" and partial applies happen. The CLI now prints `stale-reads:` when it churns the tree; treat that list as unread.
|
|
@@ -28,7 +28,7 @@ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.23.0}"
|
|
|
28
28
|
- **Refusal repair loop:** `witness design` exiting 2 prints structured violations (`field · rule · got · want`). Fix and retry — **3 total attempts**, then stop and show the human the list verbatim.
|
|
29
29
|
- **A refused or hook-blocked command is a stop, not a step to drop.** Re-issue it on its own; if it still refuses, tell the human what was blocked and why. Never proceed by deleting the refused half of a compound command — a dropped step is silent, and silence is how a skipped check becomes a shipped defect.
|
|
30
30
|
- **Re-entrancy:** derive position from `$WITNESS next` / the dashboard / `log`, never from conversation memory.
|
|
31
|
-
- **A `witness: context …` line is addressed to you.** Once per session, when this session's context has filled to the configured limit, it follows a command's result. Finish the act you are in (here: the look, through `witness design` and its gate) — a session that has done none yet does the one it was routed to — then hand back to /witness, whose `next` hands over to a fresh session.
|
|
31
|
+
- **A `witness: context …` line is addressed to you.** Once per session, when this session's context has filled to the configured limit, it follows a command's result. Finish the act you are in (here: the answer you are on, through its `witness draft` save — or, once the full look is authored, the look through `witness design` and its gate) — a session that has done none yet does the one it was routed to — then hand back to /witness, whose `next` hands over to a fresh session.
|
|
32
32
|
|
|
33
33
|
## When you run
|
|
34
34
|
|
|
@@ -37,6 +37,8 @@ The `/witness` loop invokes you for a `ui`-flagged spec in a **feature** effort
|
|
|
37
37
|
- **New screen** — `$WITNESS read <spec-id> --design` refuses `no-design`: there is no living look yet. Design from the spec.
|
|
38
38
|
- **Amend a living look** — the file exists but the spec was amended. Read the current look first; propose the *smallest* change that re-serves the (changed) behavior. If there is genuinely no visual delta, tell the human to run `witness design <spec-id> --reconfirm` (re-stamps, no session).
|
|
39
39
|
|
|
40
|
+
**Resuming.** A `draft:` line from `next` means this look has a draft: run `$WITNESS draft design-<spec-id>`, whichever design step `next` routed (`--file`, `--open` or the gate), and continue from the chosen direction, the settled answers and the saved page. A session that relayed or died mid-amendment needs nothing here: `next` hands the amendment to the grill first, and the look waits with its draft.
|
|
41
|
+
|
|
40
42
|
## Under `autonomy: on`
|
|
41
43
|
|
|
42
44
|
When the CLI prints `autonomy: on` and this run is not `--manual`, the session is still a conversation with the human, and only the critic's rounds are settled for you. The CLI settles each round itself and prints `decided: … → revise` with a revise-context: re-author from it, register the new look with `$WITNESS design <spec-id> --file`, `--open` it and re-gate, showing the human each new look — unless a `relay:` line printed under the decision: this session has reached its context limit, so print that line verbatim and END YOUR TURN — a fresh session takes the revise. When the critic is clean the CLI stops at the kept stop `design look` and prints its `kept-stop:` line: render the gate output and END YOUR TURN; the human approves the screen or sends a note. A human's `--revise --note "…"` starts the next round, and that round is settled the same way.
|
|
@@ -56,13 +58,27 @@ $WITNESS read <spec-id> --design # the current look, in amend mode; its `n
|
|
|
56
58
|
|
|
57
59
|
## The session (interactive — this stage is human judgment)
|
|
58
60
|
|
|
61
|
+
```bash
|
|
62
|
+
DIR=$(mktemp -d)
|
|
63
|
+
```
|
|
64
|
+
|
|
59
65
|
1. **Context.** Name the screen's one job and primary user (from the spec's Motivation + Behavior). If a design canon is configured, read it — it governs hierarchy, framing, action placement, and component vocabulary. Report what you found before proposing.
|
|
60
|
-
2. **
|
|
61
|
-
3. **
|
|
62
|
-
4. **
|
|
66
|
+
2. **Directions, rendered.** Up to three ranked structural directions (different hierarchies or groupings, never three skins of one idea), side by side on one self-contained comparison page, `$DIR/directions.html`, using the spec's real content. Each stays consistent with the canon.
|
|
67
|
+
3. **Check before showing.** Screenshot the page with the machine's browser (for example headless Chrome: `--headless --screenshot`), look at the picture, fix what is broken, and check again. With no browser, say "not checked" when you open it — never claim a check that did not run.
|
|
68
|
+
4. **Open it as a preview.** `$WITNESS design <spec-id> --preview "$DIR/directions.html"` opens the page through the configured opener; it registers nothing and journals nothing.
|
|
69
|
+
5. **Converge** in the grill's question shape: up to three ranked answers, the pick first with its why, the others each with when it is better and its tradeoff — never padded. After each settled answer save the draft: the chosen `direction`, the settled `answers`, and `open`. A `draft-moved` refusal means another session saved answers since: read the draft back and continue from it.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cat > "$DIR/draft.json" <<'EOF'
|
|
73
|
+
{ "direction": "grouped list", "answers": ["groups first", "no icons"], "open": ["empty state"] }
|
|
74
|
+
EOF
|
|
75
|
+
$WITNESS draft design-<spec-id> --file "$DIR/draft.json"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
6. **Author the full look** with every state the spec promises — every promised behavior visible and operable (the design-critic checks this as blocking coverage). One **self-contained** HTML file: inline all CSS/JS, embed assets as `data:` URIs, no external `src`/`href`. Give every section a stable, unique `id` (`id="essentials"`, `id="save-bar"`, …): these are the design-critic's anchors and must total **≥ 2**. Data-shape anchoring: when a section renders spec data, name the id after the data it shows, not its pixels. Check it the same way as step 3, and save the page in the draft as `page`.
|
|
79
|
+
7. **Register, show, gate** — the commands below.
|
|
63
80
|
|
|
64
81
|
```bash
|
|
65
|
-
DIR=$(mktemp -d)
|
|
66
82
|
cat > "$DIR/look.html" <<'EOF'
|
|
67
83
|
<!doctype html>
|
|
68
84
|
<html><head><meta charset="utf-8"><style>/* inline tokens + layout */</style></head>
|
|
@@ -92,4 +108,13 @@ $WITNESS gate design <spec-id> # append --manual when the run asked for it
|
|
|
92
108
|
- The design gate **always stops** — the look is human judgment, same footing as ship. It refuses to run at all until `witness design <spec-id> --open` has shown the human the current artifact. Render the gate output verbatim and in full, including its ranked options and `run:` line. **END YOUR TURN.** You never decide. The findings are *about* the design — they are never a substitute for the human being shown it.
|
|
93
109
|
- **Re-entered after `--revise`** → `witness decide design <spec-id> --show` reconstructs the verdict + note (findings anchor to `design#<id>` or `<spec-id> > ## Heading`). Re-author the HTML, re-run `witness design`, re-gate. The 3-round bound is the CLI's — surface it, never fight it. `--show` also emits `state:` and `exits:` — a `reopened` or `settled` state means the verdict above it is history, so act on the `exits:` line, not on remembered findings.
|
|
94
110
|
- Findings implicate the **slicing** (the spec is wrong, not the look)? Tell the human `witness decide design <spec-id> --revise --upstream <effort>` reopens decompose (scope-level changes chain to `recap --amend`).
|
|
95
|
-
- On approve the CLI stamps `design: {sha, spec}` on the spec; the plan stage then requires that pin. You are done — hand back to `/witness`.
|
|
111
|
+
- On approve the CLI stamps `design: {sha, spec}` on the spec, and the look's draft ends; the plan stage then requires that pin. You are done — hand back to `/witness`.
|
|
112
|
+
|
|
113
|
+
## A behavior answer
|
|
114
|
+
|
|
115
|
+
An answer that adds or changes what the screen *does* — not only how it looks — belongs to the recap, not the look.
|
|
116
|
+
|
|
117
|
+
- **Pause the look.** Check the change against `$WITNESS read <effort> --decisions`.
|
|
118
|
+
- **Amend the recap** with a numbered decision the human confirms: start from `$WITNESS draft amend-<effort>` (the seed, the latest recap, every text exact), save `amend-<effort>` drafts as you go, settle a clash with a non-goal or a constraint in one grill-shaped question, then run `$WITNESS recap --amend --file "$DIR/recap.json"`.
|
|
119
|
+
- **Then** the design draft keeps the direction, the answers and the page. Tell the human decompose re-opens (under autonomy drive updates the spec), and end the session: the look comes back pending on the amended spec, and nothing is redone.
|
|
120
|
+
- **A note at the approval stop** (`design look`) that changes behavior settles the stop first, so no stale look can be approved and the draft survives: run `witness decide design <spec-id> --revise --upstream <effort> --note "<their words>"`, then amend as above. A note only about the look stays `witness decide design <spec-id> --revise --note "…"`.
|
|
@@ -15,11 +15,11 @@ 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.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.24.0}"
|
|
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.
|
|
22
|
-
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the
|
|
22
|
+
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the grill, the design session's questions) are conversation, not a block: an affirmation there accepts your stated recommendation, with no selection entry, no journal claim and no flag; a number there (1, 2, 3) picks that answer — never a `witness decide` option. A selection does not survive session death: killed and re-run, render the block again and ask again.
|
|
23
23
|
- **Never edit `specs/**`, `plans/**`, or `designs/**`** (the canon dirs — `paths:` in witness.config.yaml may relocate them) — not with an edit tool, not with a write tool, not with Bash redirection. The CLI is the sole writer of state; you author in scratch files under `$(mktemp -d)` and hand them to the CLI. (The canon guard blocks you; the trailer audit catches what it can't.)
|
|
24
24
|
- **Read canon with `witness read <id>`, never by path.** Canon lives at the primary root; inside a worktree the files are **absent by design**, so a path read finds nothing and a stale copy cannot be mistaken for the contract. Fat artifacts: `witness read <spec-id> --design --outline`, then `--lines <a>-<b>`.
|
|
25
25
|
- **Read a file before your first edit of it in this session.** Relay boundaries, `verify-red`'s stash cycle, and worktree re-attach all change files under you — an edit against a remembered copy is how "modified since read" and partial applies happen. The CLI now prints `stale-reads:` when it churns the tree; treat that list as unread.
|
|
@@ -15,11 +15,11 @@ 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.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.24.0}"
|
|
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.
|
|
22
|
-
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the
|
|
22
|
+
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the grill, the design session's questions) are conversation, not a block: an affirmation there accepts your stated recommendation, with no selection entry, no journal claim and no flag; a number there (1, 2, 3) picks that answer — never a `witness decide` option. A selection does not survive session death: killed and re-run, render the block again and ask again.
|
|
23
23
|
- **Never edit `specs/**`, `plans/**`, or `designs/**`** (the canon dirs — `paths:` in witness.config.yaml may relocate them) — not with an edit tool, not with a write tool, not with Bash redirection. The CLI is the sole writer of state; you author in scratch files under `$(mktemp -d)` and hand them to the CLI. (The canon guard blocks you; the trailer audit catches what it can't.)
|
|
24
24
|
- **Read canon with `witness read <id>`, never by path.** Canon lives at the primary root; inside a worktree the files are **absent by design**, so a path read finds nothing and a stale copy cannot be mistaken for the contract. Fat artifacts: `witness read <spec-id> --design --outline`, then `--lines <a>-<b>`.
|
|
25
25
|
- **Read a file before your first edit of it in this session.** Relay boundaries, `verify-red`'s stash cycle, and worktree re-attach all change files under you — an edit against a remembered copy is how "modified since read" and partial applies happen. The CLI now prints `stale-reads:` when it churns the tree; treat that list as unread.
|
|
@@ -15,11 +15,11 @@ 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.
|
|
18
|
+
WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.24.0}"
|
|
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.
|
|
22
|
-
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the
|
|
22
|
+
- **The human decides; you may type it.** Run a `witness decide` verb when the human **names an option** — its number or its verb — or gives a bare affirmation ("y", "ok", "go") while a **CLI-rendered decision block carrying a recommended option** is on screen: the affirmation **selects the recommended option**, and you append `--via affirmation` to the printed command. Otherwise the string is run **byte-for-byte**: never recomposed, never reformatted, never with a placeholder you resolved yourself. The moment you compose a `--note` or resolve an id, you are authoring their decision. **A nod never takes** `--approve --override`, `--stop`, a trust grant, or `witness abandon` — those require naming, and the CLI refuses them with `nod-cannot`. A block with **no** recommendation also requires naming. **Questions you authored yourself** (the grill, the design session's questions) are conversation, not a block: an affirmation there accepts your stated recommendation, with no selection entry, no journal claim and no flag; a number there (1, 2, 3) picks that answer — never a `witness decide` option. A selection does not survive session death: killed and re-run, render the block again and ask again.
|
|
23
23
|
- **Never edit `specs/**`, `plans/**`, or `designs/**`** (the canon dirs — `paths:` in witness.config.yaml may relocate them) — not with an edit tool, not with a write tool, not with Bash redirection. The CLI is the sole writer of state; you author in scratch files under `$(mktemp -d)` and hand them to the CLI. (The canon guard blocks you; the trailer audit catches what it can't.)
|
|
24
24
|
- **Read canon with `witness read <id>`, never by path.** Canon lives at the primary root; inside a worktree the files are **absent by design**, so a path read finds nothing and a stale copy cannot be mistaken for the contract. Fat artifacts: `witness read <spec-id> --design --outline`, then `--lines <a>-<b>`.
|
|
25
25
|
- **Read a file before your first edit of it in this session.** Relay boundaries, `verify-red`'s stash cycle, and worktree re-attach all change files under you — an edit against a remembered copy is how "modified since read" and partial applies happen. The CLI now prints `stale-reads:` when it churns the tree; treat that list as unread.
|