@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
|
@@ -4,26 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
import { runSort } from './sort.js';
|
|
6
6
|
import { parseFrontmatter } from './lib/workfile.js';
|
|
7
|
-
import { readActiveStage, readLastStage } from './lib/state.js';
|
|
7
|
+
import { readActiveStage, readLastStage, writeActiveStage, clearActiveStage } from './lib/state.js';
|
|
8
|
+
import { stageBaseOf } from './lib/stage-guard.js';
|
|
8
9
|
import { ulid as defaultUlid } from './lib/ulid.js';
|
|
9
10
|
import {
|
|
10
|
-
findCycleOutputArtefact,
|
|
11
11
|
readCycleTargets,
|
|
12
12
|
readForgeFilePatterns,
|
|
13
13
|
renderDispatchPrompt,
|
|
14
14
|
synthesizeStages,
|
|
15
15
|
violation,
|
|
16
16
|
computeOpenFeedback,
|
|
17
|
+
DISPATCH_MULTI_ACTION,
|
|
18
|
+
validateDispatchMulti,
|
|
19
|
+
buildDispatchMultiResponse,
|
|
17
20
|
} from './orchestrate-cycle.js';
|
|
18
21
|
import {
|
|
19
22
|
handleSortResult,
|
|
20
23
|
setupWorkfile,
|
|
21
24
|
finaliseStage,
|
|
22
25
|
handleViolation,
|
|
26
|
+
routeDispatch,
|
|
23
27
|
} from './orchestrate-phases.js';
|
|
28
|
+
import { runQuench } from './quench-module.js';
|
|
29
|
+
import { gatherAppraiseContext, consolidateAppraise } from './appraise-module.js';
|
|
30
|
+
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
24
31
|
|
|
25
|
-
export {
|
|
26
|
-
|
|
32
|
+
export {
|
|
33
|
+
renderDispatchPrompt, synthesizeStages, computeOpenFeedback,
|
|
34
|
+
DISPATCH_MULTI_ACTION, validateDispatchMulti, buildDispatchMultiResponse,
|
|
35
|
+
};
|
|
36
|
+
export { gatherAppraiseContext, consolidateAppraise };
|
|
37
|
+
export { readCycleTargets, readForgeFilePatterns };
|
|
27
38
|
export { handleSortResult as __handleSortResultForTest };
|
|
28
39
|
|
|
29
40
|
export function needsSetup(workMdContent) {
|
|
@@ -38,28 +49,19 @@ export function needsSetup(workMdContent) {
|
|
|
38
49
|
// ---------------------------------------------------------------------------
|
|
39
50
|
|
|
40
51
|
function guardNoWorkMd(io) {
|
|
41
|
-
if (!io.exists('WORK.md'))
|
|
42
|
-
return violation('no WORK.md; flow skill must create it first');
|
|
43
|
-
}
|
|
52
|
+
if (!io.exists('WORK.md')) return violation('no WORK.md; flow skill must create it first');
|
|
44
53
|
return null;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
function guardMissingCycleId(io) {
|
|
48
57
|
const workContent = io.readFile('WORK.md');
|
|
49
58
|
const fm = parseFrontmatter(workContent);
|
|
50
|
-
if (!fm.cycle)
|
|
51
|
-
return violation('WORK.md frontmatter missing cycle field', ['WORK.md']);
|
|
52
|
-
}
|
|
59
|
+
if (!fm.cycle) return violation('WORK.md frontmatter missing cycle field', ['WORK.md']);
|
|
53
60
|
return { cycleId: fm.cycle, workContent };
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
function guardSetupInconsistent(lastResult) {
|
|
57
|
-
if (lastResult)
|
|
58
|
-
return violation(
|
|
59
|
-
'inconsistent state: lastResult provided but WORK.md still needs setup',
|
|
60
|
-
['WORK.md'],
|
|
61
|
-
);
|
|
62
|
-
}
|
|
64
|
+
if (lastResult) return violation('inconsistent state: lastResult provided but WORK.md still needs setup', ['WORK.md']);
|
|
63
65
|
return null;
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -75,61 +77,188 @@ function guardOrphanedStage(activeStage, lastResult) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
function guardMissingLastStage(lastStage) {
|
|
78
|
-
if (!lastStage)
|
|
79
|
-
return violation('lastResult provided but no last stage recorded — orphaned state');
|
|
80
|
-
}
|
|
80
|
+
if (!lastStage) return violation('lastResult provided but no last stage recorded — orphaned state');
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function checkLastResultsConflict(args) {
|
|
85
|
+
if (args.lastResult !== undefined && args.lastResults !== undefined) return violation('lastResult and lastResults are mutually exclusive');
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function checkLastResultsShape(args) {
|
|
90
|
+
if (args.lastResults === undefined) return null;
|
|
91
|
+
if (!Array.isArray(args.lastResults)) return violation('lastResults must be an array');
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isDuplicateConsolidation(lastStage, activeStage) {
|
|
96
|
+
return lastStage && lastStage.stage === activeStage.stage;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function checkLastResultsStageContext(args, activeStage, lastStage) {
|
|
100
|
+
if (args.lastResults === undefined) return null;
|
|
101
|
+
if (!activeStage) return violation('lastResults provided but no active stage exists');
|
|
102
|
+
if (stageBaseOf(activeStage.stage) !== 'appraise') return violation(`lastResults provided but active stage "${activeStage.stage}" is not an appraise stage`);
|
|
103
|
+
if (isDuplicateConsolidation(lastStage, activeStage)) return violation(`duplicate lastResults: consolidation already completed for this appraise stage "${activeStage.stage}"`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function guardLastResults(args, activeStage, lastStage) {
|
|
108
|
+
return checkLastResultsConflict(args)
|
|
109
|
+
?? checkLastResultsShape(args)
|
|
110
|
+
?? checkLastResultsStageContext(args, activeStage, lastStage);
|
|
111
|
+
}
|
|
112
|
+
|
|
84
113
|
function buildSortArgs(args, now) {
|
|
85
114
|
return {
|
|
86
|
-
cycleDef: args.cycleDef ?? null,
|
|
87
|
-
mint: args.mint,
|
|
115
|
+
cycleDef: args.cycleDef ?? null, mint: args.mint,
|
|
88
116
|
now: typeof now === 'function' ? now() : now,
|
|
89
|
-
ulid: args.ulid ?? defaultUlid,
|
|
90
|
-
defaultModel: args.defaultModel,
|
|
117
|
+
ulid: args.ulid ?? defaultUlid, defaultModel: args.defaultModel,
|
|
91
118
|
};
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
function buildSortContext(cycleId, args, io) {
|
|
95
|
-
return { cycleId, cwd: args.cwd ?? process.cwd(), io };
|
|
122
|
+
return { cycleId, cwd: args.cwd ?? process.cwd(), io, foundryDir: args.foundryDir ?? 'foundry', baseBranch: args.baseBranch ?? 'main' };
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
function buildSetupArgs(cycleResult, args, io) {
|
|
99
|
-
return {
|
|
100
|
-
cycleId: cycleResult.cycleId,
|
|
101
|
-
workContent: cycleResult.workContent,
|
|
102
|
-
io,
|
|
103
|
-
git: args.git,
|
|
104
|
-
foundryDir: 'foundry',
|
|
105
|
-
};
|
|
126
|
+
return { cycleId: cycleResult.cycleId, workContent: cycleResult.workContent, io, git: args.git, foundryDir: 'foundry' };
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
function isViolation(result) {
|
|
109
130
|
return result && result.action === 'violation';
|
|
110
131
|
}
|
|
111
132
|
|
|
133
|
+
function buildFinalizeWrapper(cycleId, args, io) {
|
|
134
|
+
return ({ lastStage, activeStage }) =>
|
|
135
|
+
finaliseStage({ lastStage, activeStage, cycleId, io, finalize: args.finalize, git: args.git });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildFeedback(cycleId, stageId, io) {
|
|
139
|
+
return {
|
|
140
|
+
add: (item) => {
|
|
141
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
142
|
+
return store.add({ file: item.file, tag: item.tag, text: item.text, source: stageId, cycle: cycleId });
|
|
143
|
+
},
|
|
144
|
+
list: (query) => {
|
|
145
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
146
|
+
let items = store.list();
|
|
147
|
+
if (query?.file) items = items.filter(it => it.file === query.file);
|
|
148
|
+
if (query?.source) items = items.filter(it => it.source === query.source);
|
|
149
|
+
return items;
|
|
150
|
+
},
|
|
151
|
+
resolve: (id, decision, reason) => {
|
|
152
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
153
|
+
const target = decision === 'approved' ? 'resolved' : 'rejected';
|
|
154
|
+
return store.transition({ id, target, stage: stageId, cycle: cycleId });
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildQuenchContext(cycleId, args, io) {
|
|
160
|
+
const stageId = `quench:${cycleId}`;
|
|
161
|
+
return { cycleId, stageId, io, git: args.git, finalize: buildFinalizeWrapper(cycleId, args, io),
|
|
162
|
+
now: args.now, ulid: args.ulid, mint: args.mint, foundryDir: 'foundry', defaultModel: args.defaultModel,
|
|
163
|
+
baseBranch: args.baseBranch ?? 'main',
|
|
164
|
+
feedback: buildFeedback(cycleId, stageId, io) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildAppraiseCtx(cycleId, args, io) {
|
|
168
|
+
const stageId = `appraise:${cycleId}`;
|
|
169
|
+
return { cycleId, io, git: args.git, finalize: buildFinalizeWrapper(cycleId, args, io),
|
|
170
|
+
foundryDir: 'foundry', defaultModel: args.defaultModel,
|
|
171
|
+
baseBranch: args.baseBranch ?? 'main',
|
|
172
|
+
activeStage: readActiveStage(io), lastStage: readLastStage(io),
|
|
173
|
+
feedback: buildFeedback(cycleId, stageId, io) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveBaseSha(io) {
|
|
177
|
+
try {
|
|
178
|
+
const sha = io.exec(['git', 'rev-parse', 'HEAD']);
|
|
179
|
+
if (sha && typeof sha === 'string' && sha.trim()) return sha.trim();
|
|
180
|
+
} catch { /* use default */ }
|
|
181
|
+
return '0000000';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function writeStageRecord(io, cycleId, route) {
|
|
185
|
+
writeActiveStage(io, { cycle: cycleId, stage: `${route}`, token: null, baseSha: resolveBaseSha(io) });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function handleQuenchRoute(sortResult, preCheck, args, io) {
|
|
189
|
+
writeStageRecord(io, preCheck.cycleId, sortResult.route);
|
|
190
|
+
const quenchCtx = buildQuenchContext(preCheck.cycleId, args, io);
|
|
191
|
+
const quenchResult = await runQuench(quenchCtx);
|
|
192
|
+
if (quenchResult.ok === false) return violation(quenchResult.error || 'quench failed');
|
|
193
|
+
const nextSort = runSort(buildSortArgs(args, args.now ?? Date.now), io);
|
|
194
|
+
return dispatchByRoute(nextSort, args, preCheck, io);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function handleAppraiseGatherRoute(sortResult, preCheck, args, io) {
|
|
198
|
+
writeStageRecord(io, preCheck.cycleId, sortResult.route);
|
|
199
|
+
const result = await gatherAppraiseContext(buildAppraiseCtx(preCheck.cycleId, args, io));
|
|
200
|
+
if (result.action === 'violation') { clearActiveStage(io); return result; }
|
|
201
|
+
if (!result.tasks || result.tasks.length === 0) {
|
|
202
|
+
// No appraisers/artefacts — consolidate with empty results to advance
|
|
203
|
+
return handleAppraiseConsolidateRoute(sortResult, preCheck, { ...args, lastResults: [] }, io);
|
|
204
|
+
}
|
|
205
|
+
const validationErr = validateDispatchMulti(result);
|
|
206
|
+
if (validationErr) return validationErr;
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function handleAppraiseConsolidateRoute(sortResult, preCheck, args, io) {
|
|
211
|
+
const ctx = buildAppraiseCtx(preCheck.cycleId, args, io);
|
|
212
|
+
const result = await consolidateAppraise(ctx, args.lastResults);
|
|
213
|
+
if (result.action === 'violation') {
|
|
214
|
+
clearActiveStage(io);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
if (!result.ok) return violation(result.error || 'appraise consolidation failed');
|
|
218
|
+
const nextSort = runSort(buildSortArgs(args, args.now ?? Date.now), io);
|
|
219
|
+
return dispatchByRoute(nextSort, args, preCheck, io);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function dispatchByRoute(sortResult, args, preCheck, io) {
|
|
223
|
+
const base = routeDispatch(sortResult.route);
|
|
224
|
+
if (base === 'quench') return handleQuenchRoute(sortResult, preCheck, args, io);
|
|
225
|
+
if (base === 'appraise') {
|
|
226
|
+
return args.lastResults
|
|
227
|
+
? handleAppraiseConsolidateRoute(sortResult, preCheck, args, io)
|
|
228
|
+
: handleAppraiseGatherRoute(sortResult, preCheck, args, io);
|
|
229
|
+
}
|
|
230
|
+
return handleSortResult(sortResult, buildSortContext(preCheck.cycleId, args, io));
|
|
231
|
+
}
|
|
232
|
+
|
|
112
233
|
export async function runOrchestrate(args, io) {
|
|
113
234
|
const preCheck = runPreChecks(io);
|
|
114
235
|
if (preCheck.error) return preCheck.error;
|
|
115
236
|
return runOrchestrateFlow(preCheck, args, io);
|
|
116
237
|
}
|
|
117
238
|
|
|
239
|
+
function checkFlowGuards(args, activeStage, lastStage) {
|
|
240
|
+
const lastResultsErr = guardLastResults(args, activeStage, lastStage);
|
|
241
|
+
if (lastResultsErr) return lastResultsErr;
|
|
242
|
+
// Only flag orphaned stage when not on consolidation path (lastResults path has activeStage but no lastResult)
|
|
243
|
+
if (args.lastResults === undefined) return guardOrphanedStage(activeStage, args.lastResult);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
118
247
|
async function runOrchestrateFlow(preCheck, args, io) {
|
|
119
248
|
const setupResult = await runSetupIfNeeded(preCheck, args, io);
|
|
120
249
|
if (isViolation(setupResult)) return setupResult;
|
|
121
|
-
|
|
122
250
|
const activeStage = readActiveStage(io);
|
|
123
251
|
const lastStage = readLastStage(io);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (orphanErr) return orphanErr;
|
|
127
|
-
|
|
252
|
+
const guardErr = checkFlowGuards(args, activeStage, lastStage);
|
|
253
|
+
if (guardErr) return guardErr;
|
|
128
254
|
const postDispatchResult = await runPostDispatch(args, activeStage, lastStage, preCheck.cycleId, io);
|
|
129
255
|
if (isViolation(postDispatchResult)) return postDispatchResult;
|
|
256
|
+
return runSortAndDispatch(args, preCheck, io);
|
|
257
|
+
}
|
|
130
258
|
|
|
259
|
+
async function runSortAndDispatch(args, preCheck, io) {
|
|
131
260
|
const sortResult = runSort(buildSortArgs(args, args.now ?? Date.now), io);
|
|
132
|
-
return
|
|
261
|
+
return dispatchByRoute(sortResult, args, preCheck, io);
|
|
133
262
|
}
|
|
134
263
|
|
|
135
264
|
function runPreChecks(io) {
|
|
@@ -143,8 +272,7 @@ function runPreChecks(io) {
|
|
|
143
272
|
async function runSetupIfNeeded(preCheck, args, io) {
|
|
144
273
|
if (!needsSetup(preCheck.workContent)) return null;
|
|
145
274
|
const err = guardSetupInconsistent(args.lastResult);
|
|
146
|
-
|
|
147
|
-
return setupWorkfile(buildSetupArgs(preCheck, args, io));
|
|
275
|
+
return err || setupWorkfile(buildSetupArgs(preCheck, args, io));
|
|
148
276
|
}
|
|
149
277
|
|
|
150
278
|
async function runPostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
@@ -152,13 +280,7 @@ async function runPostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
|
152
280
|
if (args.lastResult.ok === false) {
|
|
153
281
|
return handleViolation({ lastResult: args.lastResult, activeStage, lastStage, cycleId, io });
|
|
154
282
|
}
|
|
155
|
-
|
|
156
283
|
const stageErr = guardMissingLastStage(lastStage);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return finaliseStage({
|
|
160
|
-
lastStage, activeStage, cycleId, io,
|
|
161
|
-
finalize: args.finalize ?? null,
|
|
162
|
-
git: args.git,
|
|
163
|
-
});
|
|
284
|
+
const finaliseArgs = { lastStage, activeStage, cycleId, io, finalize: args.finalize ?? null, git: args.git };
|
|
285
|
+
return stageErr || finaliseStage(finaliseArgs);
|
|
164
286
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quench module — deterministic validation run entirely within the orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* The module discovers artefact changes via branch-based artefact discovery,
|
|
5
|
+
* runs validators for each artefact change, posts feedback for each validation
|
|
6
|
+
* item, resolves prior quench feedback, and finalises the stage. No LLM
|
|
7
|
+
* involvement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readActiveStage } from './lib/state.js';
|
|
11
|
+
import { getArtefactFiles } from './lib/artefacts.js';
|
|
12
|
+
import { getCycleDefinition } from './lib/config.js';
|
|
13
|
+
import { performValidation } from './lib/validation.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run quench (deterministic validation) for a cycle.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} ctx - Context object with io, feedback, finalize, etc.
|
|
19
|
+
* @returns {Promise<{ok: boolean, summary?: string, error?: string}>}
|
|
20
|
+
*/
|
|
21
|
+
export async function runQuench(ctx) {
|
|
22
|
+
const activeStageRecord = readActiveStage(ctx.io);
|
|
23
|
+
if (!activeStageRecord) {
|
|
24
|
+
return { ok: false, error: 'No active stage found' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cycleDef = await getCycleDefinition(ctx.foundryDir, ctx.cycleId, ctx.io);
|
|
28
|
+
const outputType = cycleDef.frontmatter['output-type'];
|
|
29
|
+
if (!outputType) {
|
|
30
|
+
return { ok: false, error: `Cycle ${ctx.cycleId} has no output-type` };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const discovery = await discoverArtefacts(ctx, outputType);
|
|
34
|
+
if (!discovery.ok) return discovery;
|
|
35
|
+
const artefacts = discovery.artefacts;
|
|
36
|
+
|
|
37
|
+
if (artefacts.length === 0) {
|
|
38
|
+
return await handleNoArtefacts(ctx, activeStageRecord);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await processArtefacts(ctx, artefacts, activeStageRecord, outputType);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function discoverArtefacts(ctx, outputType) {
|
|
45
|
+
try {
|
|
46
|
+
const artefacts = await getArtefactFiles(ctx.foundryDir, outputType, ctx.io, { baseBranch: ctx.baseBranch ?? 'main' });
|
|
47
|
+
return { ok: true, artefacts };
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return { ok: false, error: `Failed to discover artefacts: ${err.message}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handle the case where no artefacts exist for this cycle.
|
|
55
|
+
*/
|
|
56
|
+
async function handleNoArtefacts(ctx, activeStageRecord) {
|
|
57
|
+
const summary = 'SKIP: no files';
|
|
58
|
+
await ctx.finalize({
|
|
59
|
+
lastStage: { stage: ctx.stageId, summary, baseSha: activeStageRecord.baseSha },
|
|
60
|
+
activeStage: activeStageRecord,
|
|
61
|
+
});
|
|
62
|
+
return { ok: true, summary };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Process each artefact: run validation, post feedback, handle errors.
|
|
67
|
+
*/
|
|
68
|
+
async function processArtefacts(ctx, artefacts, activeStageRecord, outputType) {
|
|
69
|
+
const perArtefact = [];
|
|
70
|
+
const currentFeedback = [];
|
|
71
|
+
let allOk = true;
|
|
72
|
+
|
|
73
|
+
for (const artefact of artefacts) {
|
|
74
|
+
const result = await performValidation({
|
|
75
|
+
typeId: outputType,
|
|
76
|
+
io: ctx.io,
|
|
77
|
+
foundryDir: ctx.foundryDir,
|
|
78
|
+
artefacts: [artefact],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const outcome = handleArtefactResult(ctx, artefact, result, currentFeedback);
|
|
82
|
+
perArtefact.push(outcome.text);
|
|
83
|
+
if (!outcome.ok) allOk = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const summary = perArtefact.join('; ');
|
|
87
|
+
resolvePriorFeedback(ctx, currentFeedback);
|
|
88
|
+
|
|
89
|
+
await ctx.finalize({
|
|
90
|
+
lastStage: { stage: ctx.stageId, summary, baseSha: activeStageRecord.baseSha },
|
|
91
|
+
activeStage: activeStageRecord,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!allOk) {
|
|
95
|
+
return { ok: false, summary, error: 'One or more artefacts failed validation' };
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, summary };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Handle validation result for a single artefact.
|
|
102
|
+
*
|
|
103
|
+
* Returns { ok, text } where `ok` indicates whether the artefact passed
|
|
104
|
+
* validation and `text` is the per-artefact summary line.
|
|
105
|
+
*/
|
|
106
|
+
function handleArtefactResult(ctx, artefact, result, currentFeedback) {
|
|
107
|
+
if (isNoValidators(result)) {
|
|
108
|
+
return { ok: true, text: `${artefact.file}: OK: no validators` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (result.error) {
|
|
112
|
+
return { ok: false, text: `${artefact.file}: ${result.error}` };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isAllErrors(result)) {
|
|
116
|
+
const messages = result.errors.map(e => e.message).join('; ');
|
|
117
|
+
return { ok: false, text: `${artefact.file}: ${messages}` };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
postFeedbackItems(ctx, artefact, result, currentFeedback);
|
|
121
|
+
return { ok: true, text: `${artefact.file}: ${result.items.length} issues found` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* True when no validators are configured for this artefact type.
|
|
126
|
+
*/
|
|
127
|
+
function isNoValidators(result) {
|
|
128
|
+
return result.ok && result.validatorsRun === 0 && result.items.length === 0 && result.errors.length === 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* True when validators ran but produced only errors with no valid items.
|
|
133
|
+
*/
|
|
134
|
+
function isAllErrors(result) {
|
|
135
|
+
return result.items.length === 0 && result.errors.length > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Post feedback items for validation results and track for resolution.
|
|
140
|
+
*/
|
|
141
|
+
function postFeedbackItems(ctx, artefact, result, currentFeedback) {
|
|
142
|
+
for (const item of result.items) {
|
|
143
|
+
const tag = `law:${item.lawId}:${item.validatorId}`;
|
|
144
|
+
ctx.feedback.add({ file: artefact.file, text: item.text, tag });
|
|
145
|
+
currentFeedback.push({ file: artefact.file, tag });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve prior quench feedback items against current results.
|
|
151
|
+
*/
|
|
152
|
+
function resolvePriorFeedback(ctx, currentFeedback) {
|
|
153
|
+
const currentSigs = new Set(currentFeedback.map(fb => `${fb.file}:${fb.tag}`));
|
|
154
|
+
const priorItems = ctx.feedback.list({ source: ctx.stageId });
|
|
155
|
+
|
|
156
|
+
for (const prior of priorItems) {
|
|
157
|
+
if (prior.state === 'resolved') continue;
|
|
158
|
+
const sig = `${prior.file}:${prior.tag}`;
|
|
159
|
+
const decision = currentSigs.has(sig) ? 'rejected' : 'approved';
|
|
160
|
+
ctx.feedback.resolve(prior.id, decision);
|
|
161
|
+
}
|
|
162
|
+
}
|
package/dist/scripts/sort.js
CHANGED
|
@@ -275,7 +275,6 @@ export function runSort(args = {}, io = defaultIO) {
|
|
|
275
275
|
// Exports (for testing) — keep main() private
|
|
276
276
|
// ---------------------------------------------------------------------------
|
|
277
277
|
|
|
278
|
-
export { parseArtefactsTable } from './lib/artefacts.js';
|
|
279
278
|
export { loadHistory } from './lib/history.js';
|
|
280
279
|
export { parseFrontmatter } from './lib/workfile.js';
|
|
281
280
|
export {
|
|
@@ -40,9 +40,9 @@ Appraise makes **no disk writes**. Feedback output flows through `foundry_feedba
|
|
|
40
40
|
> 3. Investigate and fix the root cause of the failure before restarting.
|
|
41
41
|
|
|
42
42
|
Then return control to the user and stop.
|
|
43
|
-
- `foundry_artefacts_list({
|
|
44
|
-
- For each
|
|
45
|
-
- `foundry_config_laws` with the
|
|
43
|
+
- `foundry_artefacts_list({})` — enumerate the current cycle's branch artefact changes as `[{ file, state }]` entries.
|
|
44
|
+
- For each artefact change, gather its type-specific context:
|
|
45
|
+
- `foundry_config_laws` with the cycle's output type — applicable laws (global + type-specific)
|
|
46
46
|
- `foundry_config_artefact_type` with the type ID — the artefact type definition
|
|
47
47
|
- `foundry_appraisers_select` with the type ID — selected appraiser personalities with their raw model IDs
|
|
48
48
|
|
|
@@ -23,7 +23,7 @@ Human-appraise runs inside an enforced stage. Your **first** and **last** tool c
|
|
|
23
23
|
|
|
24
24
|
Human-appraise makes **no disk writes**. All output flows through `foundry_feedback_add` and `foundry_feedback_resolve`. `foundry_stage_end` flags unexpected writes as a violation.
|
|
25
25
|
|
|
26
|
-
Human-appraise **cannot** call `foundry_feedback_action
|
|
26
|
+
Human-appraise **cannot** call `foundry_feedback_action` or `foundry_feedback_wontfix` — the tools reject those calls during a human-appraise stage (action/wontfix are forge-only forward transitions). See "Feedback handling" below for the legal transitions available to human-appraise.
|
|
27
27
|
|
|
28
28
|
## Input
|
|
29
29
|
|
|
@@ -54,7 +54,7 @@ Your LAST tool call must be `foundry_stage_end({summary: '<one-sentence descript
|
|
|
54
54
|
> 3. Investigate and fix the root cause of the failure before restarting.
|
|
55
55
|
|
|
56
56
|
Then call `foundry_stage_end({summary: 'Flow is failed; no human appraisal performed'})`, return control to the user, and stop.
|
|
57
|
-
- `foundry_artefacts_list({
|
|
57
|
+
- `foundry_artefacts_list({})` — this cycle's branch artefact changes as `[{ file, state }]` entries
|
|
58
58
|
- `foundry_feedback_list` — all existing feedback
|
|
59
59
|
- `foundry_history_list({cycle: <current-cycle>})` — what has happened so far
|
|
60
60
|
|
|
@@ -75,7 +75,7 @@ Your LAST tool call must be `foundry_stage_end({summary: '<one-sentence descript
|
|
|
75
75
|
- **Approve** — "looks good" / "continue" — no feedback added, sort will advance.
|
|
76
76
|
- **Provide feedback** — `foundry_feedback_add({ file, text, tag: 'human' })`. Sort will route back to forge.
|
|
77
77
|
- **Resolve feedback** — `foundry_feedback_resolve({ id, resolution, reason? })` for items in `{actioned, wont-fix, deadlocked}`. See "Feedback handling" below for the legal transitions and authority rules.
|
|
78
|
-
- **Abort** — human-appraise cannot directly mark the artefact `blocked` (the
|
|
78
|
+
- **Abort** — human-appraise cannot directly mark the artefact `blocked` (the repository no longer has a per-artefact status tool or table). To abort: end the stage with a summary explaining the abort, then either (a) instruct the user to call `foundry_workfile_delete({ confirm: true })` to discard the cycle, or (b) reject outstanding feedback so routing exhausts iterations and sort blocks the cycle on its own.
|
|
79
79
|
|
|
80
80
|
7. `foundry_stage_end({summary})` — describe what the human decided so sort can log it.
|
|
81
81
|
|
|
@@ -96,10 +96,9 @@ What human-appraise can NOT do:
|
|
|
96
96
|
`{actioned, wont-fix}` — that is forge's lane (spec §5.1 rule 1) and
|
|
97
97
|
the tools reject calls from any non-forge stage. If an open or rejected
|
|
98
98
|
item needs work, sort will route to forge after this stage ends.
|
|
99
|
-
- **No artefact status writes.**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
routing.
|
|
99
|
+
- **No artefact status writes.** The repository no longer has a per-artefact
|
|
100
|
+
status tool or table. Status is owned by the cycle state machine through
|
|
101
|
+
sort and orchestrate routing.
|
|
103
102
|
|
|
104
103
|
What human-appraise CAN do:
|
|
105
104
|
|
|
@@ -47,6 +47,34 @@ task tool:
|
|
|
47
47
|
|
|
48
48
|
When the task returns, call `foundry_orchestrate({lastResult: {ok: true}})`. If the task tool itself errored or reported a subagent crash, pass `{ok: false, error: '<message>'}`.
|
|
49
49
|
|
|
50
|
+
### `dispatch_multi`
|
|
51
|
+
|
|
52
|
+
Payload: `{stage, cycle, tasks}`.
|
|
53
|
+
|
|
54
|
+
Fire all tasks in parallel by making multiple `task` tool calls in a single response:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
task tool:
|
|
58
|
+
subagent_type: <task.subagent_type>
|
|
59
|
+
description: "Appraise <artefact> for <cycle>"
|
|
60
|
+
prompt: <task.prompt — pass verbatim>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Repeat for every entry in the `tasks` array. If `tasks` is empty, call
|
|
64
|
+
`foundry_orchestrate({lastResults: []})` directly — no dispatch needed.
|
|
65
|
+
|
|
66
|
+
When all tasks complete, collect their outputs into a `lastResults` array:
|
|
67
|
+
|
|
68
|
+
- Task succeeded: `{ok: true, output: "<subagent output text>"}`
|
|
69
|
+
- Task failed: `{ok: false, error: "<error message>"}`
|
|
70
|
+
|
|
71
|
+
Then call `foundry_orchestrate({lastResults})`.
|
|
72
|
+
|
|
73
|
+
Each appraiser sub-agent prompt already contains the appraiser personality,
|
|
74
|
+
artefact content, and applicable laws. Do NOT inject additional instructions.
|
|
75
|
+
Do NOT call `foundry_stage_begin` or `foundry_stage_end` — the appraise
|
|
76
|
+
module handles lifecycle internally.
|
|
77
|
+
|
|
50
78
|
### `human_appraise`
|
|
51
79
|
|
|
52
80
|
Payload: `{stage, token, context}`.
|
|
@@ -59,25 +87,24 @@ When it returns, call `foundry_orchestrate({lastResult: {ok: true}})`.
|
|
|
59
87
|
|
|
60
88
|
Payload: `{cycle, artefact_file, next_cycles}`.
|
|
61
89
|
|
|
62
|
-
1.
|
|
63
|
-
2.
|
|
64
|
-
3. Return control to the flow skill.
|
|
90
|
+
1. Report to the user: "Cycle `<cycle>` complete. Output: `<artefact_file>`. Next cycles available: `<next_cycles>`."
|
|
91
|
+
2. Return control to the flow skill.
|
|
65
92
|
|
|
66
93
|
### `blocked`
|
|
67
94
|
|
|
68
95
|
Payload: `{cycle, artefact_file, reason}`.
|
|
69
96
|
|
|
70
|
-
Report to the user: "Cycle `<cycle>` blocked on `<artefact_file>`: `<reason>`." Return control to the flow skill. The
|
|
97
|
+
Report to the user: "Cycle `<cycle>` blocked on `<artefact_file>`: `<reason>`." Return control to the flow skill. The cycle is blocked.
|
|
71
98
|
|
|
72
99
|
### `violation`
|
|
73
100
|
|
|
74
101
|
Payload: `{details, affected_files}`.
|
|
75
102
|
|
|
76
|
-
Report to the user: "Cycle halted (violation): `<details>`. Affected files: `<affected_files>`." Return control to the flow skill.
|
|
103
|
+
Report to the user: "Cycle halted (violation): `<details>`. Affected files: `<affected_files>`." Return control to the flow skill. The cycle is halted by the violation; no per-artefact status is written.
|
|
77
104
|
|
|
78
105
|
## What you do NOT do
|
|
79
106
|
|
|
80
|
-
- You do NOT inline forge
|
|
107
|
+
- You do NOT inline forge work. Always dispatch forge via `task`. Quench runs internally in the orchestrator. Appraise uses `dispatch_multi` for parallel subagent dispatch followed by internal consolidation.
|
|
81
108
|
- You do NOT mint, modify, or cache tokens. The `prompt` from orchestrate already contains the token verbatim.
|
|
82
109
|
- `foundry_history_append`, `foundry_git_commit`, `foundry_stage_finalize`, and `foundry_sort` are not registered tools; orchestrate handles them internally via the loop.
|
|
83
110
|
- You do NOT reorder the protocol. `foundry_orchestrate` returns, you act, you call back. Nothing else between.
|
|
@@ -39,14 +39,14 @@ Quench makes **no disk writes**. You produce feedback via `foundry_feedback_add`
|
|
|
39
39
|
> 3. Investigate and fix the root cause of the failure before restarting.
|
|
40
40
|
|
|
41
41
|
Then return control to the user and stop.
|
|
42
|
-
3. `foundry_artefacts_list({
|
|
43
|
-
4. For each
|
|
42
|
+
3. `foundry_artefacts_list({})` — enumerate the current cycle's branch artefact changes as `[{ file, state }]` entries.
|
|
43
|
+
4. For each artefact change:
|
|
44
44
|
a. `foundry_validate_run({ typeId: '<type-id>' })` — executes all law-based validators for the artefact type. The tool returns `{ ok, validatorsRun, items, errors }`. `items` is the array of parsed feedback items; each entry carries `lawId`, `validatorId`, `file`, and `text` (plus optional `location` and `severity`). `errors` carries validator-level failures with `lawId`, `validatorId`, `type` (`parse` or `pattern-mismatch`), and `message`.
|
|
45
45
|
b. For each entry in `items`: call `foundry_feedback_add` with `{ file: item.file, text: item.text, tag: 'law:' + item.lawId + ':' + item.validatorId }`. The tag uses the law ID and validator ID returned by the tool so operators reading `WORK.feedback.yaml` can identify exactly which validator produced each item.
|
|
46
46
|
c. If `errors` is non-empty, the validators themselves misbehaved (malformed JSONL or files outside the artefact type's `file-patterns`). Report these to the user via `foundry_stage_end` summary; do not convert them to law-tagged feedback.
|
|
47
47
|
5. Call `foundry_feedback_list`. For items whose `source` matches your stage id and whose state is `actioned` or `wont-fix`, use the validation results from step 4 to resolve them by id: approve when the relevant validation now passes or the deterministic issue is gone; reject with a reason when it still fails.
|
|
48
|
-
6. If every command passes for every
|
|
49
|
-
7. If the artefact
|
|
48
|
+
6. If every command passes for every artefact change, add no new feedback.
|
|
49
|
+
7. If the artefact list is empty, `foundry_stage_end({summary: 'SKIP: no files'})` and stop.
|
|
50
50
|
8. `foundry_stage_end({summary})`.
|
|
51
51
|
|
|
52
52
|
## Feedback handling
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@really-knows-ai/foundry",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"description": "A skill-driven framework for governed artefact generation with AI coding tools. Define your own artefact types, laws, and flows — Foundry handles the forge → quench → appraise pipeline with deterministic routing, quality gates, and iterative refinement.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/.opencode/plugins/foundry.js",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "node scripts/build.js",
|
|
54
|
-
"test": "find tests -name '*.test.js' ! -name '*.integration.test.js' ! -name '*.e2e.test.js' -print0 | xargs -0 node --test --test-reporter=dot",
|
|
54
|
+
"test": "find tests -name '*.test.js' ! -name '*.integration.test.js' ! -name '*.e2e.test.js' -print0 | xargs -0 node --test --experimental-test-module-mocks --test-reporter=dot",
|
|
55
55
|
"test:unit": "pnpm run test",
|
|
56
|
-
"test:integration": "find tests -name '*.integration.test.js' -print0 | xargs -0 node --test --test-reporter=dot",
|
|
57
|
-
"test:e2e": "find tests -name '*.e2e.test.js' -print0 | xargs -0 node --test --test-reporter=dot",
|
|
58
|
-
"test:all": "node --test --test-reporter=dot",
|
|
56
|
+
"test:integration": "find tests -name '*.integration.test.js' -print0 | xargs -0 node --test --experimental-test-module-mocks --test-reporter=dot",
|
|
57
|
+
"test:e2e": "find tests -name '*.e2e.test.js' -print0 | xargs -0 node --test --experimental-test-module-mocks --test-reporter=dot",
|
|
58
|
+
"test:all": "node --test --experimental-test-module-mocks --test-reporter=dot",
|
|
59
59
|
"test:coverage": "node --test --experimental-test-coverage --test-reporter=dot",
|
|
60
60
|
"lint": "eslint src/ tests/ scripts/",
|
|
61
61
|
"build:full": "pnpm run lint --fix && pnpm run test:all && pnpm run build",
|