@really-knows-ai/foundry 3.8.5 → 3.9.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/dist/.opencode/plugins/foundry-tools/stage-output-tool.js +113 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +98 -21
- package/dist/.opencode/plugins/foundry.js +2 -0
- package/dist/CHANGELOG.md +18 -0
- package/dist/docs/architecture.md +1 -1
- package/dist/scripts/appraise-module.js +71 -74
- package/dist/scripts/lib/forge-contract.js +13 -13
- package/dist/scripts/lib/stage-output-schemas.js +174 -0
- package/dist/scripts/orchestrate-cycle.js +6 -6
- package/dist/scripts/orchestrate-dispatch.js +240 -0
- package/dist/scripts/orchestrate-finalise.js +21 -17
- package/dist/scripts/orchestrate.js +10 -92
- package/dist/skills/appraise/SKILL.md +9 -27
- package/dist/skills/assay/SKILL.md +6 -13
- package/dist/skills/forge/SKILL.md +10 -11
- package/dist/skills/human-appraise/SKILL.md +8 -8
- package/dist/skills/orchestrate/SKILL.md +2 -2
- package/dist/skills/quench/SKILL.md +5 -5
- package/package.json +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/scripts/lib/stage-output-schemas.js
|
|
2
|
+
// Stage output validation schemas for forge, appraise, and human-appraise.
|
|
3
|
+
// Each exported function validates a plain object against the stage's output
|
|
4
|
+
// schema and returns { ok: true } or { ok: false, errors: [...] }.
|
|
5
|
+
|
|
6
|
+
// ── JSON Schema definitions ──────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const FORGE_SCHEMA = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
required: ['status'],
|
|
11
|
+
properties: {
|
|
12
|
+
status: { enum: ['done', 'actioned', 'wont-fix'] },
|
|
13
|
+
reason: { type: 'string' },
|
|
14
|
+
},
|
|
15
|
+
allOf: [
|
|
16
|
+
{
|
|
17
|
+
if: { properties: { status: { const: 'wont-fix' } } },
|
|
18
|
+
then: { required: ['reason'] },
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const APPRAISE_SCHEMA = {
|
|
24
|
+
type: 'object',
|
|
25
|
+
required: ['file', 'law', 'text'],
|
|
26
|
+
properties: {
|
|
27
|
+
file: { type: 'string', minLength: 1 },
|
|
28
|
+
law: { type: 'string', minLength: 1 },
|
|
29
|
+
text: { type: 'string', minLength: 1 },
|
|
30
|
+
evidence: { type: 'string' },
|
|
31
|
+
severity: { type: 'string' },
|
|
32
|
+
location: { type: 'string' },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const HUMAN_APPRAISE_SCHEMA = {
|
|
37
|
+
type: 'object',
|
|
38
|
+
required: ['verdict'],
|
|
39
|
+
properties: {
|
|
40
|
+
verdict: { const: 'approved' },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ── Schema validator ─────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function checkObjectType(schema, data, path) {
|
|
47
|
+
if (schema.type !== 'object') return [];
|
|
48
|
+
if (isRecord(data)) return [];
|
|
49
|
+
return [`${path} — must be a plain object`];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function checkStringType(schema, data, path) {
|
|
53
|
+
if (schema.type !== 'string') return [];
|
|
54
|
+
if (typeof data !== 'string') {
|
|
55
|
+
return [`${path} — must be a string`];
|
|
56
|
+
}
|
|
57
|
+
if (schema.minLength !== undefined && data.length < schema.minLength) {
|
|
58
|
+
return [`${path} — must be a non-empty string`];
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkType(schema, data, path) {
|
|
64
|
+
return [
|
|
65
|
+
...checkObjectType(schema, data, path),
|
|
66
|
+
...checkStringType(schema, data, path),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isRecord(v) {
|
|
71
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkRequired(schema, data, path) {
|
|
75
|
+
if (!schema.required || !isRecord(data)) return [];
|
|
76
|
+
return schema.required
|
|
77
|
+
.filter(key => !(key in data))
|
|
78
|
+
.map(key => `${path}: ${key} — is required`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function checkProperties(schema, data, path) {
|
|
82
|
+
if (!schema.properties || !isRecord(data)) return [];
|
|
83
|
+
const errors = [];
|
|
84
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
85
|
+
if (key in data) {
|
|
86
|
+
errors.push(...validateSchema(propSchema, data[key], `${path}: ${key}`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return errors;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function checkAdditionalProperties(schema, data, path) {
|
|
93
|
+
if (schema.additionalProperties !== false || !schema.properties || !isRecord(data)) return [];
|
|
94
|
+
const allowed = new Set(Object.keys(schema.properties));
|
|
95
|
+
return Object.keys(data)
|
|
96
|
+
.filter(key => !allowed.has(key))
|
|
97
|
+
.map(key => `${path}: ${key} — unknown field`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkEnum(schema, data, path) {
|
|
101
|
+
if (schema.enum === undefined) return [];
|
|
102
|
+
if (schema.enum.includes(data)) return [];
|
|
103
|
+
const allowed = schema.enum.map(v => JSON.stringify(v)).join(', ');
|
|
104
|
+
return [`${path} — must be one of ${allowed}`];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkConst(schema, data, path) {
|
|
108
|
+
if (schema.const === undefined) return [];
|
|
109
|
+
if (data === schema.const) return [];
|
|
110
|
+
return [`${path} — must be ${JSON.stringify(schema.const)}`];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function checkNot(schema, data, path) {
|
|
114
|
+
if (!schema.not || !schema.not.required || !isRecord(data)) return [];
|
|
115
|
+
return schema.not.required
|
|
116
|
+
.filter(key => key in data)
|
|
117
|
+
.map(key => `${path}: ${key} — must not be present`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function skipAllOfItem(item, data) {
|
|
121
|
+
if (!item.if) return true;
|
|
122
|
+
if (!item.if.properties) return false;
|
|
123
|
+
return Object.keys(item.if.properties).some(key => !(key in data));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function checkAllOf(schema, data, path) {
|
|
127
|
+
if (!schema.allOf) return [];
|
|
128
|
+
const errors = [];
|
|
129
|
+
for (const item of schema.allOf) {
|
|
130
|
+
if (skipAllOfItem(item, data)) continue;
|
|
131
|
+
const ifErrors = validateSchema(item.if, data, path);
|
|
132
|
+
if (ifErrors.length === 0) {
|
|
133
|
+
errors.push(...validateSchema(item.then, data, path));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return errors;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateSchema(schema, data, path = '') {
|
|
140
|
+
const typeErrors = checkType(schema, data, path);
|
|
141
|
+
if (typeErrors.length > 0) return typeErrors;
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
...checkRequired(schema, data, path),
|
|
145
|
+
...checkProperties(schema, data, path),
|
|
146
|
+
...checkAdditionalProperties(schema, data, path),
|
|
147
|
+
...checkEnum(schema, data, path),
|
|
148
|
+
...checkConst(schema, data, path),
|
|
149
|
+
...checkNot(schema, data, path),
|
|
150
|
+
...checkAllOf(schema, data, path),
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
// ── Exported validators ──────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export function validateForgeOutput(data) {
|
|
156
|
+
const stage = 'forge';
|
|
157
|
+
const errors = validateSchema(FORGE_SCHEMA, data, stage);
|
|
158
|
+
if (errors.length) return { ok: false, errors };
|
|
159
|
+
return { ok: true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function validateAppraiseOutput(data) {
|
|
163
|
+
const stage = 'appraise';
|
|
164
|
+
const errors = validateSchema(APPRAISE_SCHEMA, data, stage);
|
|
165
|
+
if (errors.length) return { ok: false, errors };
|
|
166
|
+
return { ok: true };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function validateHumanAppraiseOutput(data) {
|
|
170
|
+
const stage = 'human-appraise';
|
|
171
|
+
const errors = validateSchema(HUMAN_APPRAISE_SCHEMA, data, stage);
|
|
172
|
+
if (errors.length) return { ok: false, errors };
|
|
173
|
+
return { ok: true };
|
|
174
|
+
}
|
|
@@ -266,17 +266,17 @@ function buildForgePromptLines({ cycle, outputType, forgeItem }) {
|
|
|
266
266
|
`File: ${forgeItem.file}`,
|
|
267
267
|
`Issue: ${forgeItem.text}`,
|
|
268
268
|
``,
|
|
269
|
-
`
|
|
270
|
-
` -
|
|
271
|
-
` -
|
|
269
|
+
`Call foundry_stage_output with the correct status:`,
|
|
270
|
+
` - foundry_stage_output({ status: "actioned" }) — fix the issue by changing the artefact file`,
|
|
271
|
+
` - foundry_stage_output({ status: "wont-fix", reason: "<justification>" }) — the issue is already resolved or does not apply`,
|
|
272
272
|
``,
|
|
273
|
-
`Write
|
|
273
|
+
`Then call foundry_stage_end(). Write nothing else — format is validated by the tool.`,
|
|
274
274
|
);
|
|
275
275
|
} else {
|
|
276
276
|
lines.push(
|
|
277
277
|
``,
|
|
278
278
|
`First generation — no feedback to address yet.`,
|
|
279
|
-
`Produce the artefact
|
|
279
|
+
`Produce the artefact, call foundry_stage_output({ status: "done" }), then foundry_stage_end().`,
|
|
280
280
|
);
|
|
281
281
|
}
|
|
282
282
|
return lines;
|
|
@@ -301,7 +301,7 @@ export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns, o
|
|
|
301
301
|
lines.push(
|
|
302
302
|
``,
|
|
303
303
|
`Your FIRST tool call MUST be foundry_stage_begin({stage, cycle, token}) using the values above.`,
|
|
304
|
-
`Your LAST tool call MUST be foundry_stage_end(
|
|
304
|
+
`Your LAST tool call MUST be foundry_stage_end().`,
|
|
305
305
|
);
|
|
306
306
|
return lines.join('\n');
|
|
307
307
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Foundry v3.x orchestrate: post-dispatch handlers for structured output
|
|
2
|
+
// reading from .foundry/stage-outputs/ files.
|
|
3
|
+
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { computeArtefactVersion } from './lib/artefacts.js';
|
|
6
|
+
import { enforceForgeContract } from './lib/forge-contract.js';
|
|
7
|
+
import { loadHistory } from './lib/history.js';
|
|
8
|
+
import { stageBaseOf } from './lib/stage-guard.js';
|
|
9
|
+
import { readForgeFilePatterns, violation } from './orchestrate-cycle.js';
|
|
10
|
+
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
11
|
+
import { finaliseStage } from './orchestrate-phases.js';
|
|
12
|
+
|
|
13
|
+
const FORGE_CTX = '.foundry/forge-context.json';
|
|
14
|
+
|
|
15
|
+
export function safeUnlink(io, filePath) {
|
|
16
|
+
try { io.unlink(filePath); } catch (err) {
|
|
17
|
+
if (err.code !== 'ENOENT') throw err;
|
|
18
|
+
console.warn(`ENOENT: file already removed: ${filePath}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function readStageOutput(filePath, io) {
|
|
23
|
+
try {
|
|
24
|
+
const content = await io.readFile(filePath);
|
|
25
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
26
|
+
if (lines.length === 0) return [];
|
|
27
|
+
return lines.map(line => JSON.parse(line));
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readForgeStageOutput(io) {
|
|
34
|
+
let entries;
|
|
35
|
+
try { entries = await io.readDir('.foundry/stage-outputs'); } catch { entries = []; }
|
|
36
|
+
return parseForgeOutput(entries, io);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function parseForgeOutput(entries, io) {
|
|
40
|
+
const files = (Array.isArray(entries) ? entries : []).filter(f => f.endsWith('.jsonl'));
|
|
41
|
+
if (files.length === 0) return { ok: false, error: 'forge: no stage output found' };
|
|
42
|
+
|
|
43
|
+
const allOutputs = [];
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
const filePath = path.join('.foundry/stage-outputs', f);
|
|
46
|
+
const outputs = await readStageOutput(filePath, io);
|
|
47
|
+
allOutputs.push(...outputs);
|
|
48
|
+
safeUnlink(io, filePath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (allOutputs.length === 0) return { ok: false, error: 'forge: malformed stage output' };
|
|
52
|
+
return { ok: true, outputs: allOutputs };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function enforceForgeStage(forgeCtx, fgResult, cycleId, io, cwd) {
|
|
56
|
+
const postVersion = await computeArtefactVersion('foundry', fgResult.outputType, io, cwd);
|
|
57
|
+
const feedbackStore = openFeedbackStore('WORK.feedback.yaml', io);
|
|
58
|
+
|
|
59
|
+
const stageResult = await readForgeStageOutput(io);
|
|
60
|
+
if (!stageResult.ok) return stageResult;
|
|
61
|
+
|
|
62
|
+
const outputs = stageResult.outputs;
|
|
63
|
+
if (outputs.length !== 1) {
|
|
64
|
+
return { ok: false, error: `forge stage_end: expected exactly 1 output object, got ${outputs.length}` };
|
|
65
|
+
}
|
|
66
|
+
const output = outputs[0];
|
|
67
|
+
const item = forgeCtx.forgeItem || null;
|
|
68
|
+
|
|
69
|
+
const { contractPassed } = enforceForgeContract({
|
|
70
|
+
item, preVersion: forgeCtx.forgePreVersion, postVersion, output, feedbackStore, cycleId,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (checkConsecutiveFailures(contractPassed, io, cycleId)) {
|
|
74
|
+
return { violation: 'forge contract failed 3 consecutive times — unable to satisfy feedback requirements' };
|
|
75
|
+
}
|
|
76
|
+
return { postVersion, contractPassed, output };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function countConsecutiveForgeFailures(io, cycleId) {
|
|
80
|
+
if (!io.exists('WORK.history.yaml')) return 0;
|
|
81
|
+
const entries = loadHistory('WORK.history.yaml', cycleId, io);
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
84
|
+
if (stageBaseOf(entries[i].stage) !== 'forge') break;
|
|
85
|
+
if (entries[i].contract_passed === false) count++;
|
|
86
|
+
else break;
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function checkConsecutiveFailures(contractPassed, io, cycleId) {
|
|
92
|
+
if (!contractPassed) {
|
|
93
|
+
return countConsecutiveForgeFailures(io, cycleId) + 1 >= 3;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function captureForgeContext(sortResult, args, preCheck, io) {
|
|
99
|
+
const fgResult = await readForgeFilePatterns(preCheck.cycleId, io);
|
|
100
|
+
if (!fgResult) return;
|
|
101
|
+
const preVersion = await computeArtefactVersion('foundry', fgResult.outputType, io, args.cwd);
|
|
102
|
+
const allItems = openFeedbackStore('WORK.feedback.yaml', io).list();
|
|
103
|
+
// Only capture unresolved items (open/rejected) — resolved items are terminal
|
|
104
|
+
// and presenting them to forge causes the contract to fail and revert them.
|
|
105
|
+
const unresolvedItems = allItems.filter(item => {
|
|
106
|
+
const state = item.history?.[0]?.state ?? 'open';
|
|
107
|
+
return state === 'open' || state === 'rejected';
|
|
108
|
+
});
|
|
109
|
+
if (!io.exists('.foundry')) io.mkdir('.foundry');
|
|
110
|
+
const forgeItem = unresolvedItems.length > 0
|
|
111
|
+
? ({
|
|
112
|
+
id: unresolvedItems[0].id,
|
|
113
|
+
file: unresolvedItems[0].file,
|
|
114
|
+
tag: unresolvedItems[0].tag,
|
|
115
|
+
text: unresolvedItems[0].text,
|
|
116
|
+
source: (typeof unresolvedItems[0].source === 'string'
|
|
117
|
+
? unresolvedItems[0].source.split(':')[0]
|
|
118
|
+
: unresolvedItems[0].source),
|
|
119
|
+
sourceAlias: unresolvedItems[0].source,
|
|
120
|
+
})
|
|
121
|
+
: null;
|
|
122
|
+
const ctx = { forgePreVersion: preVersion, forgeItem };
|
|
123
|
+
io.writeFile(FORGE_CTX, JSON.stringify(ctx));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function runForgePostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
127
|
+
const fgResult = await readForgeFilePatterns(cycleId, io);
|
|
128
|
+
const base = { lastStage, activeStage, cycleId, io, finalize: args.finalize, git: args.git };
|
|
129
|
+
if (!fgResult) return finaliseStage(base);
|
|
130
|
+
if (!io.exists(FORGE_CTX)) return finaliseStage(base);
|
|
131
|
+
const forgeCtx = JSON.parse(io.readFile(FORGE_CTX));
|
|
132
|
+
io.unlink(FORGE_CTX);
|
|
133
|
+
const result = await enforceForgeStage(forgeCtx, fgResult, cycleId, io, args.cwd);
|
|
134
|
+
if (result.violation) return violation(result.violation, []);
|
|
135
|
+
if (result.error) return violation(result.error, []);
|
|
136
|
+
return finaliseStage({
|
|
137
|
+
...base,
|
|
138
|
+
postVersion: result.postVersion,
|
|
139
|
+
contractPassed: result.contractPassed,
|
|
140
|
+
structuredSummary: JSON.stringify(result.output),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isVerdictApproved(output) {
|
|
145
|
+
return output && output.verdict === 'approved';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function readStageOutputFiles(io) {
|
|
149
|
+
const dir = '.foundry/stage-outputs';
|
|
150
|
+
let entries;
|
|
151
|
+
try { entries = await io.readDir(dir); } catch { entries = []; }
|
|
152
|
+
return (entries || []).filter(f => f.endsWith('.jsonl'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveHumanAppraiseVerdict(output) {
|
|
156
|
+
return isVerdictApproved(output) ? 'approved' : '';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cleanupHumanAppraiseFiles(io, files) {
|
|
160
|
+
for (const f of files) {
|
|
161
|
+
safeUnlink(io, path.join('.foundry/stage-outputs', f));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function runHumanAppraisePostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
166
|
+
const files = await readStageOutputFiles(io);
|
|
167
|
+
|
|
168
|
+
if (files.length === 0) {
|
|
169
|
+
return finaliseStage({
|
|
170
|
+
lastStage, activeStage, cycleId, io,
|
|
171
|
+
finalize: args.finalize, git: args.git,
|
|
172
|
+
structuredSummary: '',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (files.length > 1) {
|
|
177
|
+
cleanupHumanAppraiseFiles(io, files);
|
|
178
|
+
return finaliseStage({
|
|
179
|
+
lastStage, activeStage, cycleId, io,
|
|
180
|
+
finalize: args.finalize, git: args.git,
|
|
181
|
+
structuredSummary: '',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const outputs = await readStageOutput(path.join('.foundry/stage-outputs', files[0]), io);
|
|
186
|
+
cleanupHumanAppraiseFiles(io, files);
|
|
187
|
+
|
|
188
|
+
// Any approved verdict in the outputs means approved
|
|
189
|
+
const approved = outputs.some(o => o && o.verdict === 'approved');
|
|
190
|
+
const structuredSummary = approved ? 'approved' : '';
|
|
191
|
+
|
|
192
|
+
return finaliseStage({
|
|
193
|
+
lastStage, activeStage, cycleId, io,
|
|
194
|
+
finalize: args.finalize, git: args.git,
|
|
195
|
+
structuredSummary,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function runAppraisePostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
200
|
+
const files = await readStageOutputFiles(io);
|
|
201
|
+
|
|
202
|
+
if (files.length === 0) {
|
|
203
|
+
return finaliseStage({
|
|
204
|
+
lastStage, activeStage, cycleId, io,
|
|
205
|
+
finalize: args.finalize, git: args.git,
|
|
206
|
+
structuredSummary: undefined,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let totalCount = 0;
|
|
211
|
+
for (const f of files) {
|
|
212
|
+
const filePath = path.join('.foundry/stage-outputs', f);
|
|
213
|
+
const outputs = await readStageOutput(filePath, io);
|
|
214
|
+
totalCount += outputs.length;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Do NOT clean up files — consolidateAppraise handles cleanup later
|
|
218
|
+
|
|
219
|
+
const structuredSummary = totalCount > 0 ? `found:${totalCount}` : undefined;
|
|
220
|
+
|
|
221
|
+
return finaliseStage({
|
|
222
|
+
lastStage, activeStage, cycleId, io,
|
|
223
|
+
finalize: args.finalize, git: args.git,
|
|
224
|
+
structuredSummary,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function routePostDispatchStage(baseStageName, opts) {
|
|
229
|
+
if (baseStageName === 'forge') return runForgePostDispatch(opts.args, opts.activeStage, opts.lastStage, opts.cycleId, opts.io);
|
|
230
|
+
if (baseStageName === 'human-appraise') return runHumanAppraisePostDispatch(opts.args, opts.activeStage, opts.lastStage, opts.cycleId, opts.io);
|
|
231
|
+
if (baseStageName === 'appraise') return runAppraisePostDispatch(opts.args, opts.activeStage, opts.lastStage, opts.cycleId, opts.io);
|
|
232
|
+
return finaliseStage({
|
|
233
|
+
lastStage: opts.lastStage,
|
|
234
|
+
activeStage: opts.activeStage,
|
|
235
|
+
cycleId: opts.cycleId,
|
|
236
|
+
io: opts.io,
|
|
237
|
+
finalize: opts.args.finalize,
|
|
238
|
+
git: opts.args.git,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -20,22 +20,25 @@ function buildFinalizeViolation(finalizeResult) {
|
|
|
20
20
|
return violation(`stage_finalize error: ${finalizeResult.error}`, []);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function resolveStageSummary(ctx) {
|
|
24
|
+
return ctx.structuredSummary || ctx.lastStage.summary || '(no summary)';
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
function buildStageEntryBase(ctx) {
|
|
24
|
-
const summary = ctx
|
|
28
|
+
const summary = resolveStageSummary(ctx);
|
|
25
29
|
const changed = ctx.lastStage.changedFiles ?? [];
|
|
26
|
-
|
|
30
|
+
const base = { cycle: ctx.cycleId, stage: ctx.lastStage.stage,
|
|
27
31
|
iteration: ctx.iteration, comment: summary,
|
|
28
32
|
openFeedback: ctx.openFeedback, changedFiles: changed,
|
|
29
|
-
...(baseStage(ctx.lastStage.stage || '') === 'forge'
|
|
30
|
-
? buildForgeHistoryEntry({
|
|
31
|
-
cycle: ctx.cycleId, stage: ctx.lastStage.stage,
|
|
32
|
-
iteration: ctx.iteration, comment: summary,
|
|
33
|
-
artefactVersion: ctx.artefactVersion,
|
|
34
|
-
contractPassed: ctx.contractPassed,
|
|
35
|
-
changedFiles: changed,
|
|
36
|
-
})
|
|
37
|
-
: {}),
|
|
38
33
|
};
|
|
34
|
+
if (baseStage(ctx.lastStage.stage || '') !== 'forge') return base;
|
|
35
|
+
return { ...base, ...buildForgeHistoryEntry({
|
|
36
|
+
cycle: ctx.cycleId, stage: ctx.lastStage.stage,
|
|
37
|
+
iteration: ctx.iteration, comment: summary,
|
|
38
|
+
artefactVersion: ctx.artefactVersion,
|
|
39
|
+
contractPassed: ctx.contractPassed,
|
|
40
|
+
changedFiles: changed,
|
|
41
|
+
}) };
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
function writeHistoryEntries(ctx) {
|
|
@@ -58,8 +61,8 @@ async function computeAllowedPatterns(lastStage, cycleId, io) {
|
|
|
58
61
|
return allowedPatternsForStage({ stageBase: stageB, forgeFilePatterns });
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
function buildCommitMessage(cycleId, lastStage) {
|
|
62
|
-
return `[${cycleId}] ${lastStage.stage}: ${lastStage.summary || '(no summary)'}`;
|
|
64
|
+
function buildCommitMessage(cycleId, lastStage, structuredSummary) {
|
|
65
|
+
return `[${cycleId}] ${lastStage.stage}: ${structuredSummary || lastStage.summary || '(no summary)'}`;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
function rollbackState(io, original) {
|
|
@@ -68,10 +71,10 @@ function rollbackState(io, original) {
|
|
|
68
71
|
else if (io.exists('WORK.history.yaml')) { io.unlink('WORK.history.yaml'); }
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
async function tryStageCommit(git, lastStage, cycleId, io) {
|
|
74
|
+
async function tryStageCommit(git, lastStage, cycleId, io, structuredSummary) {
|
|
72
75
|
if (!git || typeof git.commit !== 'function') return null;
|
|
73
76
|
const allowedPatterns = await computeAllowedPatterns(lastStage, cycleId, io);
|
|
74
|
-
return tryCommit(git, buildCommitMessage(cycleId, lastStage), allowedPatterns, lastStage.stage);
|
|
77
|
+
return tryCommit(git, buildCommitMessage(cycleId, lastStage, structuredSummary), allowedPatterns, lastStage.stage);
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
function clearStageState(activeStage, lastStage, io) {
|
|
@@ -80,7 +83,7 @@ function clearStageState(activeStage, lastStage, io) {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
export async function finaliseStage(args) {
|
|
83
|
-
const { lastStage, activeStage, cycleId, io, finalize, git, postVersion, contractPassed } = args;
|
|
86
|
+
const { lastStage, activeStage, cycleId, io, finalize, git, postVersion, contractPassed, structuredSummary } = args;
|
|
84
87
|
const original = {
|
|
85
88
|
workMd: io.readFile('WORK.md'),
|
|
86
89
|
history: io.exists('WORK.history.yaml') ? io.readFile('WORK.history.yaml') : null,
|
|
@@ -104,8 +107,9 @@ export async function finaliseStage(args) {
|
|
|
104
107
|
lastStage: { ...lastStage, changedFiles: finalizeResult.changedFiles },
|
|
105
108
|
iteration, openFeedback, io,
|
|
106
109
|
artefactVersion: postVersion, contractPassed,
|
|
110
|
+
structuredSummary,
|
|
107
111
|
});
|
|
108
|
-
const commitErr = await tryStageCommit(git, lastStage, cycleId, io);
|
|
112
|
+
const commitErr = await tryStageCommit(git, lastStage, cycleId, io, structuredSummary);
|
|
109
113
|
if (commitErr) {
|
|
110
114
|
rollbackState(io, original);
|
|
111
115
|
clearStageState(activeStage, null, io);
|
|
@@ -7,9 +7,6 @@ import matter from 'gray-matter';
|
|
|
7
7
|
import { readActiveStage, readLastStage, writeActiveStage, clearActiveStage } from './lib/state.js';
|
|
8
8
|
import { stageBaseOf } from './lib/stage-guard.js';
|
|
9
9
|
import { ulid as defaultUlid } from './lib/ulid.js';
|
|
10
|
-
import { computeArtefactVersion } from './lib/artefacts.js';
|
|
11
|
-
import { enforceForgeContract } from './lib/forge-contract.js';
|
|
12
|
-
import { loadHistory } from './lib/history.js';
|
|
13
10
|
import { getCycleDefinition } from './lib/config.js';
|
|
14
11
|
import {
|
|
15
12
|
readCycleTargets,
|
|
@@ -33,6 +30,11 @@ import { runQuench } from './quench-module.js';
|
|
|
33
30
|
import { gatherAppraiseContext, consolidateAppraise } from './appraise-module.js';
|
|
34
31
|
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
35
32
|
import { guardNoWorkMd, guardMissingCycleId, guardSetupInconsistent, guardOrphanedStage, guardMissingLastStage, guardLastResults } from './lib/orchestrate-guards.js';
|
|
33
|
+
import {
|
|
34
|
+
captureForgeContext,
|
|
35
|
+
enforceForgeStage,
|
|
36
|
+
routePostDispatchStage,
|
|
37
|
+
} from './orchestrate-dispatch.js';
|
|
36
38
|
|
|
37
39
|
export {
|
|
38
40
|
renderDispatchPrompt, synthesizeStages, computeOpenFeedback,
|
|
@@ -112,7 +114,8 @@ function buildQuenchContext(cycleId, args, io) {
|
|
|
112
114
|
|
|
113
115
|
async function buildAppraiseCtx(cycleId, args, io) {
|
|
114
116
|
const stageId = `appraise:${cycleId}`;
|
|
115
|
-
const
|
|
117
|
+
const cycleModel = await readAppraiseModel(cycleId, io);
|
|
118
|
+
const defaultModel = cycleModel || args.defaultModel;
|
|
116
119
|
return { cycleId, io, git: args.git, finalize: buildFinalizeWrapper(cycleId, args, io),
|
|
117
120
|
foundryDir: 'foundry', defaultModel,
|
|
118
121
|
baseBranch: args.baseBranch ?? 'main', cwd: args.cwd ?? process.cwd(),
|
|
@@ -139,77 +142,6 @@ function writeStageRecord(io, cycleId, route) {
|
|
|
139
142
|
writeActiveStage(io, { cycle: cycleId, stage: `${route}`, token: null, baseSha: resolveBaseSha(io) });
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
const FORGE_CTX = '.foundry/forge-context.json';
|
|
143
|
-
|
|
144
|
-
async function captureForgeContext(sortResult, args, preCheck, io) {
|
|
145
|
-
const fgResult = await readForgeFilePatterns(preCheck.cycleId, io);
|
|
146
|
-
if (!fgResult) return;
|
|
147
|
-
const preVersion = await computeArtefactVersion('foundry', fgResult.outputType, io, args.cwd);
|
|
148
|
-
const allItems = openFeedbackStore('WORK.feedback.yaml', io).list();
|
|
149
|
-
// Only capture unresolved items (open/rejected) — resolved items are terminal
|
|
150
|
-
// and presenting them to forge causes the contract to fail and revert them.
|
|
151
|
-
const unresolvedItems = allItems.filter(item => {
|
|
152
|
-
const state = item.history?.[0]?.state ?? 'open';
|
|
153
|
-
return state === 'open' || state === 'rejected';
|
|
154
|
-
});
|
|
155
|
-
if (!io.exists('.foundry')) io.mkdir('.foundry');
|
|
156
|
-
const forgeItem = unresolvedItems.length > 0
|
|
157
|
-
? ({
|
|
158
|
-
id: unresolvedItems[0].id,
|
|
159
|
-
file: unresolvedItems[0].file,
|
|
160
|
-
tag: unresolvedItems[0].tag,
|
|
161
|
-
text: unresolvedItems[0].text,
|
|
162
|
-
source: (typeof unresolvedItems[0].source === 'string'
|
|
163
|
-
? unresolvedItems[0].source.split(':')[0]
|
|
164
|
-
: unresolvedItems[0].source),
|
|
165
|
-
sourceAlias: unresolvedItems[0].source,
|
|
166
|
-
})
|
|
167
|
-
: null;
|
|
168
|
-
const ctx = { forgePreVersion: preVersion, forgeItem };
|
|
169
|
-
io.writeFile(FORGE_CTX, JSON.stringify(ctx));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function countConsecutiveForgeFailures(io, cycleId) {
|
|
173
|
-
if (!io.exists('WORK.history.yaml')) return 0;
|
|
174
|
-
const entries = loadHistory('WORK.history.yaml', cycleId, io);
|
|
175
|
-
let count = 0;
|
|
176
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
177
|
-
if (stageBaseOf(entries[i].stage) !== 'forge') break;
|
|
178
|
-
if (entries[i].contract_passed === false) count++;
|
|
179
|
-
else break;
|
|
180
|
-
}
|
|
181
|
-
return count;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function checkConsecutiveFailures(contractPassed, io, cycleId) {
|
|
185
|
-
if (!contractPassed) {
|
|
186
|
-
return countConsecutiveForgeFailures(io, cycleId) + 1 >= 3;
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function enforceForgeStage(forgeCtx, fgResult, cycleId, io, cwd) {
|
|
192
|
-
const postVersion = await computeArtefactVersion('foundry', fgResult.outputType, io, cwd);
|
|
193
|
-
const feedbackStore = openFeedbackStore('WORK.feedback.yaml', io);
|
|
194
|
-
const lastStage = readLastStage(io);
|
|
195
|
-
const summary = (lastStage && lastStage.summary) || '';
|
|
196
|
-
const item = forgeCtx.forgeItem || null;
|
|
197
|
-
|
|
198
|
-
const { contractPassed } = enforceForgeContract({
|
|
199
|
-
item,
|
|
200
|
-
preVersion: forgeCtx.forgePreVersion,
|
|
201
|
-
postVersion,
|
|
202
|
-
summary,
|
|
203
|
-
feedbackStore,
|
|
204
|
-
cycleId,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (checkConsecutiveFailures(contractPassed, io, cycleId)) {
|
|
208
|
-
return { violation: 'forge contract failed 3 consecutive times — unable to satisfy feedback requirements' };
|
|
209
|
-
}
|
|
210
|
-
return { postVersion, contractPassed };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
145
|
async function handleQuenchRoute(sortResult, preCheck, args, io) {
|
|
214
146
|
writeStageRecord(io, preCheck.cycleId, sortResult.route);
|
|
215
147
|
const quenchCtx = buildQuenchContext(preCheck.cycleId, args, io);
|
|
@@ -269,7 +201,7 @@ export async function runOrchestrate(args, io) {
|
|
|
269
201
|
function checkFlowGuards(args, activeStage, lastStage) {
|
|
270
202
|
const lastResultsErr = guardLastResults(args, activeStage, lastStage);
|
|
271
203
|
if (lastResultsErr) return lastResultsErr;
|
|
272
|
-
// Only flag orphaned stage when not on consolidation path
|
|
204
|
+
// Only flag orphaned stage when not on consolidation path
|
|
273
205
|
if (args.lastResults === undefined) return guardOrphanedStage(activeStage, args.lastResult);
|
|
274
206
|
return null;
|
|
275
207
|
}
|
|
@@ -305,25 +237,11 @@ async function runSetupIfNeeded(preCheck, args, io) {
|
|
|
305
237
|
return err || setupWorkfile(buildSetupArgs(preCheck, args, io));
|
|
306
238
|
}
|
|
307
239
|
|
|
308
|
-
async function runForgePostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
309
|
-
const fgResult = await readForgeFilePatterns(cycleId, io);
|
|
310
|
-
const base = { lastStage, activeStage, cycleId, io, finalize: args.finalize, git: args.git };
|
|
311
|
-
if (!fgResult) return finaliseStage(base);
|
|
312
|
-
if (!io.exists(FORGE_CTX)) return finaliseStage(base);
|
|
313
|
-
const forgeCtx = JSON.parse(io.readFile(FORGE_CTX));
|
|
314
|
-
io.unlink(FORGE_CTX);
|
|
315
|
-
const result = await enforceForgeStage(forgeCtx, fgResult, cycleId, io, args.cwd);
|
|
316
|
-
if (result.violation) return violation(result.violation, []);
|
|
317
|
-
return finaliseStage({ ...base, postVersion: result.postVersion, contractPassed: result.contractPassed });
|
|
318
|
-
}
|
|
319
|
-
|
|
320
240
|
async function runPostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
321
241
|
if (!args.lastResult) return null;
|
|
322
242
|
if (args.lastResult.ok === false) {
|
|
323
243
|
return handleViolation({ lastResult: args.lastResult, activeStage, lastStage, cycleId, io });
|
|
324
244
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (stageBaseOf(lastStage.stage) === 'forge') return runForgePostDispatch(args, activeStage, lastStage, cycleId, io);
|
|
328
|
-
return finaliseStage({ lastStage, activeStage, cycleId, io, finalize: args.finalize, git: args.git });
|
|
245
|
+
if (guardMissingLastStage(lastStage)) return guardMissingLastStage(lastStage);
|
|
246
|
+
return routePostDispatchStage(stageBaseOf(lastStage.stage), { args, activeStage, lastStage, cycleId, io });
|
|
329
247
|
}
|