@sabaiway/agent-workflow-kit 11.2.0 → 12.0.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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 12.0.0 — the reader mirror refuses an open-list clause, the outgoing reader becomes a prior, the consult line names the second case and the lens walks around every check (AD-133)
8
+
9
+ **MAJOR by the AD-117 class.** `references/scripts/spec-schema.mjs` and its test are the memory 8.0.0 bodies
10
+ byte-identically — `spec-check` relays the new `open-list` rule unchanged, so a consumer store valid under the old canon
11
+ can refuse under this kit; the remedy is the closing clause, never a fourth case. `tools/script-priors.mjs` and
12
+ `FROZEN_PRIORS` gain the outgoing reader pair as `5.0.0`..`7.2.1` with its fixture bodies under
13
+ `test/fixtures/script-priors/5.0.0/`, so a deployed 5.0.0–7.2.1 reader classifies `prior`, never `custom`.
14
+
15
+ **The consult clause and the lens bullet.** `tools/procedures.mjs` `CONSULT_LINE` — two source literals joined into
16
+ ONE rendered line, each under the 1000-byte cap — renders inside the agy `--focus` payload "If this finding is the
17
+ second case against the same check, reply with the replacement invariant, not an added case."; the codex consult-brief
18
+ and the lens re-dispatch forms say "with the same payload"; `tools/procedures.test.mjs` pins the payload order for both
19
+ activities. `references/agents/review-lens.md` gains the "How to review" bullet **When the artifact is a plan, walk
20
+ around every check** (the S12 arm in `cheap-agents-derived.test.mjs`; the placed copy takes the same line on refresh).
21
+ `tools/spec-coverage.test/cli.test.mjs` characterizes the S6 exit partition (`--help` / `-h` as the sole argument, an
22
+ unreadable store, a stray `--reason` beside `--check`, the debt-write failure as an exception).
23
+
7
24
  ## 11.2.0 — a field report is triaged through a rendered procedure over a checked record (AD-132)
8
25
 
9
26
  Three consumer field reports were triaged ad hoc — verify each claim on HEAD, record the verdict,
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '11.2.0'
6
+ version: '12.0.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "11.2.0",
6
+ "version": "12.0.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "11.2.0",
3
+ "version": "12.0.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -29,6 +29,7 @@ How to review:
29
29
  restatement of design intent back to the orchestrator.
30
30
  - **Respect what is already decided.** The prompt will name findings already folded and decisions
31
31
  already locked. Re-raising them is churn; checking whether the FIX is correct is real work.
32
+ - **When the artifact is a plan, walk around every check.** For every check bullet of each governing spec, name the invariant it proves and one state that walks around it; a named state is a finding with the replacement invariant, never an added case.
32
33
  - **Accuracy over volume.** A wrong finding costs the orchestrator more than a missed one — it
33
34
  spends a verification cycle and erodes trust in the whole list. If an angle turns up nothing, say
34
35
  so in one line and move on. Never pad.
@@ -1,13 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // spec-schema.mjs — the ONE reader that DEFINES a well-formed spec document under docs/ai/specs/.
3
- //
4
- // Pure text in, verdict out: readSpecDocument(text, rel) never touches the filesystem and imports
5
- // nothing, so it seeds layout-free into any deployment. The navigator's collapse (check-docs-size.mjs)
6
- // and the future spec-check read through THIS module — "malformed" has one definition, never two.
7
- // `rel` is the path INSIDE docs/ai/specs/ (the store root navigator is `index.md`).
8
- //
9
- // SPEC_SCHEMA carries the frozen values; the engine canon (references/specs.md) is pinned against it.
10
- // A refusal names exactly one rule id per defect, so a fixture corpus can be read rule by rule.
2
+ // Canon: the methodology engine's references/specs.md (frozen schema, refusals table).
11
3
 
12
4
  export const SPEC_SCHEMA = Object.freeze({
13
5
  storePrefix: 'docs/ai/specs/',
@@ -34,17 +26,25 @@ export const SPEC_SCHEMA = Object.freeze({
34
26
  rootOwnedKeys: Object.freeze(['status', 'revision']),
35
27
  rootOwnedSections: Object.freeze(['## Scenarios', '## Out of scope', '## Module', '## Parts']),
36
28
  scenarioGrammar: '- S<N> <name> :: <repo-relative test path> :: spec:<slug>/S<N> | - S<N> <name> :: unbound',
29
+ openList: Object.freeze({
30
+ checkVerbs: Object.freeze(['refuse', 'refusal', 'REFUSE', 'REFUSAL']),
31
+ enumerationForms: Object.freeze([Object.freeze([',', ' or ']), Object.freeze([' · '])]),
32
+ closureTokens: Object.freeze([
33
+ 'only if', 'only when', 'the only', 'everything else', 'anything else', 'nothing else',
34
+ 'every other', 'any other', 'otherwise', 'exactly', 'one of', 'closed',
35
+ ]),
36
+ emphaticToken: 'ONLY',
37
+ }),
37
38
  rules: Object.freeze([
38
39
  'frontmatter', 'frontmatter-key', 'substrate-key', 'type', 'kind', 'maxlines', 'status', 'revision',
39
40
  'root-owns', 'slug', 'kind-path', 'root-uplink', 'title', 'section-missing', 'section-order',
40
- 'section-forbidden', 'fence', 'children-link', 'children-duplicate', 'fan-out', 'scenario-line',
41
+ 'section-forbidden', 'fence', 'open-list', 'children-link', 'children-duplicate', 'fan-out', 'scenario-line',
41
42
  'scenario-number', 'scenario-marker', 'scenario-path', 'scenarios-empty', 'out-of-scope', 'module-line', 'module-empty',
42
43
  'module-traversal', 'module-absolute', 'module-backslash', 'module-glob', 'module-mix', 'parts',
43
44
  ]),
44
45
  });
45
46
 
46
- // The descriptor check-docs-size.mjs joins to its ADR group: rows under `prefix` whose reader verdict
47
- // is clean collapse into ONE navigator row linking `navPath`; a row with reader errors stays visible.
47
+ // Consumer contract: scripts/check-docs-size.mjs.
48
48
  export const SPECS_COLLAPSE = Object.freeze({
49
49
  prefix: SPEC_SCHEMA.storePrefix,
50
50
  navPath: `${SPEC_SCHEMA.storePrefix}${SPEC_SCHEMA.navigatorFile}`,
@@ -72,8 +72,6 @@ const FORBIDDEN_SECTIONS = Object.freeze({
72
72
 
73
73
  const KNOWN_KEYS = Object.freeze([...SPEC_SCHEMA.substrateKeys, 'kind', ...SPEC_SCHEMA.rootOwnedKeys]);
74
74
 
75
- // The frontmatter is a closed key set: an unknown key, a repeated key or a line that is not
76
- // `key: value` is a defect (`frontmatter-key`), never silently dropped or last-one-wins.
77
75
  const parseFrontmatter = (text) => {
78
76
  const match = text.match(FRONTMATTER_RE);
79
77
  if (!match) return null;
@@ -91,10 +89,6 @@ const parseFrontmatter = (text) => {
91
89
 
92
90
  const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
93
91
 
94
- // The body as { title, sections: [{ heading, lines }], preamble, fenced } — the title is the FIRST
95
- // `# ` line, every `## ` line opens a section, the lines before the first section are the preamble.
96
- // The reader parses NO markdown code: a fence line is recorded (the `fence` refusal) rather than
97
- // modelled, so a spec carries no code sample and no line is ever ambiguous between code and structure.
98
92
  const parseBody = (body) => {
99
93
  const sections = [];
100
94
  const preamble = [];
@@ -116,8 +110,6 @@ const parseBody = (body) => {
116
110
  return { title: state.title, sections, preamble, fenced, sectionBeforeTitle: state.sectionBeforeTitle };
117
111
  };
118
112
 
119
- // Lexical classification of a repo-relative path field — the same vocabulary for module roots and
120
- // scenario bindings. Realpath/symlink containment needs the filesystem and is the checker's duty.
121
113
  export const classifyPath = (path) => {
122
114
  if (path.trim() === '') return 'empty';
123
115
  if (path.includes('\\')) return 'backslash';
@@ -139,8 +131,47 @@ const MODULE_PATH_RULES = Object.freeze({
139
131
  const isBullet = (line) => line === '-' || line.startsWith('- ');
140
132
  const bulletsOf = (lines) => lines.filter(isBullet).map((line) => line.slice(2));
141
133
  const contentOf = (lines) => lines.filter((line) => line.trim() !== '');
134
+ const isAtxHeading = (line) => /^ {0,3}#{1,6}(?:[ \t]|$)/.test(line);
135
+ const topLevelBulletsOf = (lines) => lines.reduce((state, raw) => {
136
+ const line = raw.replace(/\s+$/, '');
137
+ if (isBullet(line)) {
138
+ state.current = [line === '-' ? '' : line.slice(2)];
139
+ state.bullets.push(state.current);
140
+ } else if (line.trim() === '' || isAtxHeading(line)) state.current = null;
141
+ else if (state.current !== null) state.current.push(line.trim());
142
+ return state;
143
+ }, { bullets: [], current: null }).bullets.map((parts) => parts.join(' ').replace(/\s+/g, ' ').trim());
144
+
145
+ const CLAUSE_BREAK = String.fromCharCode(0);
146
+ const MASKED_DOT = String.fromCharCode(1);
147
+ const MASKED_SEMICOLON = String.fromCharCode(2);
148
+ const maskClauseSpans = (text) => [...text].reduce((state, character) => {
149
+ if (character === '`') state.inSpan = !state.inSpan;
150
+ state.output.push(state.inSpan && character === '.' ? MASKED_DOT : state.inSpan && character === ';' ? MASKED_SEMICOLON : character);
151
+ return state;
152
+ }, { inSpan: false, output: [] }).output.join('');
153
+ const clausesOf = (bullet) => maskClauseSpans(bullet).replace(/([.;][*`\)\]]*) /g, `$1${CLAUSE_BREAK}`).split(CLAUSE_BREAK)
154
+ .map((clause) => clause.split(MASKED_DOT).join('.').split(MASKED_SEMICOLON).join(';').trim()).filter(Boolean);
155
+ const containsInOrder = (text, tokens) => tokens.reduce((position, token) => {
156
+ const found = position === -1 ? -1 : text.indexOf(token, position);
157
+ return found === -1 ? -1 : found + token.length;
158
+ }, 0) !== -1;
159
+ const hasBoundedToken = (clause, token, flags) => new RegExp(`(^|[^A-Za-z0-9_-])${token}([^A-Za-z0-9_-]|$)`, flags).test(clause);
160
+ const checkOpenLists = (parsed, kind, errors) => {
161
+ const groups = kind === 'spec' ? parsed.sections.filter((section) => section.heading === '## Contract').map((section) => section.lines)
162
+ : kind === 'part' ? [parsed.preamble, ...parsed.sections.map((section) => section.lines)] : [];
163
+ for (const clause of groups.flatMap(topLevelBulletsOf).flatMap(clausesOf)) {
164
+ const hasVerb = SPEC_SCHEMA.openList.checkVerbs.some((verb) => clause.includes(verb));
165
+ const hasEnumeration = SPEC_SCHEMA.openList.enumerationForms.some((form) => containsInOrder(clause, form));
166
+ const hasClosure = SPEC_SCHEMA.openList.closureTokens.some((token) => hasBoundedToken(clause, token, 'i'))
167
+ || hasBoundedToken(clause, SPEC_SCHEMA.openList.emphaticToken, '');
168
+ if (hasVerb && hasEnumeration && !hasClosure) {
169
+ const prefix = clause.split(/\s+/).slice(0, 6).join(' ');
170
+ errors.push({ rule: 'open-list', message: `the clause "${prefix}…" is an open list — add a closing form to it such as "only if …; everything else …"` });
171
+ }
172
+ }
173
+ };
142
174
 
143
- // The slug a document owns: the file stem for a flat file, the folder name for an index.md.
144
175
  const describeRel = (rel) => {
145
176
  const segments = rel.split('/');
146
177
  const file = segments[segments.length - 1];
@@ -314,11 +345,7 @@ const checkParts = (parsed, at, errors) => {
314
345
  if (duplicate !== undefined) errors.push({ rule: 'parts', message: `part "${duplicate}" is listed twice` });
315
346
  };
316
347
 
317
- // The structure verdict (additive, slice 2a): the DETERMINISTIC extraction of what parsed, per
318
- // section, targets VERBATIM as written (`./x.md` and `./x/index.md` stay distinct strings). The
319
- // grammar is per line for scenarios/children/parts — a malformed line is simply absent — while the
320
- // module is a conjunction (ONE `dir/` root or an all-file list): prose, a refused path, a mix or
321
- // `*(empty)*` extracts null. `## Links` is free prose and is never extracted.
348
+ // Structure verdict: the same canon, "The frozen schema".
322
349
  const scenarioEntry = (line) => {
323
350
  const scenario = parseScenario(line);
324
351
  if (scenario === null) return null;
@@ -345,10 +372,6 @@ const extractStructure = (parsed) => ({
345
372
  module: extractModule(parsed),
346
373
  });
347
374
 
348
- // The verdict: { kind, status, revision, structure, errors: [{ rule, message }], warnings: [{ rule,
349
- // message }] }. Errors are collected past the first defect wherever later checks stay meaningful; a
350
- // missing or defective frontmatter and an unknown kind each end the read (structure stays null),
351
- // because no shape can be judged without them.
352
375
  export const readSpecDocument = (text, rel) => {
353
376
  const errors = [];
354
377
  const warnings = [];
@@ -372,6 +395,7 @@ export const readSpecDocument = (text, rel) => {
372
395
  const at = checkPath(rel, kind, errors);
373
396
  const parsed = parseBody(body);
374
397
  if (parsed.fenced.length > 0) errors.push({ rule: 'fence', message: `"${parsed.fenced[0]}" — a spec document carries no code fence` });
398
+ checkOpenLists(parsed, kind, errors);
375
399
  checkSections(parsed, kind, errors);
376
400
  if (kind === 'index') {
377
401
  checkChildren(parsed, errors);
@@ -32,6 +32,8 @@ const refuses = (text, rel, rule) => {
32
32
  const verdict = readSpecDocument(text, rel);
33
33
  expect(rulesOf(verdict)).toEqual([rule]);
34
34
  };
35
+ const withContract = (contract) => specDoc().replace('Accepts a credential pair.', contract);
36
+ const openListErrors = (contract, rel = 'login.md') => readSpecDocument(withContract(contract), rel).errors;
35
37
 
36
38
  describe('readSpecDocument — accept', () => {
37
39
  it('a flat draft spec reads clean with kind/status/revision', () => {
@@ -269,6 +271,69 @@ describe('readSpecDocument — the structure verdict (additive, slice 2a)', () =
269
271
  });
270
272
  });
271
273
 
274
+ describe('readSpecDocument — open-list clauses', () => {
275
+ const refusal = 'the clause "an anchor path reported modified, untracked…" is an open list — add a closing form to it such as "only if …; everything else …"';
276
+ const open = '- an anchor path reported modified, untracked or ignored is the named refusal `anchor-dirty`';
277
+ const closed = '- an anchor path is accepted only if `ls-files -v` lists it with tag `H` and `status` is silent — everything else, a path reported modified, untracked or ignored, is the named refusal `anchor-dirty`';
278
+
279
+ it('refuses the measured clause with the exact message; verb alone, form alone and an in-clause closure pass', () => {
280
+ expect(openListErrors(open)).toEqual([{ rule: 'open-list', message: refusal }]);
281
+ for (const text of [closed, '- refuse one case', '- alpha, beta or gamma']) expect(openListErrors(text)).toEqual([]);
282
+ });
283
+
284
+ it('joins continuation lines, collapses whitespace and keeps an indented bullet in its top-level bullet', () => {
285
+ for (const text of ['- refuse alpha,\n beta or ignored', '- refuse alpha,\n - beta or ignored']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
286
+ });
287
+
288
+ it('ends a bullet at a blank, the next top-level bullet or every ATX heading; Contract prose is ignored', () => {
289
+ for (const tail of ['\n\nEverything else closes', '\n- Everything else closes', ...Array.from({ length: 6 }, (_, i) => `\n${'#'.repeat(i + 1)} Everything else closes`)]) {
290
+ expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
291
+ }
292
+ expect(openListErrors('refuse alpha, beta or gamma')).toEqual([]);
293
+ });
294
+
295
+ it('ends a bullet at indented and tab-separated ATX headings', () => {
296
+ for (const tail of ['\n ## Everything else closes', '\n#\tEverything else closes']) expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
297
+ });
298
+
299
+ it('starts a new empty top-level bullet before following prose', () => {
300
+ for (const tail of ['\n- \nEverything else closes', '\n-\nonly if closes']) expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma' + tail), 'login.md'))).toEqual(['open-list']);
301
+ });
302
+
303
+ it('judges an open list in every Contract section', () => {
304
+ const duplicate = specDoc().replace('## Scenarios', '## Contract\n\n- refuse alpha, beta or gamma\n\n## Scenarios');
305
+ expect(rulesOf(readSpecDocument(duplicate, 'login.md')).includes('open-list')).toBe(true);
306
+ });
307
+
308
+ it('splits clauses at punctuation plus markdown tails, but not inside backtick spans', () => {
309
+ for (const tail of ['.** ', '.` ', '.) ', '.] ', '; ']) expect(rulesOf(readSpecDocument(withContract(`- refuse alpha, beta or gamma${tail}Everything else closes`), 'login.md'))).toEqual(['open-list']);
310
+ for (const text of ['- refuse `alpha. beta`, gamma or delta', '- refuse `alpha; beta`, gamma or delta', '- refuse `alpha. beta, gamma or delta']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
311
+ expect(openListErrors('- refuse alpha``. beta, gamma or delta')).toEqual([]);
312
+ });
313
+
314
+ it('keeps span content in judgement, and a closure in a neighbouring clause cannot lift a refusal', () => {
315
+ expect(rulesOf(readSpecDocument(withContract('- `refuse` alpha, beta or gamma'), 'login.md'))).toEqual(['open-list']);
316
+ expect(openListErrors('- refuse alpha, beta or gamma with `everything else`')).toEqual([]);
317
+ expect(rulesOf(readSpecDocument(withContract('- refuse alpha, beta or gamma. Everything else closes'), 'login.md'))).toEqual(['open-list']);
318
+ });
319
+
320
+ it('matches verbs, forms and closure tokens with the frozen case and boundary rules', () => {
321
+ for (const verb of ['refuse', 'refuses', 'refused', 'refusal', 'refusals', 'REFUSE', 'REFUSAL']) expect(rulesOf(readSpecDocument(withContract(`- ${verb} alpha, beta or gamma`), 'login.md'))).toEqual(['open-list']);
322
+ for (const text of ['- Refuse alpha, beta or gamma', '- refuse alpha, beta and gamma']) expect(openListErrors(text)).toEqual([]);
323
+ expect(rulesOf(readSpecDocument(withContract('- refuse alpha · beta'), 'login.md'))).toEqual(['open-list']);
324
+ for (const token of SPEC_SCHEMA.openList.closureTokens) expect(openListErrors(`- ${token.toUpperCase()}: refuse alpha, beta or gamma`)).toEqual([]);
325
+ for (const text of ['- refuse alpha, beta or gamma — none of these', '- refuse alpha, beta or gamma — closed-key', '- refuse alpha, beta or gamma — READ-ONLY', '- refuse alpha, beta or gamma — if only this', '- refuse alpha, beta or gamma — not only this']) expect(rulesOf(readSpecDocument(withContract(text), 'login.md'))).toEqual(['open-list']);
326
+ expect(openListErrors('- refuse alpha, beta or gamma — ONLY this')).toEqual([]);
327
+ });
328
+
329
+ it('judges every part bullet, never an index, and freezes every schema list and inner pair', () => {
330
+ expect(rulesOf(readSpecDocument(partDoc({ extra: `\n${open}\n` }), 'auth/login/sessions.md'))).toEqual(['open-list']);
331
+ expect(readSpecDocument(indexDoc({ children: [], preamble: `\n${open}\n` }), 'auth/index.md').errors).toEqual([]);
332
+ expect(SPEC_SCHEMA.openList).toEqual({ checkVerbs: ['refuse', 'refusal', 'REFUSE', 'REFUSAL'], enumerationForms: [[',', ' or '], [' · ']], closureTokens: ['only if', 'only when', 'the only', 'everything else', 'anything else', 'nothing else', 'every other', 'any other', 'otherwise', 'exactly', 'one of', 'closed'], emphaticToken: 'ONLY' });
333
+ expect([SPEC_SCHEMA.openList, SPEC_SCHEMA.openList.checkVerbs, SPEC_SCHEMA.openList.enumerationForms, ...SPEC_SCHEMA.openList.enumerationForms, SPEC_SCHEMA.openList.closureTokens].every(Object.isFrozen)).toBe(true);
334
+ });
335
+ });
336
+
272
337
  describe('classifyPath + the frozen constants', () => {
273
338
  it('classifies the lexical path forms', () => {
274
339
  expect(['src/', 'src/a.mjs', '../x', '/x', 'C:/x', 'a\\b', 'src/*.mjs'].map(classifyPath)).toEqual([
@@ -232,7 +232,8 @@ const backendSetLabel = (backends) =>
232
232
  // review slot can resolve or request reviewed|council (execute floors at solo|delegated), so the gate
233
233
  // reads the recipe, the requested recipe and the roster.
234
234
  const REVIEW_RECIPES = new Set(['reviewed', 'council']);
235
- const CONSULT_LINE = ' • Before every fold of a finding raised by a review member (a bridge backend or a placed lens): ASK that member whether the proposed fold solves it and adds no new problem; WAIT for its answer, READ it, then edit only as accepted or corrected — agy: agy-review --continue --decided @f --focus "Finding: <finding>. Proposed fold: <exact fold>. Does this proposed fold solve the finding and add no new problem? Reply accept, or correct with exact replacement text."; codex: fresh codex-review plan <consult-brief> written before the working tree changes; a placed lens: re-dispatch the same lens vehicle with the finding and the proposed fold. A self-review finding, or any finding when no review member ran, is folded directly — the exemption is the finding\'s ORIGIN, never the recipe word on the slot line.';
235
+ const CONSULT_LINE = ' • Before every fold of a finding raised by a review member (a bridge backend or a placed lens): ASK that member whether the proposed fold solves it and adds no new problem; WAIT for its answer, READ it, then edit only as accepted or corrected — agy: agy-review --continue --decided @f --focus "Finding: <finding>. Proposed fold: <exact fold>. Does this proposed fold solve the finding and add no new problem? If this finding is the second case against the same check, reply with the replacement invariant, not an added case. ' +
236
+ 'Reply accept, or correct with exact replacement text."; codex: fresh codex-review plan <consult-brief> with the same payload written before the working tree changes; a placed lens: re-dispatch the same lens vehicle with the same payload. A self-review finding, or any finding when no review member ran, is folded directly — the exemption is the finding\'s ORIGIN, never the recipe word on the slot line.';
236
237
  const ARMED_CONSULT_LINE = ' • ARMED pre-fold sequence for a bridge-raised finding: the round is open → dispatch that bridge\'s consult with a nonce → WAIT and READ → fold accepted or corrected → flow-writer consult-attestation <planId> --backend <id> --nonce <n> --proposed-fix-digest <the-sha256-of-the-fold-text> → then edit. A lens-raised finding re-dispatches the lens without a nonce, WAIT and READ, then edit as accepted or corrected — it mints no manifest and no attestation — only its per-round participation rides its internal-attestation.';
237
238
  // activity-aware (AD-046): the triage classification vocabulary rides EVERY review-backed activity;
238
239
  // the LEDGER pointer renders ONLY for plan-execution — the ledger is plan-execution-scoped (AD-045),
@@ -34,6 +34,8 @@ export const SCRIPT_PRIORS = Object.freeze([
34
34
  prior('spec-schema.mjs', '4.7.0', '4.7.0', '40b5b038d5ec5ed53c327c6d269d22fe5fa2bed99ae711fbf84306ad047be452'),
35
35
  prior('spec-schema.test.mjs', '4.7.0', '4.7.0', 'fde896419924223e54cfcabfdb1ef5807df5386fac700ed7c1463b6e7f81501b'),
36
36
  prior('check-docs-size.mjs', '4.6.0', '6.0.0', '22d020c3668cdbfb4cbc1f67a8a85b2baa2d0c4956808a50626d37409e81ab38'),
37
+ prior('spec-schema.mjs', '5.0.0', '7.2.1', '6a7d1e836c158baab5a412a1ad76d34891934dc28c88486c264a4b5d46f5892a'),
38
+ prior('spec-schema.test.mjs', '5.0.0', '7.2.1', '698564d7283281be1826019d214df475bb876687915de086818d8f5ece0e0336'),
37
39
  ]);
38
40
 
39
41
  export const digestOf = (bytes) => createHash('sha256').update(bytes).digest('hex');