@sabaiway/agent-workflow-kit 10.4.0 → 10.5.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 +55 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +7 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +69 -17
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +73 -2
- package/bridges/antigravity-cli-bridge/capability.json +2 -2
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +3 -0
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +89 -18
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +55 -2
- package/bridges/codex-cli-bridge/capability.json +2 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/review-lens.md +5 -3
- package/references/modes/agents.md +1 -1
- package/references/modes/procedures.md +9 -5
- package/references/modes/recipes.md +2 -2
- package/references/modes/set-recipe.md +4 -4
- package/references/modes/status.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/references/templates/orchestration.json +1 -1
- package/tools/bridge-posture.mjs +48 -0
- package/tools/carriers.mjs +21 -9
- package/tools/cheap-agents-read.mjs +86 -24
- package/tools/cheap-agents.mjs +47 -7
- package/tools/detect-backends.mjs +2 -2
- package/tools/direct-run.mjs +3 -0
- package/tools/fold-scope.mjs +5 -60
- package/tools/grounding.mjs +2 -2
- package/tools/orchestration-config.mjs +19 -78
- package/tools/orchestration-readme.mjs +70 -0
- package/tools/plan-shape-cli.mjs +112 -0
- package/tools/plan-shape-facts.mjs +204 -0
- package/tools/plan-shape.mjs +348 -0
- package/tools/procedures.mjs +132 -31
- package/tools/recipes.mjs +60 -79
- package/tools/repo-lex.mjs +40 -0
- package/tools/review-roster-resolve.mjs +104 -0
- package/tools/review-roster.mjs +128 -0
- package/tools/review-rounds-cli.mjs +92 -0
- package/tools/review-rounds.mjs +115 -0
- package/tools/set-recipe-roster.mjs +167 -0
- package/tools/set-recipe.mjs +80 -23
- package/tools/velocity-profile.mjs +8 -22
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { tokenizeMarkdown } from '../references/scripts/markdown-blocks.mjs';
|
|
2
|
+
|
|
3
|
+
export const PLAN_TITLE_PREFIX = '# Plan: ';
|
|
4
|
+
export const PLAN_HEADINGS = Object.freeze([
|
|
5
|
+
'## Goal and boundary',
|
|
6
|
+
'## Module ledger',
|
|
7
|
+
'## Verification',
|
|
8
|
+
'## Phase: Cleanup',
|
|
9
|
+
'## Next steps',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const MAX_PLAN_LINES = 100;
|
|
13
|
+
const MAX_LEDGER_ROWS = 25;
|
|
14
|
+
const MAX_ROW_BYTES = 200;
|
|
15
|
+
const SECTION_CAPS = Object.freeze({
|
|
16
|
+
'## Goal and boundary': 10,
|
|
17
|
+
'## Module ledger': 60,
|
|
18
|
+
'## Verification': 20,
|
|
19
|
+
});
|
|
20
|
+
const CLEANUP_NEXT_CAP = 10;
|
|
21
|
+
const ROW_ID = /^[A-Za-z0-9._-]+$/;
|
|
22
|
+
const VERBS = new Set(['create', 'modify', 'delete']);
|
|
23
|
+
const GLOB_BYTE = /[*?[{]/;
|
|
24
|
+
const TOTAL_LINE = /^total:\s+(~?\d+)\s+→\s+(~?\d+)\s+lines(?:\s+.+)?$/;
|
|
25
|
+
const SPEC_LINE = /docs\/ai\/specs\/|(?:^|\W)(?:not adopted|adopting|nothing spec-covered touched)(?:\W|$)/;
|
|
26
|
+
const TEST_PATH = /(?:^|\/)\S+\.test\.[^/]+$/;
|
|
27
|
+
const EXTENSION_PHASE = /^## Phase: (?!Cleanup\s*$)\S(?:.*\S)?$/;
|
|
28
|
+
const BULLET = /^-\s+\S/;
|
|
29
|
+
|
|
30
|
+
export const getLineCount = (text) => text === '' ? 0 : text.split('\n').length - (text.endsWith('\n') ? 1 : 0);
|
|
31
|
+
export const isSweep = (path) => GLOB_BYTE.test(path);
|
|
32
|
+
const endsWithPath = (path, suffix) => path === suffix || path.endsWith(`/${suffix}`);
|
|
33
|
+
export const unique = (items) => [...new Set(items)];
|
|
34
|
+
const getFindingLine = (document, index) => document.frontLines + index + 1;
|
|
35
|
+
const makeFinding = (line, code, message, rowId = null) => ({ line, code, message, rowId });
|
|
36
|
+
const getHeading = (document, text) => document.headings.find((heading) => heading.text === text);
|
|
37
|
+
const getNextSection = (document, heading) => document.headings.find((item) => item.index > heading.index && item.level <= 2);
|
|
38
|
+
const getBodyEnd = (document) => document.lines.length - (document.lines.at(-1) === '' ? 1 : 0);
|
|
39
|
+
const getSectionEnd = (document, heading) => getNextSection(document, heading)?.index ?? getBodyEnd(document);
|
|
40
|
+
const getSectionLines = (document, headingText) => {
|
|
41
|
+
const heading = getHeading(document, headingText);
|
|
42
|
+
return heading ? document.lines.slice(heading.index + 1, getSectionEnd(document, heading)) : [];
|
|
43
|
+
};
|
|
44
|
+
const getSectionSpan = (document, headingText) => {
|
|
45
|
+
const heading = getHeading(document, headingText);
|
|
46
|
+
return heading ? getSectionEnd(document, heading) - heading.index : 0;
|
|
47
|
+
};
|
|
48
|
+
const getBudget = (value) => {
|
|
49
|
+
const parsed = /^\d+$/.test(value) ? Number(value) : null;
|
|
50
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
51
|
+
};
|
|
52
|
+
const getFigure = (value) => Number(String(value).replace(/^~/, ''));
|
|
53
|
+
const getConcretePaths = (row, facts) => isSweep(row.path) ? facts.expansions?.[row.path] ?? [] : [row.path];
|
|
54
|
+
const getPathFact = (facts, path) => facts.pathFacts?.[path] ?? null;
|
|
55
|
+
const getAnchorPath = (anchor) => anchor.replace(/:[1-9]\d*$/, '');
|
|
56
|
+
const hasPathDefect = (path) => path.startsWith('/') || path.includes('\\') || path.split('/').includes('..');
|
|
57
|
+
const hasRaiseBullet = (verificationBullets, path) => verificationBullets.some((bullet) =>
|
|
58
|
+
bullet.includes('--write-baseline') && bullet.includes('--reason') && bullet.includes(path));
|
|
59
|
+
|
|
60
|
+
// fenceContinues (queue-audit's reader): only a NESTED fence continues an open bullet — a column-0
|
|
61
|
+
// fence is a document block and closes it; `gaps` records where an absorbed run sat, so no reader
|
|
62
|
+
// joins text from both sides of a code block into one claim.
|
|
63
|
+
export const bulletBlocks = (lines, fencedLines, from, to, { fenceContinues = false } = {}) => {
|
|
64
|
+
const indexes = Array.from({ length: Math.max(0, to - from) }, (_, offset) => from + offset);
|
|
65
|
+
const reduced = indexes.reduce((state, index) => {
|
|
66
|
+
if (fencedLines.has(index)) {
|
|
67
|
+
const absorbing = state.absorbing ?? Boolean(fenceContinues && state.current && /^\s+\S/.test(lines[index]));
|
|
68
|
+
if (!absorbing) return { blocks: state.current ? [...state.blocks, state.current] : state.blocks, current: null, absorbing };
|
|
69
|
+
const gaps = new Set([...state.current.gaps, state.current.lines.length - 1]);
|
|
70
|
+
return { blocks: state.blocks, current: { ...state.current, span: state.current.span + 1, gaps }, absorbing };
|
|
71
|
+
}
|
|
72
|
+
const line = lines[index];
|
|
73
|
+
if (BULLET.test(line)) {
|
|
74
|
+
return {
|
|
75
|
+
blocks: state.current ? [...state.blocks, state.current] : state.blocks,
|
|
76
|
+
current: { start: index, lines: [line], span: 1, gaps: new Set() },
|
|
77
|
+
absorbing: null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (state.current && (line.trim() === '' || /^\s+\S/.test(line))) {
|
|
81
|
+
return { blocks: state.blocks, current: { ...state.current, lines: [...state.current.lines, line], span: state.current.span + 1 }, absorbing: null };
|
|
82
|
+
}
|
|
83
|
+
return { blocks: state.current ? [...state.blocks, state.current] : state.blocks, current: null, absorbing: null };
|
|
84
|
+
}, { blocks: [], current: null, absorbing: null });
|
|
85
|
+
return reduced.current ? [...reduced.blocks, reduced.current] : reduced.blocks;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const parseRow = (raw, line) => {
|
|
89
|
+
const parts = raw.split(' | ').map((part) => part.trim());
|
|
90
|
+
if (parts.length !== 6) return { raw, line, valid: false, id: parts[0] || null };
|
|
91
|
+
const [id, verb, path, responsibility, budget, anchor] = parts;
|
|
92
|
+
const budgetValid = budget === 'n/a' || budget === '—' || getBudget(budget) !== null;
|
|
93
|
+
const deleteValid = verb === 'delete'
|
|
94
|
+
? budget === '—' && anchor === '—'
|
|
95
|
+
: budget !== '—' && anchor !== '—' && anchor.length > 0;
|
|
96
|
+
const valid = ROW_ID.test(id) && VERBS.has(verb) && path.length > 0 && responsibility.length > 0 && budgetValid && deleteValid;
|
|
97
|
+
return {
|
|
98
|
+
raw,
|
|
99
|
+
line,
|
|
100
|
+
valid,
|
|
101
|
+
id,
|
|
102
|
+
verb,
|
|
103
|
+
path,
|
|
104
|
+
responsibility,
|
|
105
|
+
budget,
|
|
106
|
+
budgetLines: getBudget(budget),
|
|
107
|
+
anchor,
|
|
108
|
+
anchorPath: anchor === '—' ? null : getAnchorPath(anchor),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const parseLedger = (text, suppliedDocument = null) => {
|
|
113
|
+
const document = suppliedDocument ?? tokenizeMarkdown(String(text ?? ''), 'the plan');
|
|
114
|
+
const start = getHeading(document, PLAN_HEADINGS[1]);
|
|
115
|
+
const end = getHeading(document, PLAN_HEADINGS[2]);
|
|
116
|
+
if (!start || !end || end.index <= start.index) return { rows: [], total: null, entries: [], document };
|
|
117
|
+
const entries = document.lines.slice(start.index + 1, end.index).map((raw, offset) => ({
|
|
118
|
+
raw,
|
|
119
|
+
index: start.index + 1 + offset,
|
|
120
|
+
line: getFindingLine(document, start.index + 1 + offset),
|
|
121
|
+
}));
|
|
122
|
+
const rows = entries.filter((entry) => entry.raw.includes(' | ')).map((entry) => parseRow(entry.raw, entry.line));
|
|
123
|
+
const totals = entries.filter((entry) => entry.raw.trim().startsWith('total:'));
|
|
124
|
+
const totalMatch = totals.length === 1 ? TOTAL_LINE.exec(totals[0].raw.trim()) : null;
|
|
125
|
+
const total = totalMatch ? {
|
|
126
|
+
line: totals[0].line,
|
|
127
|
+
beforeRaw: totalMatch[1],
|
|
128
|
+
afterRaw: totalMatch[2],
|
|
129
|
+
before: getFigure(totalMatch[1]),
|
|
130
|
+
after: getFigure(totalMatch[2]),
|
|
131
|
+
} : null;
|
|
132
|
+
return { rows, total, totals, entries, document };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const getVerificationBullets = (document) => {
|
|
136
|
+
const heading = getHeading(document, PLAN_HEADINGS[2]);
|
|
137
|
+
if (!heading) return [];
|
|
138
|
+
return bulletBlocks(document.lines, document.fencedLines, heading.index + 1, getSectionEnd(document, heading))
|
|
139
|
+
.map((block) => block.lines.join('\n').replace(/^\-\s+/, '').replace(/\s+/g, ' ').trim());
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const checkHeadings = (text, document) => {
|
|
143
|
+
const findings = [];
|
|
144
|
+
const first = document.headings[0];
|
|
145
|
+
if (!first || first.level !== 1 || !first.text.startsWith(PLAN_TITLE_PREFIX) || first.text.slice(PLAN_TITLE_PREFIX.length).trim() === '') {
|
|
146
|
+
findings.push(makeFinding(first ? getFindingLine(document, first.index) : 1, 'title', `the first heading must open with "${PLAN_TITLE_PREFIX}" and a non-empty title`));
|
|
147
|
+
}
|
|
148
|
+
const secondLevel = document.headings.filter((heading) => heading.level === 2);
|
|
149
|
+
for (const literal of PLAN_HEADINGS) {
|
|
150
|
+
const matches = secondLevel.filter((heading) => heading.text === literal);
|
|
151
|
+
if (matches.length !== 1) findings.push(makeFinding(matches[0] ? getFindingLine(document, matches[0].index) : 1, 'headings', `${literal} must appear exactly once`));
|
|
152
|
+
}
|
|
153
|
+
const positions = PLAN_HEADINGS.map((literal) => secondLevel.findIndex((heading) => heading.text === literal));
|
|
154
|
+
if (positions.every((position) => position >= 0) && positions.some((position, index) => index > 0 && position <= positions[index - 1])) {
|
|
155
|
+
findings.push(makeFinding(1, 'headings', 'the five plan sections must keep their literal order'));
|
|
156
|
+
}
|
|
157
|
+
for (const heading of secondLevel) {
|
|
158
|
+
if (PLAN_HEADINGS.includes(heading.text)) continue;
|
|
159
|
+
const verification = positions[2];
|
|
160
|
+
const cleanup = positions[3];
|
|
161
|
+
const position = secondLevel.indexOf(heading);
|
|
162
|
+
if (!EXTENSION_PHASE.test(heading.text) || verification < 0 || cleanup < 0 || position <= verification || position >= cleanup) {
|
|
163
|
+
findings.push(makeFinding(getFindingLine(document, heading.index), 'headings', `${heading.text} is not an admitted extension phase between Verification and Cleanup`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (getLineCount(text) > MAX_PLAN_LINES) findings.push(makeFinding(1, 'line-cap', `the plan has ${getLineCount(text)} lines; the cap is ${MAX_PLAN_LINES}`));
|
|
167
|
+
for (const [heading, cap] of Object.entries(SECTION_CAPS)) {
|
|
168
|
+
const span = getSectionSpan(document, heading);
|
|
169
|
+
if (span > cap) findings.push(makeFinding(getFindingLine(document, getHeading(document, heading).index), 'section-cap', `${heading} spans ${span} lines; the cap is ${cap}`));
|
|
170
|
+
}
|
|
171
|
+
const cleanupNext = getSectionSpan(document, PLAN_HEADINGS[3]) + getSectionSpan(document, PLAN_HEADINGS[4]);
|
|
172
|
+
if (cleanupNext > CLEANUP_NEXT_CAP) findings.push(makeFinding(1, 'section-cap', `Cleanup and Next steps span ${cleanupNext} lines; their shared cap is ${CLEANUP_NEXT_CAP}`));
|
|
173
|
+
return findings;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const checkLedgerStructure = (parsed, facts) => {
|
|
177
|
+
const findings = [];
|
|
178
|
+
const nonBlank = parsed.entries.filter((entry) => entry.raw.trim() !== '');
|
|
179
|
+
const rowLines = new Set(parsed.rows.map((row) => row.line));
|
|
180
|
+
const totalLines = new Set((parsed.totals ?? []).map((total) => total.line));
|
|
181
|
+
for (const entry of nonBlank) {
|
|
182
|
+
if (!rowLines.has(entry.line) && !totalLines.has(entry.line)) findings.push(makeFinding(entry.line, 'ledger-line', 'a ledger line must be a six-field row or the total line'));
|
|
183
|
+
}
|
|
184
|
+
for (const row of parsed.rows) {
|
|
185
|
+
if (!row.valid) findings.push(makeFinding(row.line, 'row-grammar', 'the row must carry six valid fields and delete rows alone use — for budget and anchor', row.id));
|
|
186
|
+
}
|
|
187
|
+
if (parsed.rows.length > MAX_LEDGER_ROWS) findings.push(makeFinding(parsed.rows[MAX_LEDGER_ROWS].line, 'row-cap', `the ledger has ${parsed.rows.length} rows; the cap is ${MAX_LEDGER_ROWS}`));
|
|
188
|
+
const ids = new Map();
|
|
189
|
+
for (const row of parsed.rows.filter((item) => item.id)) {
|
|
190
|
+
const prior = ids.get(row.id);
|
|
191
|
+
if (prior) findings.push(makeFinding(row.line, 'duplicate-id', `${row.id} duplicates the row on line ${prior}`, row.id));
|
|
192
|
+
else ids.set(row.id, row.line);
|
|
193
|
+
}
|
|
194
|
+
for (const row of parsed.rows.filter((item) => item.valid)) {
|
|
195
|
+
const countedBytes = Buffer.byteLength([row.id, row.verb, row.responsibility, row.budget].join(' | '), 'utf8');
|
|
196
|
+
if (countedBytes > MAX_ROW_BYTES) findings.push(makeFinding(row.line, 'row-bytes', `${row.id} has ${countedBytes} counted bytes; the cap is ${MAX_ROW_BYTES}`, row.id));
|
|
197
|
+
const contained = !hasPathDefect(row.path) && getPathFact(facts, row.path)?.contained !== false;
|
|
198
|
+
if (!contained) findings.push(makeFinding(row.line, 'containment', `${row.id} path must be a contained repo-relative POSIX path`, row.id));
|
|
199
|
+
const anchorContained = row.anchorPath === null || (!hasPathDefect(row.anchorPath) && getPathFact(facts, row.anchorPath)?.contained !== false);
|
|
200
|
+
if (!anchorContained) findings.push(makeFinding(row.line, 'containment', `${row.id} anchor must be a contained repo-relative POSIX path`, row.id));
|
|
201
|
+
if (isSweep(row.path) && (row.verb !== 'modify' || !/\([1-9]\d* files\)/.test(row.responsibility))) {
|
|
202
|
+
findings.push(makeFinding(row.line, 'sweep', `${row.id} sweep must use modify and assert its count as (N files)`, row.id));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const owners = new Map();
|
|
206
|
+
for (const row of parsed.rows.filter((item) => item.valid)) {
|
|
207
|
+
for (const path of getConcretePaths(row, facts)) {
|
|
208
|
+
const prior = owners.get(path);
|
|
209
|
+
if (prior) findings.push(makeFinding(row.line, 'duplicate-path', `${path} is owned by both ${prior} and ${row.id}`, row.id));
|
|
210
|
+
else owners.set(path, row.id);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const last = nonBlank.at(-1);
|
|
214
|
+
if (!parsed.total || (parsed.totals ?? []).length !== 1 || last?.line !== parsed.total?.line) {
|
|
215
|
+
findings.push(makeFinding(last?.line ?? 1, 'total', 'the exact total line must be the last non-blank ledger line'));
|
|
216
|
+
}
|
|
217
|
+
const bullets = getVerificationBullets(parsed.document);
|
|
218
|
+
if (bullets.length === 0) findings.push(makeFinding(getHeading(parsed.document, PLAN_HEADINGS[2]) ? getFindingLine(parsed.document, getHeading(parsed.document, PLAN_HEADINGS[2]).index) : 1, 'acceptance', 'Verification must carry at least one top-level - bullet'));
|
|
219
|
+
if (!getSectionLines(parsed.document, PLAN_HEADINGS[0]).some((line) => SPEC_LINE.test(line))) {
|
|
220
|
+
findings.push(makeFinding(getHeading(parsed.document, PLAN_HEADINGS[0]) ? getFindingLine(parsed.document, getHeading(parsed.document, PLAN_HEADINGS[0]).index) : 1, 'governing-spec', 'Goal and boundary must name a governing spec path or the adopted state'));
|
|
221
|
+
}
|
|
222
|
+
return findings;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const resolveAnchorCandidates = (repoFiles, suffix, precedingPaths) => {
|
|
226
|
+
if (repoFiles.includes(suffix)) return [suffix];
|
|
227
|
+
const above = unique(precedingPaths.filter((path) => endsWithPath(path, suffix)));
|
|
228
|
+
return above.length > 0 ? above : unique(repoFiles.filter((path) => endsWithPath(path, suffix)));
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const getAnchorCandidates = (row, preceding, facts) => {
|
|
232
|
+
const suffix = row.anchorPath;
|
|
233
|
+
if (!suffix || hasPathDefect(suffix)) return [];
|
|
234
|
+
const precedingPaths = preceding.filter((item) => item.valid).flatMap((item) => getConcretePaths(item, facts));
|
|
235
|
+
return facts.candidates(suffix, precedingPaths);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const getCurrentLines = (rows, facts, verbs) => rows
|
|
239
|
+
.filter((row) => row.valid && verbs.has(row.verb) && row.budget !== 'n/a')
|
|
240
|
+
.flatMap((row) => getConcretePaths(row, facts))
|
|
241
|
+
.reduce((sum, path) => sum + (getPathFact(facts, path)?.lines ?? 0), 0);
|
|
242
|
+
|
|
243
|
+
const checkAuthoring = (parsed, facts) => {
|
|
244
|
+
const findings = [];
|
|
245
|
+
const bullets = getVerificationBullets(parsed.document);
|
|
246
|
+
for (const [index, row] of parsed.rows.entries()) {
|
|
247
|
+
if (!row.valid) continue;
|
|
248
|
+
const paths = getConcretePaths(row, facts);
|
|
249
|
+
const expectedKind = row.verb === 'create' ? 'absent' : 'regular';
|
|
250
|
+
if (paths.length === 0 && isSweep(row.path)) findings.push(makeFinding(row.line, 'kind', `${row.id} sweep resolves to no regular path`, row.id));
|
|
251
|
+
for (const path of paths) {
|
|
252
|
+
const observed = getPathFact(facts, path)?.kind ?? 'unknown';
|
|
253
|
+
if (observed !== expectedKind) findings.push(makeFinding(row.line, 'kind', `${row.id} expects ${expectedKind}, observed ${observed} at ${path}`, row.id));
|
|
254
|
+
}
|
|
255
|
+
const anchorCandidates = row.anchorPath ? getAnchorCandidates(row, parsed.rows.slice(0, index), facts) : null;
|
|
256
|
+
if (anchorCandidates && anchorCandidates.length !== 1) {
|
|
257
|
+
const count = anchorCandidates.length;
|
|
258
|
+
findings.push(makeFinding(row.line, 'anchor', `${row.id} anchor resolves to ${count} candidates; make it unique`, row.id));
|
|
259
|
+
}
|
|
260
|
+
if (row.verb !== 'delete' && row.budgetLines !== null && !facts.capDeclared) {
|
|
261
|
+
findings.push(makeFinding(row.line, 'cap-declaration', `${row.id} uses an integer budget but no source-size cap is declared; use n/a`, row.id));
|
|
262
|
+
}
|
|
263
|
+
for (const path of paths) {
|
|
264
|
+
const pathFact = getPathFact(facts, path);
|
|
265
|
+
if (!pathFact?.inScope) continue;
|
|
266
|
+
if (row.budget === 'n/a') findings.push(makeFinding(row.line, 'source-budget', `${row.id} is in source scope and cannot use n/a`, row.id));
|
|
267
|
+
if (row.verb === 'create' && !TEST_PATH.test(path)) {
|
|
268
|
+
const hasEarlierTest = parsed.rows.slice(0, index).some((prior) => prior.valid && ['create', 'modify'].includes(prior.verb) && TEST_PATH.test(prior.path));
|
|
269
|
+
if (!hasEarlierTest) findings.push(makeFinding(row.line, 'red-first', `${row.id} needs a create or modify test row above it`, row.id));
|
|
270
|
+
}
|
|
271
|
+
const ceiling = row.verb === 'modify' && pathFact.recordedLines != null ? pathFact.recordedLines : facts.cap;
|
|
272
|
+
if (row.budgetLines !== null && ceiling != null && row.budgetLines > ceiling && !hasRaiseBullet(bullets, row.path)) {
|
|
273
|
+
findings.push(makeFinding(row.line, 'budget-cap', `${row.id} budget ${row.budgetLines} exceeds ${ceiling}; name a reasoned baseline raise for ${row.path}`, row.id));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (row.verb === 'create') {
|
|
277
|
+
const pathFact = getPathFact(facts, row.path);
|
|
278
|
+
if (pathFact?.shipped === true && pathFact.pinTest && !parsed.rows.some((candidate) => candidate.valid && candidate.verb === 'modify' && candidate.path === pathFact.pinTest)) {
|
|
279
|
+
findings.push(makeFinding(row.line, 'package-pin', `${row.id} ships but no modify row owns ${pathFact.pinTest}`, row.id));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (parsed.total) {
|
|
284
|
+
const current = getCurrentLines(parsed.rows, facts, new Set(['modify', 'delete']));
|
|
285
|
+
if (parsed.total.before !== current) findings.push(makeFinding(parsed.total.line, 'before-total', `before is ${parsed.total.before}; current counted paths sum to ${current}`));
|
|
286
|
+
}
|
|
287
|
+
return { findings, skips: parsed.rows.filter((row) => row.verb === 'create').flatMap((row) =>
|
|
288
|
+
getPathFact(facts, row.path)?.pinSkip ? [`${row.id}: package pin skipped — ${getPathFact(facts, row.path).pinSkip}`] : []) };
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const checkPostState = (parsed, facts) => {
|
|
292
|
+
const findings = [];
|
|
293
|
+
for (const row of parsed.rows.filter((item) => item.valid)) {
|
|
294
|
+
const paths = getConcretePaths(row, facts);
|
|
295
|
+
if (isSweep(row.path)) {
|
|
296
|
+
const expected = Number(/\(([1-9]\d*) files\)/.exec(row.responsibility)?.[1]);
|
|
297
|
+
if (paths.length === 0 || paths.length !== expected) findings.push(makeFinding(row.line, 'sweep-count', `${row.id} expected ${expected} files and resolves to ${paths.length}`, row.id));
|
|
298
|
+
}
|
|
299
|
+
for (const path of paths) {
|
|
300
|
+
const fact = getPathFact(facts, path);
|
|
301
|
+
const expectedKind = row.verb === 'delete' ? 'absent' : 'regular';
|
|
302
|
+
if (fact?.kind !== expectedKind) findings.push(makeFinding(row.line, 'post-kind', `${row.id} expects ${expectedKind}, observed ${fact?.kind ?? 'unknown'} at ${path}`, row.id));
|
|
303
|
+
if (row.verb !== 'delete' && row.budgetLines !== null && fact?.kind === 'regular' && fact.lines > row.budgetLines) {
|
|
304
|
+
findings.push(makeFinding(row.line, 'post-budget', `${path} has ${fact.lines} lines, above ${row.id} budget ${row.budgetLines}`, row.id));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (parsed.total) {
|
|
309
|
+
const current = getCurrentLines(parsed.rows, facts, new Set(['create', 'modify']));
|
|
310
|
+
if (parsed.total.after < current) findings.push(makeFinding(parsed.total.line, 'after-total', `after is ${parsed.total.after}; counted paths sum to ${current}`));
|
|
311
|
+
}
|
|
312
|
+
return { findings, skips: [] };
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const parseForJudge = (text) => {
|
|
316
|
+
try {
|
|
317
|
+
return { parsed: parseLedger(text), error: null };
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return { parsed: null, error };
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const judge = (text, facts, stateRules) => {
|
|
324
|
+
const parsedResult = parseForJudge(text);
|
|
325
|
+
if (parsedResult.error) {
|
|
326
|
+
const line = Number(/:(\d+):/.exec(parsedResult.error.message)?.[1] ?? 1);
|
|
327
|
+
return { findings: [makeFinding(line, 'block-model', parsedResult.error.message)], skips: [], parsed: null };
|
|
328
|
+
}
|
|
329
|
+
const parsed = parsedResult.parsed;
|
|
330
|
+
const structural = [...checkHeadings(String(text ?? ''), parsed.document), ...checkLedgerStructure(parsed, facts)];
|
|
331
|
+
const state = stateRules ? stateRules(parsed, facts) : { findings: [], skips: [] };
|
|
332
|
+
const findings = [...structural, ...state.findings].map((finding, index) => ({ ...finding, order: index }))
|
|
333
|
+
.sort((left, right) => left.line - right.line || left.order - right.order)
|
|
334
|
+
.map(({ order, ...finding }) => finding);
|
|
335
|
+
return { findings, skips: state.skips, parsed };
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export const checkPlan = (text, facts) => judge(text, facts, checkAuthoring);
|
|
339
|
+
export const verifyPlan = (text, facts) => judge(text, facts, checkPostState);
|
|
340
|
+
export const checkPlanStructure = (text, facts) => judge(text, facts, null);
|
|
341
|
+
|
|
342
|
+
export const formatFindings = (result, label = 'plan') => {
|
|
343
|
+
const lines = result.findings.length === 0
|
|
344
|
+
? [`plan-shape: ACCEPT — ${label}`]
|
|
345
|
+
: [`plan-shape: REFUSE — ${label}`, ...result.findings.map((finding) =>
|
|
346
|
+
`${label}:${finding.line}: ${finding.rowId ? `${finding.rowId}: ` : ''}${finding.code}: ${finding.message}`)];
|
|
347
|
+
return [...lines, ...result.skips.map((skip) => `plan-shape: SKIP — ${skip}`)].join('\n');
|
|
348
|
+
};
|
package/tools/procedures.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { resolveEngineDir, readEngineFragment, PROCEDURES_FRAGMENT_REL } from '.
|
|
|
35
35
|
import { plansInFlight, PLANS_REL } from './plan-files.mjs';
|
|
36
36
|
// The family's ONE shell quoter for a RENDERED command operand (bare when the value is already safe,
|
|
37
37
|
// single-quoted otherwise) — the same leaf eight other command renderers here read through.
|
|
38
|
-
import { shellQuoteArg } from './repo-lex.mjs';
|
|
38
|
+
import { shellQuoteArg, isSeedablePathToken, isRenderableLine, escapeForDisplay, isArtifactPathCarriable } from './repo-lex.mjs';
|
|
39
39
|
// The config schema/read core (orchestration-config.mjs, the single config contract): the reader +
|
|
40
40
|
// the SHARED slot/recipe validity, never the fs-writer (orchestration-write.mjs) DIRECTLY — the
|
|
41
41
|
// import-split test pins the direct-import rule.
|
|
@@ -47,6 +47,7 @@ import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from
|
|
|
47
47
|
// structural — test/read-graph-purity.test.mjs pins it).
|
|
48
48
|
import { resolveFlowStorePath, readFlowStore } from './flow-store-read.mjs';
|
|
49
49
|
import { CHAIN_KIND } from './flow-record.mjs';
|
|
50
|
+
import { readRegistration } from './mcp-registration.mjs';
|
|
50
51
|
// The declared source-size practice (D-17 U1), read through the practice's PURE READ core — never
|
|
51
52
|
// source-size-check.mjs, which owns the writer half: this advisor is a read root of
|
|
52
53
|
// test/read-graph-purity.test.mjs, and the core exists so a surface can ask without reaching a writer.
|
|
@@ -166,7 +167,7 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) => {
|
|
|
166
167
|
// planRecipe's drift-guarded dispatch for WHICH backends, then resolve each (backend, role) to its
|
|
167
168
|
// manifest wrapper cmd via the bridge registry — no wrapper name is hand-composed here. A vehicle
|
|
168
169
|
// step is NOT a bridge: it carries its own state and is never looked up in a manifest.
|
|
169
|
-
const { dispatch } = planRecipe(resolved.recipe, detection);
|
|
170
|
+
const { dispatch } = resolved.roster ? { dispatch: [] } : planRecipe(resolved.recipe, detection);
|
|
170
171
|
const vehicles = dispatch.filter((d) => d.vehicle != null).map((d) => ({ backend: d.backend, state: d.vehicle }));
|
|
171
172
|
const bridged = dispatch.filter((d) => d.vehicle == null);
|
|
172
173
|
const backends = bridged.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
|
|
@@ -189,8 +190,9 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) => {
|
|
|
189
190
|
// carrier-typed slot (registry-driven — no slot name is spelled here), solo when it has none.
|
|
190
191
|
const effectiveCarrier = (slots) => slots.find((s) => s.slotType === 'carrier')?.recipe ?? 'solo';
|
|
191
192
|
|
|
192
|
-
// An unsatisfiable EXPLICIT override is
|
|
193
|
-
//
|
|
193
|
+
// An unsatisfiable EXPLICIT override is a warning (loud, flagged for the agent to relay); a graceful
|
|
194
|
+
// config/default degradation remains a per-slot reason. Canon/registry slot-skew warnings are
|
|
195
|
+
// collected separately, after the live section is read.
|
|
194
196
|
const collectWarnings = (slots) =>
|
|
195
197
|
slots
|
|
196
198
|
.filter((s) => s.overrideUnsatisfied)
|
|
@@ -216,24 +218,34 @@ const backendSetLabel = (backends) =>
|
|
|
216
218
|
: ` → ${backends[0]}`;
|
|
217
219
|
|
|
218
220
|
// The review-loop economics block (M1 + M6's firing half) — printed when the activity engages a review
|
|
219
|
-
//
|
|
221
|
+
// member: a slot RESOLVING reviewed | council, a roster, or one whose config or override REQUESTED a
|
|
222
|
+
// review recipe and degraded (readiness removes no configured obligation, and the table still judges
|
|
223
|
+
// it); omitted only for a solo nobody asked to be otherwise. It paraphrases the procedures.md
|
|
220
224
|
// Fold + loop step + orchestration §4 canon (no rival rule): the ≤2-round architecture cap, the bar met by RAISING a
|
|
221
225
|
// surviving major to an acceptance invariant (not exhausting prose), backend divergence = the crossover
|
|
222
226
|
// stop, the thin-plan/diff-review carve-out, a self-consistency read before every re-review, and the
|
|
223
227
|
// REQUIRED per-round structured emission {round N · finding-origin tally · per-backend verdict}. Only a
|
|
224
|
-
// review slot can resolve reviewed|council (execute floors at solo|delegated), so
|
|
228
|
+
// review slot can resolve or request reviewed|council (execute floors at solo|delegated), so the gate
|
|
229
|
+
// reads the recipe, the requested recipe and the roster.
|
|
225
230
|
const REVIEW_RECIPES = new Set(['reviewed', 'council']);
|
|
231
|
+
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.';
|
|
232
|
+
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.';
|
|
226
233
|
// activity-aware (AD-046): the triage classification vocabulary rides EVERY review-backed activity;
|
|
227
234
|
// the LEDGER pointer renders ONLY for plan-execution — the ledger is plan-execution-scoped (AD-045),
|
|
228
235
|
// and pointing plan-authoring at it would send rounds of the wrong activity into the code loop's gate.
|
|
229
|
-
|
|
230
|
-
|
|
236
|
+
// A lens-only roster resolves to `solo` and a fully degraded request resolves to `solo`: both still run
|
|
237
|
+
// a review round, which is why the gate never reads the effective recipe alone.
|
|
238
|
+
const reviewLoopAdvice = (slots, activity, flowArmed = false, plans = []) =>
|
|
239
|
+
slots.some((s) => s.roster != null || REVIEW_RECIPES.has(s.recipe) || REVIEW_RECIPES.has(s.degradedFrom))
|
|
231
240
|
? [
|
|
232
241
|
'Review-loop economics (procedures.md Fold + loop · orchestration.md §4) — the review this recipe runs:',
|
|
242
|
+
CONSULT_LINE,
|
|
243
|
+
...(activity === 'plan-execution' && flowArmed ? [ARMED_CONSULT_LINE] : []),
|
|
233
244
|
' • Cap architecture plan-review at ≤2 rounds; the bar is met by RAISING a surviving major to an acceptance invariant (or handing it to Execute/diff-review), never by exhausting the strictest backend.',
|
|
234
245
|
' • Backend divergence (one backend grounded-ships while another keeps revising mechanics) IS the crossover stop.',
|
|
235
246
|
' • Route an all-mechanics/CI or prose-only artifact to a thin plan + diff-review; run a self-consistency read before every re-review.',
|
|
236
247
|
' • Each round MUST emit {round N · finding-origin tally (first-draft / fold-induced / mechanics) · per-backend verdict} so the crossover is a computed signal.',
|
|
248
|
+
...(activity === 'plan-authoring' ? roundRenderAdvice(slots, plans) : []),
|
|
237
249
|
' • At the cap, classify every surviving blocking finding: fixable-bug (fold ONCE as a red→green test, re-review) / inherent-layer-residual (document + raise to an acceptance criterion) / escalate (the maintainer decides); a minor never forces triage.',
|
|
238
250
|
...(activity === 'plan-execution'
|
|
239
251
|
? [
|
|
@@ -246,38 +258,119 @@ const reviewLoopAdvice = (slots, activity) =>
|
|
|
246
258
|
// The grounding pre-step (AD-038, extending the AD-033 verbatim-contract rendering): whenever the
|
|
247
259
|
// resolved dispatch includes agy-review, print the CONCRETE facts-assembly invocation + the
|
|
248
260
|
// --facts form as a copy-paste pre-step — population, not placeholders. Plan-path population rule
|
|
249
|
-
// (the review-state plan-in-flight detector): exactly ONE plan in flight → render it
|
|
250
|
-
// zero or
|
|
261
|
+
// (the review-state plan-in-flight detector): exactly ONE renderable plan in flight → render it
|
|
262
|
+
// populated; zero, several, or a name a one-line render cannot carry → the explicit placeholder + a
|
|
263
|
+
// one-line discovery caveat; a name the receipt encoder refuses falls back on the receipt-minting
|
|
264
|
+
// or receipt-matching command only (`agy-review plan`, the round table). The
|
|
251
265
|
// suggested --out lives OUTSIDE the repo (/tmp) — grounding.mjs refuses a non-scratch destination.
|
|
252
266
|
// Exported for the bridge-tier byte-parity pin (AD-044 Plan 4): the velocity tier seeds the
|
|
253
267
|
// grounding rule in EXACTLY this rendered spelling — `node "${GROUNDING_TOOL}"` — so seeded and
|
|
254
268
|
// rendered forms can never drift apart.
|
|
255
269
|
export const GROUNDING_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'grounding.mjs');
|
|
270
|
+
export const REVIEW_ROUNDS_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'review-rounds-cli.mjs');
|
|
271
|
+
export const REPO_SEARCH_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'repo-search.mjs');
|
|
256
272
|
const GROUNDING_FACTS_OUT = '/tmp/review-facts.md';
|
|
273
|
+
const MINTS_RECEIPT = Object.freeze({ mintsReceipt: true });
|
|
274
|
+
|
|
275
|
+
// The round table judges what review-rounds-cli resolves from the config (S27), never this render's
|
|
276
|
+
// override or degradation, so the advisor states that fact beneath the command instead of predicting
|
|
277
|
+
// when the two agree; a bridge-less roster gets the fact that no receipt can exist in the command's place.
|
|
278
|
+
// A requested review recipe renders the command even when every bridge is unavailable here: readiness
|
|
279
|
+
// degradation removes no configured obligation, so the table still has something to say.
|
|
280
|
+
const ROUND_RENDER_FACT_LINE = " ↳ the table judges the obligation review-rounds-cli resolves from docs/ai/orchestration.json (S27) — the configured recipe, or the computed default for a silent slot — never this run's --override or a degraded recipe; it reads receipts only: a backend that did not run shows as missing (no receipts when none ran), and its degrade record is judged by review-state and core-evidence summary, not here.";
|
|
281
|
+
const ROUND_RENDER_NO_BRIDGE_LINE = " • Round render: a roster with no bridge mints no receipt, so review-rounds-cli cannot supply this round's verdicts — emit the finding-origin tally plus each lens member's verdict (or silent) directly.";
|
|
282
|
+
const roundRenderAdvice = (slots, plans) => {
|
|
283
|
+
const review = slots.find((s) => s.slot === 'review');
|
|
284
|
+
if (review === undefined) return [];
|
|
285
|
+
const roster = review.roster ?? null;
|
|
286
|
+
const shouldRenderCommand = roster === null
|
|
287
|
+
? (review.backends ?? []).length > 0 || REVIEW_RECIPES.has(review.degradedFrom)
|
|
288
|
+
: roster.some((row) => row.kind === 'bridge');
|
|
289
|
+
if (!shouldRenderCommand) return [ROUND_RENDER_NO_BRIDGE_LINE];
|
|
290
|
+
const lensMembers = (roster ?? []).filter((row) => row.kind === 'lens').map((row) => row.member);
|
|
291
|
+
const operand = populatedPlan(plans, MINTS_RECEIPT);
|
|
292
|
+
return [
|
|
293
|
+
` • Round render (verdict half of the per-round emission; the finding-origin tally stays the orchestrator's): node ${renderToolPath(REVIEW_ROUNDS_TOOL)} --artifact ${operand ?? '<plan-file>'}`,
|
|
294
|
+
...planDiscoveryCaveat(plans, '--artifact', 'populate --artifact with the plan file under review.', operand === null ? ['--artifact'] : []),
|
|
295
|
+
ROUND_RENDER_FACT_LINE,
|
|
296
|
+
...(lensMembers.length > 0 ? [` ↳ the table carries the bridge verdicts only — add each lens member's verdict (or silent) by hand: ${lensMembers.join(', ')}`] : []),
|
|
297
|
+
];
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// The ONE plan in flight as a pasteable operand: shell-significant bytes ride shellQuoteArg; a name a
|
|
301
|
+
// one-line render cannot carry is never populated, nor one the receipt encoder refuses (S21) for a
|
|
302
|
+
// command that MINTS or MATCHES a plan receipt: agy-review plan would be refused pre-spend by name, the
|
|
303
|
+
// round table would match no receipt.
|
|
304
|
+
const populatedPlan = (plans, { mintsReceipt = false } = {}) => {
|
|
305
|
+
if (plans.length !== 1 || !isRenderableLine(plans[0])) return null;
|
|
306
|
+
if (mintsReceipt && !isArtifactPathCarriable(plans[0])) return null;
|
|
307
|
+
return shellQuoteArg(`${PLANS_REL}/${plans[0]}`);
|
|
308
|
+
};
|
|
309
|
+
const planDiscoveryCaveat = (plans, flag, noPlanAction, fellBack = []) => {
|
|
310
|
+
if (plans.length === 0) return [` ↳ plan discovery: no plan in flight under ${PLANS_REL} — ${noPlanAction}`];
|
|
311
|
+
if (plans.length > 1) return [` ↳ plan discovery: ${plans.length} plans in flight under ${PLANS_REL} (${plans.map(escapeForDisplay).join(', ')}) — populate ${flag} with the one under review.`];
|
|
312
|
+
return fellBack.length === 0
|
|
313
|
+
? []
|
|
314
|
+
: [` ↳ plan discovery: the plan in flight ${escapeForDisplay(plans[0])} carries a character that either a review receipt or a rendered command cannot carry — ${fellBack.join(' and ')} ${fellBack.length === 1 ? 'stays a placeholder' : 'stay placeholders'}; rename the plan.`];
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// The kit-tools tier seeds `Bash(node <abs>/tools/repo-search.mjs:*)` BARE (velocity-profile.mjs
|
|
318
|
+
// deriveKitToolsAllowlist, the same predicate), so a seedable path renders bare — a quoted path is
|
|
319
|
+
// a different prefix and the allow rule is dead. A path the tier could never seed has no rule to
|
|
320
|
+
// match and is single-quoted for a safe paste (double quotes would still expand `$` and backticks).
|
|
321
|
+
const renderToolPath = (abs) => (isSeedablePathToken(abs) ? abs : shellQuoteArg(abs));
|
|
322
|
+
// `registered` is the registration on disk, never the tool's availability in this session — the
|
|
323
|
+
// command line stays as the fallback under the typed form. `toolPath` is a test seam (an unseedable
|
|
324
|
+
// kit path is not constructible from a test against the real checkout).
|
|
325
|
+
const readersSweepAdvice = (activity, registered, toolPath = REPO_SEARCH_TOOL) => {
|
|
326
|
+
if (activity !== 'plan-authoring') return [];
|
|
327
|
+
return [
|
|
328
|
+
'Readers sweep (before the first review) — for every changed config key, registry entry, exported constant, receipt field or canon sentence:',
|
|
329
|
+
...(registered ? [' use: repo_search {"pattern": "<the literal>"} — the kit\'s read-only MCP tool (registration complete; if it is not loaded in this session, the command below)'] : []),
|
|
330
|
+
` run: node ${renderToolPath(toolPath)} --pattern <the literal> (a pattern carrying a shell-significant byte goes through --pattern-file <f> instead)`,
|
|
331
|
+
' classify every reader as a ledger row, a stated non-goal, or unchanged with its proof',
|
|
332
|
+
];
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// The canon's own claim — the kit "parses only each section's `Slots:` line" — made true: an installed
|
|
336
|
+
// engine older (or newer) than this kit's registry prints its section verbatim, so the skew is SAID.
|
|
337
|
+
// A section without a Slots line is a customized canon, not a skew: silent. Never the exit code.
|
|
338
|
+
const SLOTS_LINE = /^Slots:[ \t]*(.+?)[ \t]*$/mu;
|
|
339
|
+
const slotSkewWarning = (section, activity) => {
|
|
340
|
+
const listed = section.match(SLOTS_LINE)?.[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
341
|
+
const registry = Object.keys(ACTIVITIES[activity].slots);
|
|
342
|
+
if (listed == null || listed.join(',') === registry.join(',')) return [];
|
|
343
|
+
return [`the installed engine's canon lists slots (${listed.join(', ')}) for ${activity} while this kit's registry names (${registry.join(', ')}) — the resolved lines follow the registry; the engine and this kit are out of step — /agent-workflow-kit status names which member is behind: upgrade that one. Tell the user.`];
|
|
344
|
+
};
|
|
257
345
|
const groundingPreStepAdvice = (activity, slots, plans) => {
|
|
258
346
|
if (!slots.some((s) => (s.backends ?? []).includes('agy-review'))) return [];
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
347
|
+
const operand = populatedPlan(plans);
|
|
348
|
+
const planArg = operand === null ? '--plan <path>' : `--plan ${operand}`;
|
|
349
|
+
const reviewOperand = populatedPlan(plans, MINTS_RECEIPT);
|
|
350
|
+
// plan-authoring reviews the plan FILE — a plain name in flight renders the review command populated;
|
|
351
|
+
// the renderability and receipt-carriability fallbacks are the only placeholders a known path produces.
|
|
262
352
|
const reviewForm =
|
|
263
353
|
activity === 'plan-authoring'
|
|
264
|
-
?
|
|
265
|
-
?
|
|
266
|
-
:
|
|
354
|
+
? reviewOperand === null
|
|
355
|
+
? 'agy-review plan <plan-file>'
|
|
356
|
+
: `agy-review plan ${reviewOperand}`
|
|
267
357
|
: 'agy-review code';
|
|
268
358
|
// `run:`/`then:` prefixes keep these POPULATED command lines machine-distinguishable from the
|
|
269
359
|
// verbatim contract DESCRIPTORS above (the descriptor drift guard set-equals bare wrapper lines).
|
|
270
|
-
//
|
|
360
|
+
// The TOOL path stays double-quoted (the bridge tier seeds that exact byte-form); the plan operand
|
|
361
|
+
// rides shellQuoteArg — bare when safe, single-quoted otherwise.
|
|
271
362
|
const lines = [
|
|
272
363
|
'Grounding pre-step (agy is dispatched — assemble the verified facts BEFORE the review; grounding.mjs slices verbatim, judgment additions stay yours):',
|
|
273
364
|
` run: node "${GROUNDING_TOOL}" --constraints --autonomy ${planArg} --out ${GROUNDING_FACTS_OUT}`,
|
|
274
365
|
` then: ${reviewForm} --facts @${GROUNDING_FACTS_OUT}`,
|
|
275
366
|
];
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
367
|
+
const fellBack = [operand === null ? '--plan' : null, activity === 'plan-authoring' && reviewOperand === null ? 'agy-review plan' : null].filter(Boolean);
|
|
368
|
+
lines.push(...planDiscoveryCaveat(
|
|
369
|
+
plans,
|
|
370
|
+
'--plan',
|
|
371
|
+
'substitute the plan file you are reviewing against, or drop --plan for constraints+autonomy facts.',
|
|
372
|
+
fellBack,
|
|
373
|
+
));
|
|
281
374
|
return lines;
|
|
282
375
|
};
|
|
283
376
|
|
|
@@ -486,7 +579,7 @@ const contractLines = ({ cmd, contract, settings }) => {
|
|
|
486
579
|
return lines;
|
|
487
580
|
};
|
|
488
581
|
|
|
489
|
-
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck }) => {
|
|
582
|
+
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck }) => {
|
|
490
583
|
const lines = [
|
|
491
584
|
section,
|
|
492
585
|
'',
|
|
@@ -510,9 +603,10 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
510
603
|
if ((flowHalves ?? []).length) lines.push('', ...flowHalves);
|
|
511
604
|
const autonomyBlock = autonomyAdvice(activity, autonomy);
|
|
512
605
|
if (autonomyBlock.length) lines.push('', ...autonomyBlock);
|
|
606
|
+
if (readersSweep.length) lines.push('', ...readersSweep);
|
|
513
607
|
const grounding = groundingPreStepAdvice(activity, slots, plans);
|
|
514
608
|
if (grounding.length) lines.push('', ...grounding);
|
|
515
|
-
const advice = reviewLoopAdvice(slots, activity);
|
|
609
|
+
const advice = reviewLoopAdvice(slots, activity, flowArmed, plans);
|
|
516
610
|
if (advice.length) lines.push('', ...advice);
|
|
517
611
|
if (foldScope.length) lines.push('', ...foldScope);
|
|
518
612
|
if (specCheck.length) lines.push('', ...specCheck);
|
|
@@ -525,15 +619,19 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
525
619
|
return lines.join('\n');
|
|
526
620
|
};
|
|
527
621
|
|
|
528
|
-
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck }) => ({
|
|
622
|
+
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck }) => ({
|
|
529
623
|
activity,
|
|
530
624
|
section,
|
|
531
625
|
slots: Object.fromEntries(
|
|
532
626
|
// `backends: string[]` is the STABLE pre-existing shape (wrapper names) — never repurposed.
|
|
533
627
|
// `contracts` is the ADDITIVE per-dispatch driving-contract field (empty for solo).
|
|
534
|
-
slots.map((s) => [s.slot, {
|
|
628
|
+
slots.map((s) => [s.slot, {
|
|
629
|
+
recipe: s.recipe, source: s.source, degradedFrom: s.degradedFrom, reason: s.reason, backends: s.backends, contracts: s.contracts,
|
|
630
|
+
...(s.roster ? { roster: s.roster } : {}),
|
|
631
|
+
}]),
|
|
535
632
|
),
|
|
536
|
-
reviewLoop: reviewLoopAdvice(slots, activity),
|
|
633
|
+
reviewLoop: reviewLoopAdvice(slots, activity, flowArmed, plans),
|
|
634
|
+
readersSweep,
|
|
537
635
|
// ADDITIVE (AD-038): the populated grounding pre-step, structured (empty when agy is not dispatched).
|
|
538
636
|
groundingPreStep: groundingPreStepAdvice(activity, slots, plans),
|
|
539
637
|
// ADDITIVE (cost-tiered execution): the unconditional cost-lane advisory, structured.
|
|
@@ -608,7 +706,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
608
706
|
),
|
|
609
707
|
});
|
|
610
708
|
const slots = resolveAllSlots({ activity, config, detection, overrides });
|
|
611
|
-
const warnings = [...detectWarnings, ...collectWarnings(slots)];
|
|
709
|
+
const warnings = [...detectWarnings, ...collectWarnings(slots), ...slotSkewWarning(section, activity)];
|
|
612
710
|
const plans = plansInFlight(cwd);
|
|
613
711
|
// The autonomy facts (AD-044 Plan 4): resolved levels + red-lines from the policy file. A
|
|
614
712
|
// malformed policy renders LOUDLY in the block AND flips the exit to 1 (config error) — the
|
|
@@ -628,13 +726,16 @@ export const main = (argv, ctx = {}) => {
|
|
|
628
726
|
// The flow armed-halves block (P8): probed ONLY when the config carries a flow block — an
|
|
629
727
|
// unarmed project keeps byte-identical output (human AND JSON) and never pays the store probe.
|
|
630
728
|
const flowProbe = ctx.flowProbe ?? defaultFlowProbe;
|
|
631
|
-
const
|
|
729
|
+
const flowState = config?.flow == null ? null : flowProbe(cwd);
|
|
730
|
+
const flowHalves = flowState == null ? null : flowHalvesAdvice(config.flow, flowState);
|
|
731
|
+
const registrationReader = ctx.readRegistration ?? readRegistration;
|
|
732
|
+
const readersSweep = readersSweepAdvice(activity, activity === 'plan-authoring' && registrationReader(cwd).registered, ctx.repoSearchTool);
|
|
632
733
|
const declaredPractice = declaredPracticeAdvice(cwd, readFile, lstat);
|
|
633
734
|
const foldScope = foldScopeAdvice(activity, config, plans);
|
|
634
735
|
const specCheck = specCheckAdvice(activity);
|
|
635
736
|
const stdout = json
|
|
636
|
-
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck }), null, 2)
|
|
637
|
-
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck });
|
|
737
|
+
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck }), null, 2)
|
|
738
|
+
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck });
|
|
638
739
|
if (autonomy?.error) {
|
|
639
740
|
return { code: 1, stdout, stderr: `procedures: malformed ${AUTONOMY_REL} — ${autonomy.error}` };
|
|
640
741
|
}
|