@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,48 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
4
|
+
import { KNOWN_BACKENDS } from './detect-backends.mjs';
|
|
5
|
+
import { receiptIdOfCmd } from './carriers.mjs';
|
|
6
|
+
|
|
7
|
+
const oneLine = (value) => String(value).replace(/[\s]+/gu, ' ').trim();
|
|
8
|
+
|
|
9
|
+
const validPosture = (posture) => posture !== null && typeof posture === 'object' && !Array.isArray(posture)
|
|
10
|
+
&& typeof posture.model === 'string' && posture.model.length > 0
|
|
11
|
+
&& (!Object.hasOwn(posture, 'effort') || (typeof posture.effort === 'string' && posture.effort.length > 0))
|
|
12
|
+
&& (!Object.hasOwn(posture, 'tier') || posture.tier === null || (typeof posture.tier === 'string' && posture.tier.length > 0))
|
|
13
|
+
&& Object.keys(posture).every((key) => ['model', 'effort', 'tier'].includes(key));
|
|
14
|
+
|
|
15
|
+
const postureString = (posture, backend, settings) => {
|
|
16
|
+
const parts = [`model=${oneLine(posture.model)}`];
|
|
17
|
+
if (Object.hasOwn(posture, 'effort')) parts.push(`effort=${oneLine(posture.effort)}`);
|
|
18
|
+
if (Object.hasOwn(posture, 'tier')) {
|
|
19
|
+
const knob = (settings?.active ?? []).find((row) => row.key === 'CODEX_SERVICE_TIER' && row.bridge === backend.name);
|
|
20
|
+
parts.push(knob ? `tier=${oneLine(knob.value)} (bridge-settings)` : `tier=${posture.tier ?? 'standard'}`);
|
|
21
|
+
}
|
|
22
|
+
return parts.join(' ');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const posturesByBackend = (ctx = {}) => {
|
|
26
|
+
const bundleRoot = ctx.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
|
|
27
|
+
const read = ctx.readFile ?? readFileSync;
|
|
28
|
+
return Object.fromEntries(KNOWN_BACKENDS.flatMap((backend) => {
|
|
29
|
+
const cmd = backend.roleCmds?.review;
|
|
30
|
+
const receiptId = receiptIdOfCmd(cmd);
|
|
31
|
+
if (receiptId === null) return [];
|
|
32
|
+
const path = join(bundleRoot, backend.name, 'capability.json');
|
|
33
|
+
try {
|
|
34
|
+
const manifest = JSON.parse(String(read(path, 'utf8')));
|
|
35
|
+
if (!Object.hasOwn(manifest, 'posture')) {
|
|
36
|
+
return [[receiptId, { state: 'none', posture: null, path }]];
|
|
37
|
+
}
|
|
38
|
+
if (!validPosture(manifest.posture)) throw new Error('invalid posture block');
|
|
39
|
+
return [[receiptId, {
|
|
40
|
+
state: 'valid',
|
|
41
|
+
posture: postureString(manifest.posture, backend, ctx.settings ?? { active: [] }),
|
|
42
|
+
path,
|
|
43
|
+
}]];
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return [[receiptId, { state: 'unreadable', posture: null, path, reason: oneLine(error?.message ?? error) }]];
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
};
|
package/tools/carriers.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { refuseDirectRun } from './direct-run.mjs';
|
|
|
12
12
|
// `policy` marks an activity that is a SESSION with an autonomy level of its own; a routine chore
|
|
13
13
|
// runs inside such a session and carries none.
|
|
14
14
|
export const ACTIVITIES = {
|
|
15
|
-
'plan-authoring': { slots: { author: 'carrier', review: 'review' }, policy: true },
|
|
15
|
+
'plan-authoring': { slots: { author: 'carrier', fold: 'carrier', review: 'review' }, policy: true },
|
|
16
16
|
'plan-execution': { slots: { execute: 'execute', review: 'review' }, policy: true },
|
|
17
17
|
routine: { slots: { carrier: 'carrier', parallel: 'switch' }, policy: false },
|
|
18
18
|
};
|
|
@@ -31,6 +31,18 @@ export const SLOT_RECIPES = {
|
|
|
31
31
|
export const SWITCH_SLOT = 'switch';
|
|
32
32
|
export const SWITCH_DEFAULT = 'on';
|
|
33
33
|
|
|
34
|
+
const CODEX = 'codex-cli-bridge';
|
|
35
|
+
const AGY = 'antigravity-cli-bridge';
|
|
36
|
+
|
|
37
|
+
export const DISPLAY_ALIASES = Object.freeze({ [CODEX]: 'codex', [AGY]: 'agy' });
|
|
38
|
+
export const BACKEND_PRIORITY = Object.freeze([CODEX, AGY]);
|
|
39
|
+
export const REVIEW_CMD_ALIASES = Object.freeze({
|
|
40
|
+
'codex-review': Object.freeze({ backend: CODEX, receiptId: 'codex' }),
|
|
41
|
+
'agy-review': Object.freeze({ backend: AGY, receiptId: 'agy' }),
|
|
42
|
+
});
|
|
43
|
+
export const receiptIdOfCmd = (cmd) => REVIEW_CMD_ALIASES[cmd]?.receiptId ?? null;
|
|
44
|
+
export const LENS_VERDICTS = Object.freeze(['ship', 'ship with nits', 'revise', 'rethink']);
|
|
45
|
+
|
|
34
46
|
export const isSwitchSlot = (slotType) => slotType === SWITCH_SLOT;
|
|
35
47
|
|
|
36
48
|
export const CARRY_ROLE = 'carry';
|
|
@@ -90,12 +102,12 @@ export const vehicleDegradeReason = (survey, applyHint = EXECUTOR_APPLY) => {
|
|
|
90
102
|
// Pure constants. `procedures.mjs` prints them for a slot resolved to `subagent`; the wording is a
|
|
91
103
|
// red line, so a render composes these strings and never re-words one.
|
|
92
104
|
|
|
93
|
-
// The slice noun is per
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'plan-authoring':
|
|
97
|
-
'plan-execution': 'a slice is a set of file-disjoint ledger rows; wording is copied verbatim where wording is a red line',
|
|
98
|
-
routine: "a slice is a bounded mechanical task; a read-only one (a sweep, gate triage) rides its placed read-only vehicle, or is carried solo with a stated reason when that vehicle is absent; a write-capable one (a regeneration, a fixture build) rides the executor; the changelog stays the orchestrator's",
|
|
105
|
+
// The slice noun is per SLOT — authoring and folding share an activity but carry different work.
|
|
106
|
+
export const SLICE_BY_SLOT = {
|
|
107
|
+
'plan-authoring.author': 'a slice is a brief naming the goal, the governing spec(s) and the ledger constraints; the subagent drafts the plan or the contract from it, and the orchestrator reviews the draft as its own',
|
|
108
|
+
'plan-authoring.fold': "a slice is the round's findings with their dispositions; the subagent edits the plan or the contract in place and returns; the orchestrator runs the self-consistency read itself",
|
|
109
|
+
'plan-execution.execute': 'a slice is a set of file-disjoint ledger rows; wording is copied verbatim where wording is a red line',
|
|
110
|
+
'routine.carrier': "a slice is a bounded mechanical task; a read-only one (a sweep, gate triage) rides its placed read-only vehicle, or is carried solo with a stated reason when that vehicle is absent; a write-capable one (a regeneration, a fixture build) rides the executor; the changelog stays the orchestrator's",
|
|
99
111
|
};
|
|
100
112
|
|
|
101
113
|
export const VEHICLE_STATE_TOKEN = '<state>';
|
|
@@ -120,10 +132,10 @@ export const SUBAGENT_SLOT_TYPES = Object.entries(SLOT_RECIPES)
|
|
|
120
132
|
.map(([slotType]) => slotType);
|
|
121
133
|
|
|
122
134
|
// dispatchForm({ activity, slot, state }) → the lines a `subagent`-resolved slot renders: the
|
|
123
|
-
//
|
|
135
|
+
// slot's slice sentence, then the four shared lines with the surveyed vehicle state filled in.
|
|
124
136
|
// A slot whose type cannot hold `subagent` (a review slot) and an unknown activity render nothing.
|
|
125
137
|
export const dispatchForm = ({ activity, slot, state } = {}) => {
|
|
126
|
-
const slice =
|
|
138
|
+
const slice = SLICE_BY_SLOT[`${activity}.${slot}`];
|
|
127
139
|
const slotType = ACTIVITIES[activity]?.slots?.[slot];
|
|
128
140
|
if (!slice || !SUBAGENT_SLOT_TYPES.includes(slotType)) return [];
|
|
129
141
|
return [slice, ...DISPATCH_LINES.map((line) => line.replace(VEHICLE_STATE_TOKEN, state ?? MISSING))];
|
|
@@ -7,6 +7,7 @@ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
7
7
|
import { dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { refuseDirectRun } from './direct-run.mjs';
|
|
10
|
+
import { deriveLensTemplate } from './review-roster-resolve.mjs';
|
|
10
11
|
|
|
11
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
|
|
@@ -23,6 +24,7 @@ const EXIT_PRECONDITION = 1;
|
|
|
23
24
|
export const CHEAP_AGENTS_STAMP = 'CHEAP_AGENTS_STAMP';
|
|
24
25
|
export const CHEAP_AGENTS_SYMLINK = 'CHEAP_AGENTS_SYMLINK';
|
|
25
26
|
export const CHEAP_AGENTS_BUNDLE = 'CHEAP_AGENTS_BUNDLE';
|
|
27
|
+
export const CHEAP_AGENTS_CONFIG = 'CHEAP_AGENTS_CONFIG';
|
|
26
28
|
|
|
27
29
|
export const makeCheapAgentsError = (code, message) =>
|
|
28
30
|
Object.assign(new Error(`${ERROR_PREFIX} ${message}`), { name: 'CheapAgentsError', code, exitCode: EXIT_PRECONDITION });
|
|
@@ -107,13 +109,23 @@ export const planPlacement = (templates, projectDir, deps = {}) => {
|
|
|
107
109
|
|
|
108
110
|
export const EXECUTOR_VEHICLE = 'executor.md';
|
|
109
111
|
export const EXECUTOR_VEHICLE_REL = `${AGENTS_DIR}/${EXECUTOR_VEHICLE}`;
|
|
112
|
+
export const EXECUTOR_VEHICLE_SPEC = Object.freeze({
|
|
113
|
+
stem: 'executor', template: 'executor', model: null, effort: null, tools: 'full', derived: false,
|
|
114
|
+
});
|
|
115
|
+
const READ_ONLY_TOOLS = new Set(['Read', 'Grep', 'Glob']);
|
|
110
116
|
|
|
111
117
|
// The YAML subset a vehicle's frontmatter is read with: a bare scalar, a single- or double-quoted
|
|
112
118
|
// scalar, or a flow sequence; an unquoted ` #comment` and surrounding whitespace are dropped first.
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
const stripComment = (raw) => String(raw ?? '').replace(/^((?:[^"'#]|"[^"]*"|'[^']*')*?)\s+#.*$/u, '$1').trim();
|
|
120
|
+
const cleanValue = (raw) => stripComment(raw).replace(/^(["'])(.*)\1$/u, '$2').replace(/^\[(.*)\]$/u, '$1').trim();
|
|
121
|
+
|
|
122
|
+
const scalarValue = (raw) => {
|
|
123
|
+
const value = stripComment(raw);
|
|
124
|
+
const quoted = value.match(/^(["'])(.*)\1$/u);
|
|
125
|
+
if (quoted) return quoted[2].trim();
|
|
126
|
+
return /^[[{|>]|["']/u.test(value) ? null : value;
|
|
116
127
|
};
|
|
128
|
+
const scalarOf = (frontmatter, key) => scalarValue(frontmatter.match(new RegExp(`^${key}:(.*)$`, 'mu'))?.[1]);
|
|
117
129
|
|
|
118
130
|
// The block-sequence items under the `tools:` key: `- item` lines indented deeper than the key,
|
|
119
131
|
// with blank and comment lines allowed between them; the first other line ends the list.
|
|
@@ -133,40 +145,90 @@ const blockItems = (frontmatter) => {
|
|
|
133
145
|
return items;
|
|
134
146
|
};
|
|
135
147
|
|
|
136
|
-
const
|
|
137
|
-
|
|
148
|
+
const frontmatterOf = (content) => String(content).replace(/\r\n/gu, '\n').match(/^---\n([\s\S]*?)\n---(?:\n|$)/u)?.[1] ?? '';
|
|
149
|
+
|
|
150
|
+
const listedTools = (frontmatter) => {
|
|
151
|
+
const inline = cleanValue(frontmatter.match(/^tools:(.*)$/mu)?.[1]);
|
|
152
|
+
return (inline || blockItems(frontmatter).join(', ')).split(',').map((tool) => cleanValue(tool)).filter(Boolean);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const executorFrontmatterRefusal = (frontmatter, stem) => {
|
|
138
156
|
for (const key of ['name', 'tools']) {
|
|
139
157
|
if ((frontmatter.match(new RegExp(`^${key}:`, 'gmu')) ?? []).length > 1) return `duplicate \`${key}:\` key in the frontmatter`;
|
|
140
158
|
}
|
|
141
|
-
if (
|
|
159
|
+
if (scalarOf(frontmatter, 'name') !== stem) return `frontmatter does not declare \`name: ${stem}\``;
|
|
142
160
|
if (!/^tools:/mu.test(frontmatter)) return null;
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const granted = listed.split(',').map((tool) => cleanValue(tool));
|
|
147
|
-
return granted.includes('Bash') ? null : `tools: ${listed} is read-only`;
|
|
161
|
+
const granted = listedTools(frontmatter);
|
|
162
|
+
if (granted.length === 0) return 'tools: is empty — grant a list that includes Bash, or drop the line';
|
|
163
|
+
return granted.includes('Bash') ? null : `tools: ${granted.join(', ')} is read-only`;
|
|
148
164
|
};
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
const lensFrontmatter = (frontmatter, stem) => {
|
|
167
|
+
for (const key of ['name', 'model', 'effort', 'tools']) {
|
|
168
|
+
const count = (frontmatter.match(new RegExp(`^${key}:`, 'gmu')) ?? []).length;
|
|
169
|
+
if (count !== 1) return { refusal: count === 0 ? `frontmatter has no \`${key}:\` key` : `duplicate \`${key}:\` key in the frontmatter` };
|
|
170
|
+
}
|
|
171
|
+
if (scalarOf(frontmatter, 'name') !== stem) {
|
|
172
|
+
return { refusal: `frontmatter does not declare \`name: ${stem}\`` };
|
|
173
|
+
}
|
|
174
|
+
const scalars = Object.fromEntries(['model', 'effort'].map((key) => [key, scalarOf(frontmatter, key)]));
|
|
175
|
+
for (const [key, value] of Object.entries(scalars)) {
|
|
176
|
+
if (value === null) return { refusal: `${key}: is not a scalar` };
|
|
177
|
+
if (value === '') return { refusal: `${key}: is empty` };
|
|
178
|
+
}
|
|
179
|
+
const tools = listedTools(frontmatter);
|
|
180
|
+
if (tools.length === 0) return { refusal: 'tools: must grant a non-empty read-only list' };
|
|
181
|
+
const unsafe = tools.find((tool) => !READ_ONLY_TOOLS.has(tool));
|
|
182
|
+
if (unsafe) return { refusal: `tools: grants non-read-only tool ${unsafe}` };
|
|
183
|
+
return { refusal: null, ...scalars };
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const templateFor = (spec, deps) => {
|
|
187
|
+
if (spec.template == null) return null;
|
|
188
|
+
const bundled = readBundledAgents(deps).find((item) => item.name === `${spec.template}.md`);
|
|
189
|
+
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${spec.template}.md is missing from the bundle`);
|
|
190
|
+
return {
|
|
191
|
+
name: `${spec.stem}.md`,
|
|
192
|
+
content: spec.derived ? deriveLensTemplate(bundled.content, spec) : bundled.content,
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const surveyVehicle = (projectDir, spec, deps = {}) => {
|
|
197
|
+
const rel = `${AGENTS_DIR}/${spec.stem}.md`;
|
|
154
198
|
try {
|
|
155
|
-
const template =
|
|
156
|
-
if (!template) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${EXECUTOR_VEHICLE} is missing from the bundle`);
|
|
199
|
+
const template = templateFor(spec, deps);
|
|
157
200
|
const fs = readFsDeps(deps);
|
|
158
201
|
assertDirSafe(join(projectDir, CLAUDE_DIR), CLAUDE_DIR, fs);
|
|
159
202
|
assertDirSafe(join(projectDir, AGENTS_DIR), AGENTS_DIR, fs);
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
if (placement.action === '
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
203
|
+
const placeholder = template ?? { name: `${spec.stem}.md`, content: null };
|
|
204
|
+
const [placement] = planPlacement([placeholder], projectDir, deps);
|
|
205
|
+
if (placement.action === 'place') {
|
|
206
|
+
const reason = template === null ? 'no bundled template to derive it from' : null;
|
|
207
|
+
return { state: 'missing', reason, rel };
|
|
208
|
+
}
|
|
209
|
+
const frontmatter = frontmatterOf(placement.existing ?? template?.content);
|
|
210
|
+
if (placement.action === 'already-current') {
|
|
211
|
+
if (spec.tools !== 'read-only') return { state: 'placed', reason: null, rel };
|
|
212
|
+
const lens = lensFrontmatter(frontmatter, spec.stem);
|
|
213
|
+
return lens.refusal === null
|
|
214
|
+
? { state: 'placed', reason: null, rel, model: lens.model, effort: lens.effort }
|
|
215
|
+
: { state: 'unusable', reason: lens.refusal, rel };
|
|
216
|
+
}
|
|
217
|
+
if (spec.tools !== 'read-only') {
|
|
218
|
+
const refusal = executorFrontmatterRefusal(frontmatter, spec.stem);
|
|
219
|
+
return refusal === null
|
|
220
|
+
? { state: 'customized', reason: null, rel }
|
|
221
|
+
: { state: 'unusable', reason: refusal, rel };
|
|
222
|
+
}
|
|
223
|
+
const lens = lensFrontmatter(frontmatter, spec.stem);
|
|
224
|
+
return lens.refusal === null
|
|
225
|
+
? { state: 'customized', reason: null, rel, model: lens.model, effort: lens.effort }
|
|
226
|
+
: { state: 'unusable', reason: lens.refusal, rel };
|
|
167
227
|
} catch (err) {
|
|
168
228
|
return { state: 'unusable', reason: err?.message ?? String(err), rel };
|
|
169
229
|
}
|
|
170
230
|
};
|
|
171
231
|
|
|
232
|
+
export const surveyExecutorVehicle = (projectDir, deps = {}) => surveyVehicle(projectDir, EXECUTOR_VEHICLE_SPEC, deps);
|
|
233
|
+
|
|
172
234
|
refuseDirectRun(import.meta.url);
|
package/tools/cheap-agents.mjs
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// The family's second `.claude/` writer, the velocity-profile.mjs writer discipline verbatim:
|
|
11
11
|
// • preview-then-mutate — `--dry-run` is the DEFAULT and writes nothing; `--apply` writes;
|
|
12
12
|
// • deployment-gated — `--apply` STOPs unless docs/ai/.workflow-version equals the lineage
|
|
13
|
-
// head (a dry-run stays usable
|
|
13
|
+
// head (a dry-run stays usable whatever the stamp says; an unreadable orchestration config
|
|
14
|
+
// STOPs both, since the derived lenses it names cannot be known);
|
|
14
15
|
// • symlink-safe — a symlinked `.claude` / `.claude/agents` / target file is a STOP, never a
|
|
15
16
|
// write-through;
|
|
16
17
|
// • NEVER overwrites an existing .claude/agents/ file whose content differs from the bundled
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
// apply report reminds you.
|
|
26
27
|
//
|
|
27
28
|
// Exit codes: 0 done / dry-run (incl. preserved customizations — a user's file is a legitimate
|
|
28
|
-
// state, not an error); 1 precondition STOP (stamp, symlink, missing bundle
|
|
29
|
+
// state, not an error); 1 precondition STOP (stamp, symlink, missing bundle, an unreadable
|
|
30
|
+
// orchestration config — the derived lenses it names cannot be known); 2 usage.
|
|
29
31
|
// Dependency-free, Node >= 22. No side effects on import.
|
|
30
32
|
|
|
31
33
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
@@ -33,6 +35,9 @@ import { join } from 'node:path';
|
|
|
33
35
|
import { fileURLToPath } from 'node:url';
|
|
34
36
|
import { isDirectRun } from './direct-run.mjs';
|
|
35
37
|
import { shellQuoteArg } from './repo-lex.mjs';
|
|
38
|
+
import { loadConfig } from './orchestration-config.mjs';
|
|
39
|
+
import { lensMembersOf } from './review-roster.mjs';
|
|
40
|
+
import { deriveLensTemplate, lensVehicleSpec } from './review-roster-resolve.mjs';
|
|
36
41
|
// The READ core, never a second copy: the bundle, the placement plan and the executor survey live
|
|
37
42
|
// there so the read-only advisor graph can reach them without reaching this writer.
|
|
38
43
|
import {
|
|
@@ -42,6 +47,9 @@ import {
|
|
|
42
47
|
EXPECTED_WORKFLOW_VERSION,
|
|
43
48
|
UTF8,
|
|
44
49
|
CHEAP_AGENTS_STAMP,
|
|
50
|
+
CHEAP_AGENTS_BUNDLE,
|
|
51
|
+
CHEAP_AGENTS_CONFIG,
|
|
52
|
+
EXECUTOR_VEHICLE,
|
|
45
53
|
makeCheapAgentsError,
|
|
46
54
|
readFsDeps,
|
|
47
55
|
readBundledAgents,
|
|
@@ -59,6 +67,7 @@ export {
|
|
|
59
67
|
CHEAP_AGENTS_STAMP,
|
|
60
68
|
CHEAP_AGENTS_SYMLINK,
|
|
61
69
|
CHEAP_AGENTS_BUNDLE,
|
|
70
|
+
CHEAP_AGENTS_CONFIG,
|
|
62
71
|
makeCheapAgentsError,
|
|
63
72
|
readBundledAgents,
|
|
64
73
|
planPlacement,
|
|
@@ -86,6 +95,7 @@ model. The fifth, executor, is the ONE full-tool vehicle: dispatched only for a
|
|
|
86
95
|
authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and
|
|
87
96
|
it never commits.
|
|
88
97
|
Default is --dry-run (a preview; writes nothing). --apply writes.
|
|
98
|
+
Configured derived review lenses are planned and placed beside the bundled vehicles.
|
|
89
99
|
An existing file with DIFFERENT content is preserved and reported, never overwritten.`;
|
|
90
100
|
|
|
91
101
|
export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
@@ -95,10 +105,12 @@ const writeFsDeps = (deps = {}) => ({
|
|
|
95
105
|
writeFile: deps.writeFile ?? writeFileSync,
|
|
96
106
|
});
|
|
97
107
|
|
|
98
|
-
export const preflightCheapAgents = ({ cwd }, deps = {}) => {
|
|
108
|
+
export const preflightCheapAgents = ({ cwd, derived = [] }, deps = {}) => {
|
|
99
109
|
const fs = readFsDeps(deps);
|
|
100
110
|
const projectDir = cwd ?? process.cwd();
|
|
101
|
-
const
|
|
111
|
+
const templatesByName = new Map(readBundledAgents(deps).map((template) => [template.name, template]));
|
|
112
|
+
for (const template of derived) templatesByName.set(template.name, template);
|
|
113
|
+
const templates = [...templatesByName.values()];
|
|
102
114
|
const stamp = readStamp(join(projectDir, WORKFLOW_STAMP), fs);
|
|
103
115
|
const stampOk = stamp === EXPECTED_WORKFLOW_VERSION;
|
|
104
116
|
assertDirSafe(join(projectDir, CLAUDE_DIR), CLAUDE_DIR, fs);
|
|
@@ -107,11 +119,38 @@ export const preflightCheapAgents = ({ cwd }, deps = {}) => {
|
|
|
107
119
|
return { projectDir, stamp, stampOk, plan };
|
|
108
120
|
};
|
|
109
121
|
|
|
122
|
+
const loadConfigOrStop = (cwd, deps) => {
|
|
123
|
+
try {
|
|
124
|
+
return loadConfig(cwd, deps.readFile, deps.lstat).config;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw makeCheapAgentsError(CHEAP_AGENTS_CONFIG, `${err?.message ?? err} — the agents writer cannot read the configured review lenses — nothing is placed`);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const configuredDerivedTemplates = (cwd, deps) => {
|
|
131
|
+
const config = loadConfigOrStop(cwd, deps);
|
|
132
|
+
const bundle = readBundledAgents(deps);
|
|
133
|
+
const templates = new Map();
|
|
134
|
+
for (const member of lensMembersOf(config)) {
|
|
135
|
+
const spec = lensVehicleSpec(member);
|
|
136
|
+
if (!spec.derived) continue;
|
|
137
|
+
const base = bundle.find((template) => template.name === `${spec.template}.md`);
|
|
138
|
+
if (!base) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${spec.template}.md is missing from the bundle`);
|
|
139
|
+
templates.set(`${spec.stem}.md`, { name: `${spec.stem}.md`, content: deriveLensTemplate(base.content, spec) });
|
|
140
|
+
}
|
|
141
|
+
return [...templates.values()];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const applyCheapAgentsCommand = (root) =>
|
|
145
|
+
`node ${shellQuoteArg(fileURLToPath(import.meta.url))} --apply --cwd ${shellQuoteArg(root)}`;
|
|
146
|
+
|
|
110
147
|
// ── the writer ────────────────────────────────────────────────────────────────────────
|
|
111
148
|
|
|
112
149
|
export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
113
150
|
const fs = writeFsDeps(deps);
|
|
114
|
-
const
|
|
151
|
+
const projectDir = cwd ?? process.cwd();
|
|
152
|
+
const derived = configuredDerivedTemplates(projectDir, deps);
|
|
153
|
+
const preflight = preflightCheapAgents({ cwd: projectDir, derived }, deps);
|
|
115
154
|
if (dryRun) return { wrote: false, dryRun: true, ...preflight };
|
|
116
155
|
|
|
117
156
|
if (!preflight.stampOk) {
|
|
@@ -147,8 +186,9 @@ export const formatResult = (result) => {
|
|
|
147
186
|
if (!result.stampOk) {
|
|
148
187
|
lines.push(`note: no current deployment stamp found (${result.stamp ?? 'none'}) — --apply will refuse until init/upgrade runs.`);
|
|
149
188
|
}
|
|
189
|
+
const readOnlyCount = result.plan.filter((item) => item.name !== EXECUTOR_VEHICLE).length;
|
|
150
190
|
lines.push(
|
|
151
|
-
|
|
191
|
+
`${readOnlyCount} vehicles are Claude Code subagents with READ-ONLY tools and NO shell as bundled or derived (a customized file keeps whatever it grants) — so a fan-out on the shipped templates can never turn into a wave of approval prompts.`,
|
|
152
192
|
`three of those ride the cheap lane (model: haiku, effort: low) for mechanical work; ${FALLBACK_LENS_ADDITIONAL_ONLY}`,
|
|
153
193
|
'executor is the one FULL-TOOL vehicle: dispatched only for a bounded execution, authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and it never commits.',
|
|
154
194
|
);
|
|
@@ -156,7 +196,7 @@ export const formatResult = (result) => {
|
|
|
156
196
|
// item's one-liner, and that flow's contract is "run the printed command, no improvisation" — a
|
|
157
197
|
// bare "re-run with --apply" would leave the caller to reconstruct --cwd and its quoting.
|
|
158
198
|
if (result.dryRun && result.plan.some((item) => item.action === 'place')) {
|
|
159
|
-
lines.push(`to apply, run exactly:
|
|
199
|
+
lines.push(`to apply, run exactly: ${applyCheapAgentsCommand(result.projectDir)}`);
|
|
160
200
|
}
|
|
161
201
|
if (!result.dryRun && result.wrote) {
|
|
162
202
|
lines.push('hidden-mode note: if this deployment is hidden, run the hide-footprint reconcile so the placed files stay out of `git status`.');
|
|
@@ -97,7 +97,7 @@ const RAW_BACKENDS = [
|
|
|
97
97
|
],
|
|
98
98
|
grounding: 'automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags',
|
|
99
99
|
continue: [],
|
|
100
|
-
receipt: 'side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit\'s review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized \'Verdict: <ship|revise|rethink>\' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review',
|
|
100
|
+
receipt: 'side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; durationS = integer wall-clock seconds from CLI start through verdict parsing and the wrapper prints review duration: <n>s; blocking = the count of [blocker] and [major] lines (schema mode: findings with either severity; a payload whose findings cannot be counted fails the run, no receipt); artifactPath = the normalized realpath on plan receipts only (repo-relative inside the work tree, absolute otherwise), while a double quote, backslash or control byte refuses pre-spend because the receipt encoder cannot carry it; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit\'s review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized \'Verdict: <ship|revise|rethink>\' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review',
|
|
101
101
|
notes: [
|
|
102
102
|
'the review posture banner appends a banner-only timeout=<duration> field — exactly the duration handed to timeout(1); the hard-timeout preflight fails CLOSED when no timeout/gtimeout binary exists (the wrapper refuses by name before any CLI run, so an uncapped review run can no longer happen), and the field never enters the receipt posture or the D5 banner↔receipt parity',
|
|
103
103
|
'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
|
|
@@ -139,7 +139,7 @@ const RAW_BACKENDS = [
|
|
|
139
139
|
'agy-review --continue [--decided @f] [--focus "…"] [--nonce <n>]',
|
|
140
140
|
'agy-review --conversation <id> [--decided @f] [--focus "…"] [--nonce <n>]',
|
|
141
141
|
],
|
|
142
|
-
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
|
|
142
|
+
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; durationS = integer wall-clock seconds from CLI start through verdict parsing and the wrapper prints review duration: <n>s; blocking = the count of numbered items in the first ### Blocking section; artifactPath = the normalized realpath on plan/diff receipts only (repo-relative inside the work tree, absolute otherwise), while a double quote, backslash or control byte refuses pre-spend because the receipt encoder cannot carry it; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
|
|
143
143
|
notes: [
|
|
144
144
|
'transport: every review dispatch drives the CLI in --output-format json (plus --disable-slash-commands) and the returned envelope is parsed in node (bin/agy-envelope.mjs) — the operator-facing invocations and flags above do NOT change, and on a ZERO exit the wrapper still PRINTS the review text, never JSON. A missing or unreadable envelope on a zero exit is a loud failure with NO receipt, never a downgraded verdict and never a fallback to raw-stdout parsing; a non-zero CLI exit keeps its own code and message, and publishes the captured stdout unchanged from the SINGLE dispatch or the FINAL fed turn (which may therefore be a JSON or partial payload — the envelope is parsed only on a zero exit); an INTERMEDIATE feed turn is the exception, its output stays private (Invariant E) and its failure prints only a named error. Enforced by a PRE-SPEND capability probe, not a version floor: agy --help must advertise --output-format and --disable-slash-commands, node must be >= 22, and bin/agy-envelope.mjs must be present — otherwise the review refuses before any run is spent and names the missing capability',
|
|
145
145
|
'pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt',
|
package/tools/direct-run.mjs
CHANGED
|
@@ -62,6 +62,9 @@ export const LIBRARY_ONLY_MODULES = Object.freeze({
|
|
|
62
62
|
// The READ core of the subagent-vehicle surface: it sits one name away from the writer
|
|
63
63
|
// references/modes/agents.md DOES name, and reaching for it is reaching for the agents mode.
|
|
64
64
|
'cheap-agents-read.mjs': '/agent-workflow-kit agents',
|
|
65
|
+
'review-roster.mjs': '/agent-workflow-kit recipes',
|
|
66
|
+
'review-roster-resolve.mjs': '/agent-workflow-kit recipes',
|
|
67
|
+
'set-recipe-roster.mjs': '/agent-workflow-kit set-recipe',
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
// The frozen refusal line. One line, names the module, names the command.
|
package/tools/fold-scope.mjs
CHANGED
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
// Dependency-free, Node >= 22.
|
|
23
23
|
|
|
24
24
|
import { tokenizeMarkdown } from '../references/scripts/markdown-blocks.mjs';
|
|
25
|
+
import { PLAN_HEADINGS, bulletBlocks } from './plan-shape.mjs';
|
|
26
|
+
|
|
27
|
+
export { bulletBlocks };
|
|
25
28
|
|
|
26
29
|
export const CLASSES = ['in-scope', 'new-invariant', 'blocking'];
|
|
27
30
|
export const ROW_FIELDS = ['invariant', 'origin', 'narrow fix', 'proof', 'residual exposure'];
|
|
28
|
-
export const ACCEPTANCE_HEADING =
|
|
31
|
+
export const ACCEPTANCE_HEADING = PLAN_HEADINGS[2];
|
|
29
32
|
// The canon says a deferral row carries "the origin `file:line`". Anchored at the start of the value
|
|
30
33
|
// and a POSITIVE line number, so "file.mjs:12junk" and "file.mjs:0" are not one; trailing context
|
|
31
34
|
// after the token is fine, because the canon asks the row to CARRY a file:line, not to carry nothing
|
|
@@ -35,68 +38,10 @@ const ORIGIN_SHAPE = /^\S+:[1-9]\d*(\s|$)/;
|
|
|
35
38
|
// literals refuses fail-closed; the general per-project status grammar is queued, not guessed here.
|
|
36
39
|
const CLOSED_MARKERS = ['DONE', 'CLOSED'];
|
|
37
40
|
const ORIGIN_MISSING = 'origin (the canon requires a file:line)';
|
|
38
|
-
const BULLET = /^-\s+\S/;
|
|
39
41
|
|
|
40
42
|
const normalize = (s) => String(s ?? '').replace(/\r/g, '').replace(/\s+/g, ' ').trim();
|
|
41
43
|
const contains = (haystack, needle) => normalize(haystack).toLowerCase().includes(needle);
|
|
42
44
|
|
|
43
|
-
// The ONE bullet scan both readers use, over the block model's lines. A fenced region is a quotation
|
|
44
|
-
// AND a boundary: it closes the block it interrupts, so text past a fence can never join the bullet
|
|
45
|
-
// before it (which would let a far-side literal satisfy a near-side claim). A `-` plus any whitespace
|
|
46
|
-
// run opens a block; a blank or indented line continues it; any other unindented line closes it.
|
|
47
|
-
// Blocks are returned RAW (their own lines) — the queue reader needs the field lines inside them —
|
|
48
|
-
// each carrying the body index it OPENS at, because a second reader (queue-audit.mjs) reports rows by
|
|
49
|
-
// file line and a scan that dropped the index would have to re-derive it against a different grammar.
|
|
50
|
-
//
|
|
51
|
-
// `fenceContinues` is the SECOND reader's question, and it is a different one. A deferral row asks
|
|
52
|
-
// what a bullet CLAIMS, so a fence must cut it. A queue row asks what a bullet COSTS and whether it
|
|
53
|
-
// is still work, and there the fence-as-boundary is a hole: measured, a row carrying a code block
|
|
54
|
-
// reported ONE line and its `**DONE 2026-01-01:**` two lines further down was invisible, so the
|
|
55
|
-
// per-row cap could be walked straight past and a closure went unseen.
|
|
56
|
-
//
|
|
57
|
-
// Under the option only a NESTED fence continues an open block — one whose opening line is indented,
|
|
58
|
-
// which is what makes it part of the list item at all. A fence opening at column 0 is a
|
|
59
|
-
// DOCUMENT-level block and still closes the row, exactly as an unindented line does; absorbing it
|
|
60
|
-
// charged a one-line row for six. The run is decided ONCE, at its opening line, so a content line
|
|
61
|
-
// inside it cannot re-decide the question.
|
|
62
|
-
//
|
|
63
|
-
// The absorbed lines never enter `lines` — a marker inside a quotation is not a status — so the
|
|
64
|
-
// block records where they were: `span` is the row's PHYSICAL extent, and `gaps` holds the `lines`
|
|
65
|
-
// indices a fence run follows, so a reader assembling a multi-line span cannot join text from both
|
|
66
|
-
// sides of a code block into one claim.
|
|
67
|
-
export const bulletBlocks = (lines, fencedLines, from, to, { fenceContinues = false } = {}) => {
|
|
68
|
-
const blocks = [];
|
|
69
|
-
let current = null;
|
|
70
|
-
let absorbing = null;
|
|
71
|
-
const close = () => {
|
|
72
|
-
if (current) blocks.push(current);
|
|
73
|
-
current = null;
|
|
74
|
-
};
|
|
75
|
-
for (let index = from; index < to; index += 1) {
|
|
76
|
-
if (fencedLines.has(index)) {
|
|
77
|
-
if (absorbing === null) absorbing = Boolean(fenceContinues && current && /^\s+\S/.test(lines[index]));
|
|
78
|
-
if (!absorbing) close();
|
|
79
|
-
else {
|
|
80
|
-
current.span += 1;
|
|
81
|
-
current.gaps.add(current.lines.length - 1);
|
|
82
|
-
}
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
absorbing = null;
|
|
86
|
-
const line = lines[index];
|
|
87
|
-
if (BULLET.test(line)) {
|
|
88
|
-
close();
|
|
89
|
-
current = { start: index, lines: [line], span: 1, gaps: new Set() };
|
|
90
|
-
} else if (current && (line.trim() === '' || /^\s+\S/.test(line))) {
|
|
91
|
-
current.lines.push(line);
|
|
92
|
-
current.span += 1;
|
|
93
|
-
} else {
|
|
94
|
-
close();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
close();
|
|
98
|
-
return blocks;
|
|
99
|
-
};
|
|
100
45
|
|
|
101
46
|
// extractAcceptance(planText) -> the top-level bullets under `## Verification`, each collapsed to one
|
|
102
47
|
// line. Per the planning canon those bullets ARE the acceptance criteria and they are the WHOLE list.
|
|
@@ -131,7 +76,7 @@ const parseFields = (block) => {
|
|
|
131
76
|
if (match) {
|
|
132
77
|
open = match[1].toLowerCase().replace(/\s+/g, ' ');
|
|
133
78
|
values[open] = [...(values[open] ?? []), match[2].trim()];
|
|
134
|
-
} else if (open && /^\s+\S/.test(line) &&
|
|
79
|
+
} else if (open && /^\s+\S/.test(line) && !/^-\s+\S/.test(line.trim())) {
|
|
135
80
|
values[open][values[open].length - 1] += ` ${line.trim()}`;
|
|
136
81
|
} else {
|
|
137
82
|
open = null;
|
package/tools/grounding.mjs
CHANGED
|
@@ -36,7 +36,7 @@ import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
|
36
36
|
// (f) --autonomy (AD-044 Plan 3): the effective per-project autonomy policy for the facts payload.
|
|
37
37
|
// READ core only — never autonomy-write.mjs (the import-split invariant).
|
|
38
38
|
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from './autonomy-config.mjs';
|
|
39
|
-
|
|
39
|
+
import { PLAN_HEADINGS } from './plan-shape.mjs';
|
|
40
40
|
const PLAN_EXECUTION = 'plan-execution';
|
|
41
41
|
|
|
42
42
|
// The agy single-argv byte contract (mirrors agy-review.sh — the wrapper is the enforcement home).
|
|
@@ -44,7 +44,7 @@ export const DEFAULT_MAX_PROMPT_BYTES = 120000;
|
|
|
44
44
|
export const ARGV_HARD_MAX = 131000;
|
|
45
45
|
|
|
46
46
|
export const CONSTRAINTS_HEADING = /^## .*Hard Constraints$/;
|
|
47
|
-
export const PLAN_SECTIONS =
|
|
47
|
+
export const PLAN_SECTIONS = PLAN_HEADINGS.slice(0, 3);
|
|
48
48
|
|
|
49
49
|
// ── pure section slicing (exactly-one-match; the inject-methodology discipline) ────────
|
|
50
50
|
|