@really-knows-ai/foundry 3.3.9 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +32 -43
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +9 -2
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +0 -13
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +8 -245
- package/dist/CHANGELOG.md +67 -0
- package/dist/docs/architecture.md +1 -1
- package/dist/docs/tools.md +5 -28
- package/dist/docs/work-spec.md +2 -2
- package/dist/scripts/appraise-module.js +464 -0
- package/dist/scripts/lib/artefacts.js +137 -122
- package/dist/scripts/lib/attestation/attest.js +10 -18
- package/dist/scripts/lib/attestation/payload.js +36 -10
- package/dist/scripts/lib/finalize.js +5 -5
- package/dist/scripts/lib/validation.js +244 -0
- package/dist/scripts/lib/workfile.js +0 -3
- package/dist/scripts/orchestrate-cycle.js +70 -34
- package/dist/scripts/orchestrate-phases.js +68 -38
- package/dist/scripts/orchestrate.js +169 -47
- package/dist/scripts/quench-module.js +162 -0
- package/dist/scripts/sort.js +0 -1
- package/dist/skills/appraise/SKILL.md +3 -3
- package/dist/skills/human-appraise/SKILL.md +6 -7
- package/dist/skills/orchestrate/SKILL.md +33 -6
- package/dist/skills/quench/SKILL.md +4 -4
- package/package.json +5 -5
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared validation utilities extracted from the plugin's validate-tools.
|
|
3
|
+
*
|
|
4
|
+
* These functions run deterministic validator commands (no LLM involvement)
|
|
5
|
+
* and return structured results consumed by the quench module and the
|
|
6
|
+
* plugin's `foundry_validate_run` tool.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { readdir } from 'fs/promises';
|
|
11
|
+
import { join, relative, dirname, sep } from 'path';
|
|
12
|
+
import { minimatch } from 'minimatch';
|
|
13
|
+
import { getLawsForQuench, getArtefactType } from './config.js';
|
|
14
|
+
import { parseValidatorJsonl } from './validator-jsonl.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Private helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
function shellQuote(value) {
|
|
21
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validatePatterns(patterns, typeId) {
|
|
25
|
+
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
26
|
+
return { ok: false, error: `Artefact type ${typeId} has no file-patterns` };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SKIP_DIRS = new Set(['node_modules', '.git']);
|
|
32
|
+
|
|
33
|
+
function toPosix(p) {
|
|
34
|
+
return sep === '/' ? p : p.split(sep).join('/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readdirSafe(dir) {
|
|
38
|
+
try {
|
|
39
|
+
return await readdir(dir, { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function* walkFiles(root, dir) {
|
|
46
|
+
for (const entry of await readdirSafe(dir)) {
|
|
47
|
+
const full = join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
49
|
+
yield* walkFiles(root, full);
|
|
50
|
+
} else if (entry.isFile()) {
|
|
51
|
+
yield toPosix(relative(root, full));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fileMatchesAnyPattern(rel, patterns) {
|
|
57
|
+
for (const pattern of patterns) {
|
|
58
|
+
if (minimatch(rel, pattern)) return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Exported functions
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read file-patterns for an artefact type from its definition.
|
|
69
|
+
*/
|
|
70
|
+
export async function getValidationPatterns(foundryDir, typeId, io) {
|
|
71
|
+
const artType = await getArtefactType(foundryDir, typeId, io);
|
|
72
|
+
return artType.frontmatter['file-patterns'] || [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run validation commands for an artefact type deterministically.
|
|
77
|
+
*
|
|
78
|
+
* Accepts an IO interface and foundryDir directly (no plugin context
|
|
79
|
+
* dependency). When `artefacts` is provided, the `{files}` substitution
|
|
80
|
+
* uses non-deleted files from the artefact list instead of expanding
|
|
81
|
+
* patterns across the worktree.
|
|
82
|
+
*
|
|
83
|
+
* @param {{ typeId: string, io: object, foundryDir: string, artefacts?: Array<{file: string, state: string}> }} params
|
|
84
|
+
* @returns {Promise<{ ok: boolean, validatorsRun: number, items: Array, errors: Array }>}
|
|
85
|
+
*/
|
|
86
|
+
export async function performValidation({ typeId, io, foundryDir, artefacts }) {
|
|
87
|
+
let patterns;
|
|
88
|
+
try {
|
|
89
|
+
patterns = await getValidationPatterns(foundryDir, typeId, io);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { ok: false, error: err.message };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const validationErr = validatePatterns(patterns, typeId);
|
|
95
|
+
if (validationErr) return validationErr;
|
|
96
|
+
|
|
97
|
+
const laws = await getLawsForQuench(foundryDir, io, { typeId });
|
|
98
|
+
if (!laws?.length) {
|
|
99
|
+
return { ok: true, validatorsRun: 0, items: [], errors: [] };
|
|
100
|
+
}
|
|
101
|
+
return runValidatorsAndReport(laws, patterns, foundryDir, artefacts);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run validators for a set of laws and build the aggregated result.
|
|
106
|
+
*
|
|
107
|
+
* When `artefacts` is provided, the `{files}` substitution uses non-deleted
|
|
108
|
+
* files from the artefact list. Otherwise patterns are expanded across the
|
|
109
|
+
* worktree (backwards compatibility for callers like `foundry_validate_run`).
|
|
110
|
+
*/
|
|
111
|
+
export async function runValidatorsAndReport(laws, patterns, foundryDir, artefacts) {
|
|
112
|
+
const worktree = dirname(foundryDir);
|
|
113
|
+
let expandedFiles;
|
|
114
|
+
if (artefacts) {
|
|
115
|
+
expandedFiles = artefacts
|
|
116
|
+
.filter(({ state }) => state !== 'deleted')
|
|
117
|
+
.map(({ file }) => file)
|
|
118
|
+
.sort();
|
|
119
|
+
} else {
|
|
120
|
+
expandedFiles = await expandPatterns(patterns, worktree);
|
|
121
|
+
}
|
|
122
|
+
const substitutions = {
|
|
123
|
+
pattern: patterns.map(shellQuote).join(' '),
|
|
124
|
+
files: expandedFiles.map(shellQuote).join(' '),
|
|
125
|
+
};
|
|
126
|
+
const results = await runValidators(laws, patterns, substitutions, worktree);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
ok: results.errors.length === 0,
|
|
130
|
+
validatorsRun: results.validatorsRun,
|
|
131
|
+
items: results.items,
|
|
132
|
+
errors: results.errors,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Expand glob patterns to actual file paths in the worktree.
|
|
138
|
+
*
|
|
139
|
+
* Uses readdir + minimatch for Node 20 compatibility (glob added in Node 22).
|
|
140
|
+
*/
|
|
141
|
+
export async function expandPatterns(patterns, worktree) {
|
|
142
|
+
const files = new Set();
|
|
143
|
+
for await (const rel of walkFiles(worktree, worktree)) {
|
|
144
|
+
if (fileMatchesAnyPattern(rel, patterns)) {
|
|
145
|
+
files.add(rel);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return Array.from(files).sort();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Run all validators across a list of laws and collect results.
|
|
153
|
+
*/
|
|
154
|
+
export async function runValidators(laws, patterns, substitutions, worktree) {
|
|
155
|
+
const results = {
|
|
156
|
+
validatorsRun: 0,
|
|
157
|
+
items: [],
|
|
158
|
+
errors: [],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (const law of laws) {
|
|
162
|
+
if (!law.validators || law.validators.length === 0) continue;
|
|
163
|
+
await runLawValidators(law, patterns, substitutions, worktree, results);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Run validators for a single law.
|
|
171
|
+
*/
|
|
172
|
+
export async function runLawValidators(law, patterns, substitutions, worktree, results) {
|
|
173
|
+
for (const validator of law.validators) {
|
|
174
|
+
// Skip commands that require {files} when there are no matching files
|
|
175
|
+
if (substitutions.files === '' && /(?:^|\s)\{files\}(?=\s|$)/.test(validator.command)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
results.validatorsRun++;
|
|
179
|
+
const expanded = expandValidatorCommand(validator.command, substitutions);
|
|
180
|
+
const parseResult = await executeValidator(expanded, worktree, patterns);
|
|
181
|
+
collectValidatorResult(parseResult, law.id, validator.id, results);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Fold a single validator's parse result into the aggregate results.
|
|
187
|
+
*/
|
|
188
|
+
export function collectValidatorResult(parseResult, lawId, validatorId, results) {
|
|
189
|
+
for (const item of parseResult.items) {
|
|
190
|
+
results.items.push({ lawId, validatorId, ...item });
|
|
191
|
+
}
|
|
192
|
+
for (const message of parseResult.parseErrors) {
|
|
193
|
+
results.errors.push({ lawId, validatorId, type: 'parse', message });
|
|
194
|
+
}
|
|
195
|
+
for (const message of parseResult.patternErrors) {
|
|
196
|
+
results.errors.push({ lawId, validatorId, type: 'pattern-mismatch', message });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Execute a validator command and parse its JSONL output.
|
|
202
|
+
*/
|
|
203
|
+
export async function executeValidator(expanded, worktree, patterns) {
|
|
204
|
+
try {
|
|
205
|
+
const output = execSync(expanded, {
|
|
206
|
+
cwd: worktree,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
209
|
+
});
|
|
210
|
+
const { Readable } = await import('stream');
|
|
211
|
+
const stream = Readable.from([output]);
|
|
212
|
+
return await parseValidatorJsonl(stream, patterns);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// Validator command failed — prefer stdout for JSONL
|
|
215
|
+
// (tools like rg exit 1 with results on stdout)
|
|
216
|
+
const output = (err.stdout || err.stderr || err.message || '').trim();
|
|
217
|
+
const { Readable } = await import('stream');
|
|
218
|
+
const stream = Readable.from([output]);
|
|
219
|
+
return await parseValidatorJsonl(stream, patterns);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Expand validator command placeholders {pattern} and {files}.
|
|
225
|
+
*
|
|
226
|
+
* Both placeholders are recognised only as standalone tokens bounded by
|
|
227
|
+
* whitespace or start/end of string. Surrounding single or double quotes
|
|
228
|
+
* around the placeholder are stripped first.
|
|
229
|
+
*/
|
|
230
|
+
export function expandValidatorCommand(command, { pattern, files }) {
|
|
231
|
+
let cmd = command
|
|
232
|
+
.replace(/"\{pattern\}"/g, '{pattern}')
|
|
233
|
+
.replace(/'\{pattern\}'/g, '{pattern}')
|
|
234
|
+
.replace(/"\{files\}"/g, '{files}')
|
|
235
|
+
.replace(/'\{files\}'/g, '{files}');
|
|
236
|
+
|
|
237
|
+
cmd = cmd.replace(/(?:^|\s)\{pattern\}(?=\s|$)/g, (match) =>
|
|
238
|
+
match.startsWith('{') ? pattern : ' ' + pattern);
|
|
239
|
+
|
|
240
|
+
cmd = cmd.replace(/(?:^|\s)\{files\}(?=\s|$)/g, (match) =>
|
|
241
|
+
match.startsWith('{') ? files : ' ' + files);
|
|
242
|
+
|
|
243
|
+
return cmd;
|
|
244
|
+
}
|
|
@@ -5,21 +5,12 @@ import {
|
|
|
5
5
|
getCycleDefinition,
|
|
6
6
|
getArtefactType,
|
|
7
7
|
} from './lib/config.js';
|
|
8
|
-
import { parseArtefactsTable, setArtefactStatus } from './lib/artefacts.js';
|
|
9
8
|
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
10
9
|
|
|
11
10
|
// ---------------------------------------------------------------------------
|
|
12
11
|
// Public helpers (re-exported by orchestrate.js for tests).
|
|
13
12
|
// ---------------------------------------------------------------------------
|
|
14
13
|
|
|
15
|
-
export function findCycleOutputArtefact(cycleId, io) {
|
|
16
|
-
if (!io.exists('WORK.md')) return null;
|
|
17
|
-
const content = io.readFile('WORK.md');
|
|
18
|
-
const rows = parseArtefactsTable(content);
|
|
19
|
-
const match = rows.find(r => r.cycle === cycleId);
|
|
20
|
-
return match ? { file: match.file, type: match.type, status: match.status } : null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
export async function readCycleTargets(cycleId, io) {
|
|
24
15
|
try {
|
|
25
16
|
const cd = await getCycleDefinition('foundry', cycleId, io);
|
|
@@ -47,7 +38,11 @@ export async function readForgeFilePatterns(cycleId, io) {
|
|
|
47
38
|
}
|
|
48
39
|
const output = extractOutputType(cd);
|
|
49
40
|
if (!output) return null;
|
|
50
|
-
|
|
41
|
+
try {
|
|
42
|
+
return await fetchFilePatterns(output, io);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
// ---------------------------------------------------------------------------
|
|
@@ -148,31 +143,7 @@ export function tryCommit(git, message, allowedPatterns, phase) {
|
|
|
148
143
|
}
|
|
149
144
|
}
|
|
150
145
|
|
|
151
|
-
function findCycleRow(cycleId, rows) {
|
|
152
|
-
return rows.find(r => r.cycle === cycleId);
|
|
153
|
-
}
|
|
154
146
|
|
|
155
|
-
function writeBlockedStatus(content, row, io) {
|
|
156
|
-
try {
|
|
157
|
-
io.writeFile('WORK.md', setArtefactStatus(content, row.file, 'blocked'));
|
|
158
|
-
return { ok: true };
|
|
159
|
-
} catch (e) {
|
|
160
|
-
return { ok: false, error: e?.message || String(e) };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function markArtefactBlocked(cycleId, io) {
|
|
165
|
-
if (!io.exists('WORK.md')) return { ok: true };
|
|
166
|
-
const content = io.readFile('WORK.md');
|
|
167
|
-
const rows = parseArtefactsTable(content);
|
|
168
|
-
const row = findCycleRow(cycleId, rows);
|
|
169
|
-
if (!row) return { ok: true };
|
|
170
|
-
return writeBlockedStatus(content, row, io);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function formatBlockNote(blockResult) {
|
|
174
|
-
return blockResult.ok ? '' : ` (also: failed to mark artefact blocked: ${blockResult.error})`;
|
|
175
|
-
}
|
|
176
147
|
|
|
177
148
|
// ---------------------------------------------------------------------------
|
|
178
149
|
// Stage synthesis (pure utility, used by setupWorkfile and exported publicly).
|
|
@@ -188,6 +159,71 @@ export function synthesizeStages({ cycleId, hasValidation, humanAppraise, assay
|
|
|
188
159
|
return stages;
|
|
189
160
|
}
|
|
190
161
|
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Dispatch-multi action constants and validators.
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
export const DISPATCH_MULTI_ACTION = 'dispatch_multi';
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validate a dispatch_multi action object.
|
|
170
|
+
*
|
|
171
|
+
* Checks action type, tasks array shape, and each task's required fields.
|
|
172
|
+
* Returns undefined on success, a violation object on failure.
|
|
173
|
+
*/
|
|
174
|
+
function validString(value) {
|
|
175
|
+
return typeof value === 'string' && value.length > 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function checkActionType(action) {
|
|
179
|
+
if (!action || action.action !== DISPATCH_MULTI_ACTION) {
|
|
180
|
+
return violation(`action must be "${DISPATCH_MULTI_ACTION}"`, []);
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function validateAllTasks(tasks) {
|
|
186
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
187
|
+
const err = checkDispatchTask(tasks[i], i);
|
|
188
|
+
if (err) return err;
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function validateDispatchMulti(action) {
|
|
194
|
+
const typeErr = checkActionType(action);
|
|
195
|
+
if (typeErr) return typeErr;
|
|
196
|
+
if (!Array.isArray(action.tasks)) {
|
|
197
|
+
return violation('dispatch_multi tasks must be an array', []);
|
|
198
|
+
}
|
|
199
|
+
return validateAllTasks(action.tasks);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function checkDispatchTask(task, index) {
|
|
203
|
+
if (!validString(task.subagent_type)) {
|
|
204
|
+
return violation(`dispatch_multi tasks[${index}]: subagent_type must be a non-empty string`, []);
|
|
205
|
+
}
|
|
206
|
+
if (!validString(task.prompt)) {
|
|
207
|
+
return violation(`dispatch_multi tasks[${index}]: prompt must be a non-empty string`, []);
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Build a dispatch_multi response object.
|
|
214
|
+
*
|
|
215
|
+
* Pure utility for constructing the action envelope expected by the
|
|
216
|
+
* orchestrate skill's LLM loop.
|
|
217
|
+
*/
|
|
218
|
+
export function buildDispatchMultiResponse(tasks, stage, cycle) {
|
|
219
|
+
return {
|
|
220
|
+
action: DISPATCH_MULTI_ACTION,
|
|
221
|
+
tasks,
|
|
222
|
+
stage,
|
|
223
|
+
cycle,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
191
227
|
// ---------------------------------------------------------------------------
|
|
192
228
|
// Dispatch prompt rendering (pure utility, used by handleSortResult and exported publicly).
|
|
193
229
|
// ---------------------------------------------------------------------------
|
|
@@ -13,38 +13,62 @@ import { stageBaseOf } from './lib/stage-guard.js';
|
|
|
13
13
|
import { allowedPatternsForStage } from './lib/git-policy.js';
|
|
14
14
|
import { loadExtractor } from './lib/assay/loader.js';
|
|
15
15
|
import { checkExtractorAgainstCycle } from './lib/assay/permissions.js';
|
|
16
|
+
import { getArtefactFiles } from './lib/artefacts.js';
|
|
16
17
|
import {
|
|
17
|
-
findCycleOutputArtefact,
|
|
18
18
|
readCycleTargets,
|
|
19
19
|
readForgeFilePatterns,
|
|
20
20
|
readRecentFeedback,
|
|
21
21
|
computeOpenFeedback,
|
|
22
22
|
violation,
|
|
23
23
|
tryCommit,
|
|
24
|
-
markArtefactBlocked,
|
|
25
|
-
formatBlockNote,
|
|
26
24
|
synthesizeStages,
|
|
27
25
|
renderDispatchPrompt,
|
|
28
26
|
} from './orchestrate-cycle.js';
|
|
29
27
|
|
|
30
|
-
async function
|
|
31
|
-
const
|
|
32
|
-
|
|
28
|
+
async function findOutputArtefacts(cfm, io, foundryDir, baseBranch) {
|
|
29
|
+
const outputType = cfm ? cfm['output-type'] : undefined;
|
|
30
|
+
if (!outputType) return null;
|
|
31
|
+
const artefacts = await getArtefactFiles(foundryDir, outputType, io, { baseBranch });
|
|
32
|
+
return artefacts.find(a => a.state !== 'deleted') || null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
35
|
+
async function doneAction(cycleId, io, foundryDir, baseBranch) {
|
|
36
|
+
const fd = foundryDir || 'foundry';
|
|
37
|
+
const base = baseBranch || 'main';
|
|
38
|
+
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
39
|
+
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
40
|
+
const artefactFile = artefact ? artefact.file : null;
|
|
41
|
+
return { action: 'done', cycle: cycleId, artefact_file: artefactFile, next_cycles: await readCycleTargets(cycleId, io) };
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
|
|
44
|
+
async function blockedAction(cycleId, io, details, foundryDir, baseBranch) {
|
|
45
|
+
const fd = foundryDir || 'foundry';
|
|
46
|
+
const base = baseBranch || 'main';
|
|
47
|
+
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
48
|
+
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
49
|
+
const artefactFile = artefact ? artefact.file : null;
|
|
50
|
+
const reason = details || 'iteration limit reached with unresolved feedback';
|
|
51
|
+
return { action: 'blocked', cycle: cycleId, artefact_file: artefactFile, reason };
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
54
|
+
async function humanAppraiseAction(route, token, ctx) {
|
|
55
|
+
const { cycleId, io, baseBranch } = ctx;
|
|
56
|
+
const fd = ctx.foundryDir || 'foundry';
|
|
57
|
+
const base = baseBranch || 'main';
|
|
58
|
+
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
59
|
+
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
60
|
+
const artefactFile = artefact ? artefact.file : null;
|
|
61
|
+
return { action: 'human_appraise', stage: route, token, context: { cycle: cycleId, artefact_file: artefactFile, recent_feedback: readRecentFeedback(io) } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function missingModelViolation(cycleId, route, io, foundryDir, baseBranch) {
|
|
65
|
+
const fd = foundryDir || 'foundry';
|
|
66
|
+
const base = baseBranch || 'main';
|
|
67
|
+
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
68
|
+
const outputType = cfm ? cfm['output-type'] : undefined;
|
|
69
|
+
const artefacts = outputType ? await getArtefactFiles(fd, outputType, io, { baseBranch: base }) : [];
|
|
70
|
+
const affectedFiles = artefacts.filter(a => a.state !== 'deleted').map(a => a.file);
|
|
71
|
+
return violation(`cycle ${cycleId} stage ${route} has no model declared in cycle definition`, affectedFiles);
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
function makeDispatchPayload(route, cycleId, token, cwd, filePatterns) {
|
|
@@ -52,30 +76,38 @@ function makeDispatchPayload(route, cycleId, token, cwd, filePatterns) {
|
|
|
52
76
|
}
|
|
53
77
|
|
|
54
78
|
async function buildDispatchAction(route, model, token, ctx) {
|
|
55
|
-
if (!model) return missingModelViolation(ctx.cycleId, route, ctx.io);
|
|
79
|
+
if (!model) return missingModelViolation(ctx.cycleId, route, ctx.io, ctx.foundryDir, ctx.baseBranch ?? 'main');
|
|
56
80
|
const base = route.split(':')[0];
|
|
57
81
|
const filePatterns = base === 'forge' ? await readForgeFilePatterns(ctx.cycleId, ctx.io) : null;
|
|
58
82
|
return { action: 'dispatch', stage: route, subagent_type: model, prompt: renderDispatchPrompt(makeDispatchPayload(route, ctx.cycleId, token, ctx.cwd, filePatterns)) };
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
function routeDispatch(route) {
|
|
85
|
+
export function routeDispatch(route) {
|
|
62
86
|
return typeof route === 'string' ? route.split(':')[0] : '';
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
async function handleTerminalRoute(route, sortResult, ctx) {
|
|
66
|
-
|
|
67
|
-
if (route === '
|
|
68
|
-
return
|
|
90
|
+
const baseBranch = ctx.baseBranch || 'main';
|
|
91
|
+
if (route === 'done') return doneAction(ctx.cycleId, ctx.io, ctx.foundryDir, baseBranch);
|
|
92
|
+
if (route === 'blocked') return blockedAction(ctx.cycleId, ctx.io, sortResult.details, ctx.foundryDir, baseBranch);
|
|
93
|
+
const details = sortResult.details || 'sort returned violation';
|
|
94
|
+
return violation(details);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isTerminalRoute(route) {
|
|
98
|
+
return route === 'done' || route === 'blocked' || route === 'violation';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getRouteBase(route) {
|
|
102
|
+
return routeDispatch(route);
|
|
69
103
|
}
|
|
70
104
|
|
|
71
105
|
export async function handleSortResult(sortResult, ctx) {
|
|
72
106
|
const { route, model, token } = sortResult;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
return humanAppraiseAction(route, token, ctx.cycleId, ctx.io);
|
|
78
|
-
}
|
|
107
|
+
const routeBase = getRouteBase(route);
|
|
108
|
+
if (isTerminalRoute(route)) return handleTerminalRoute(route, sortResult, ctx);
|
|
109
|
+
if (routeBase === 'quench' || routeBase === 'appraise') return violation(routeBase + ' route reached handleSortResult');
|
|
110
|
+
if (routeBase === 'human-appraise') return humanAppraiseAction(route, token, ctx);
|
|
79
111
|
return buildDispatchAction(route, model, token, ctx);
|
|
80
112
|
}
|
|
81
113
|
|
|
@@ -236,12 +268,11 @@ function readOriginalState(io) {
|
|
|
236
268
|
return { workMd: io.readFile('WORK.md'), history: io.exists('WORK.history.yaml') ? io.readFile('WORK.history.yaml') : null };
|
|
237
269
|
}
|
|
238
270
|
|
|
239
|
-
function buildFinalizeViolation(finalizeResult
|
|
240
|
-
const blockNote = formatBlockNote(blockResult);
|
|
271
|
+
function buildFinalizeViolation(finalizeResult) {
|
|
241
272
|
if (finalizeResult.error === 'unexpected_files') {
|
|
242
|
-
return violation(`unexpected files written by subagent: ${(finalizeResult.files || []).join(', ')}
|
|
273
|
+
return violation(`unexpected files written by subagent: ${(finalizeResult.files || []).join(', ')}`, finalizeResult.files || []);
|
|
243
274
|
}
|
|
244
|
-
return violation(`stage_finalize error: ${finalizeResult.error}
|
|
275
|
+
return violation(`stage_finalize error: ${finalizeResult.error}`, []);
|
|
245
276
|
}
|
|
246
277
|
|
|
247
278
|
function writeHistoryEntries(ctx) {
|
|
@@ -284,9 +315,8 @@ export async function finaliseStage(args) {
|
|
|
284
315
|
}
|
|
285
316
|
const finalizeResult = await finalize({ cycleId, stage: lastStage.stage, baseSha: lastStage.baseSha, io });
|
|
286
317
|
if (!finalizeResult.ok) {
|
|
287
|
-
const blockResult = markArtefactBlocked(cycleId, io);
|
|
288
318
|
clearStageState(activeStage, null, io);
|
|
289
|
-
return buildFinalizeViolation(finalizeResult
|
|
319
|
+
return buildFinalizeViolation(finalizeResult);
|
|
290
320
|
}
|
|
291
321
|
const historyPath = 'WORK.history.yaml';
|
|
292
322
|
const iteration = getIteration(historyPath, cycleId, io);
|
|
@@ -303,12 +333,12 @@ export async function finaliseStage(args) {
|
|
|
303
333
|
}
|
|
304
334
|
|
|
305
335
|
export function handleViolation(args) {
|
|
306
|
-
const { lastResult, activeStage, lastStage
|
|
336
|
+
const { lastResult, activeStage, lastStage } = args;
|
|
307
337
|
const failedStage = activeStage || lastStage;
|
|
308
338
|
if (!failedStage) { return violation('lastResult.ok=false but no stage recorded — orphaned state'); }
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
339
|
+
clearStageState(activeStage, lastStage, args.io);
|
|
340
|
+
return violation(
|
|
341
|
+
`subagent dispatch failed: ${lastResult.error || 'unknown error'}`,
|
|
342
|
+
lastResult.affected_files || [],
|
|
343
|
+
);
|
|
314
344
|
}
|