@really-knows-ai/foundry 3.5.8 → 3.6.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/README.md +16 -10
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +2 -3
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +9 -5
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +3 -1
- package/dist/CHANGELOG.md +38 -0
- package/dist/README.md +16 -10
- package/dist/docs/README.md +6 -6
- package/dist/docs/architecture.md +59 -19
- package/dist/docs/concepts.md +55 -19
- package/dist/docs/getting-started.md +37 -15
- package/dist/docs/memory-maintenance.md +3 -3
- package/dist/docs/tools.md +131 -70
- package/dist/docs/work-spec.md +38 -52
- package/dist/scripts/appraise-module.js +69 -7
- package/dist/scripts/lib/artefacts.js +43 -1
- package/dist/scripts/lib/config-creators/cycle.js +6 -10
- package/dist/scripts/lib/config-validators/cycle.js +1 -9
- package/dist/scripts/lib/feedback-store.js +26 -51
- package/dist/scripts/lib/finalize.js +10 -2
- package/dist/scripts/lib/forge-contract.js +93 -0
- package/dist/scripts/lib/history.js +2 -1
- package/dist/scripts/lib/sort-reason.js +11 -8
- package/dist/scripts/lib/sort-routing.js +185 -63
- package/dist/scripts/lib/workfile.js +28 -0
- package/dist/scripts/orchestrate-cycle.js +3 -13
- package/dist/scripts/orchestrate-phases.js +51 -45
- package/dist/scripts/orchestrate-terminals.js +37 -2
- package/dist/scripts/orchestrate.js +62 -5
- package/dist/scripts/quench-module.js +54 -12
- package/dist/scripts/sort.js +42 -62
- package/dist/skills/add-cycle/SKILL.md +4 -4
- package/dist/skills/add-flow/SKILL.md +1 -1
- package/dist/skills/human-appraise/SKILL.md +12 -40
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getCycleDefinition } from './lib/config.js';
|
|
2
|
-
import { getArtefactFiles } from './lib/artefacts.js';
|
|
2
|
+
import { getArtefactFiles, computeArtefactVersion } from './lib/artefacts.js';
|
|
3
3
|
import { readCycleTargets, readRecentFeedback, violation } from './orchestrate-cycle.js';
|
|
4
|
+
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
5
|
+
import { baseStage } from './lib/sort-routing.js';
|
|
4
6
|
|
|
5
7
|
async function findOutputArtefacts(cfm, io, foundryDir, baseBranch) {
|
|
6
8
|
const outputType = cfm ? cfm['output-type'] : undefined;
|
|
@@ -29,15 +31,48 @@ export async function blockedAction(cycleId, io, details, foundryDir, baseBranch
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export async function humanAppraiseAction(route, token, ctx) {
|
|
32
|
-
const { cycleId, io, baseBranch } = ctx;
|
|
34
|
+
const { cycleId, io, baseBranch, cwd } = ctx;
|
|
33
35
|
const fd = ctx.foundryDir || 'foundry';
|
|
34
36
|
const base = baseBranch || 'main';
|
|
35
37
|
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await resolveStaleHumanAppraiseFeedback(cfm, fd, io, cycleId, cwd);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return { action: 'violation', details: `version check failed: ${err.message}`, recoverable: false, affected_files: [] };
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
37
46
|
const artefactFile = artefact ? artefact.file : null;
|
|
38
47
|
return { action: 'human_appraise', stage: route, token, context: { cycle: cycleId, artefact_file: artefactFile, recent_feedback: readRecentFeedback(io) } };
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Resolve stale human-appraise feedback. Errors propagate to the caller
|
|
52
|
+
* (humanAppraiseAction) which surfaces them as a violation.
|
|
53
|
+
*/
|
|
54
|
+
async function resolveStaleHumanAppraiseFeedback(cfm, fd, io, cycleId, cwd) {
|
|
55
|
+
const outputType = cfm['output-type'];
|
|
56
|
+
if (!outputType) return;
|
|
57
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
58
|
+
const currentVersion = await computeArtefactVersion(fd, outputType, io, cwd);
|
|
59
|
+
for (const item of store.list()) {
|
|
60
|
+
if (shouldSkipHumanAppraiseResolve(item, currentVersion)) continue;
|
|
61
|
+
store.autoResolve({
|
|
62
|
+
id: item.id,
|
|
63
|
+
reason: `superseded by forge revision ${currentVersion}`,
|
|
64
|
+
cycle: cycleId,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shouldSkipHumanAppraiseResolve(item, currentVersion) {
|
|
70
|
+
if (item.history[0].state === 'resolved') return true;
|
|
71
|
+
if (baseStage(item.source) !== 'human-appraise') return true;
|
|
72
|
+
if (item.artefact_version === currentVersion) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
41
76
|
export async function missingModelViolation(cycleId, route, io, foundryDir, baseBranch) {
|
|
42
77
|
const fd = foundryDir || 'foundry';
|
|
43
78
|
const base = baseBranch || 'main';
|
|
@@ -8,6 +8,9 @@ import matter from 'gray-matter';
|
|
|
8
8
|
import { readActiveStage, readLastStage, writeActiveStage, clearActiveStage } from './lib/state.js';
|
|
9
9
|
import { stageBaseOf } from './lib/stage-guard.js';
|
|
10
10
|
import { ulid as defaultUlid } from './lib/ulid.js';
|
|
11
|
+
import { computeArtefactVersion } from './lib/artefacts.js';
|
|
12
|
+
import { enforceForgeContract } from './lib/forge-contract.js';
|
|
13
|
+
import { loadHistory } from './lib/history.js';
|
|
11
14
|
import {
|
|
12
15
|
readCycleTargets,
|
|
13
16
|
readForgeFilePatterns,
|
|
@@ -138,7 +141,10 @@ function buildFeedback(cycleId, stageId, io) {
|
|
|
138
141
|
return {
|
|
139
142
|
add: (item) => {
|
|
140
143
|
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
141
|
-
return store.add({
|
|
144
|
+
return store.add({
|
|
145
|
+
file: item.file, tag: item.tag, text: item.text, source: stageId, cycle: cycleId,
|
|
146
|
+
artefact_version: item.artefact_version,
|
|
147
|
+
});
|
|
142
148
|
},
|
|
143
149
|
list: (query) => {
|
|
144
150
|
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
@@ -159,7 +165,7 @@ function buildQuenchContext(cycleId, args, io) {
|
|
|
159
165
|
const stageId = `quench:${cycleId}`;
|
|
160
166
|
return { cycleId, stageId, io, git: args.git, finalize: buildFinalizeWrapper(cycleId, args, io),
|
|
161
167
|
now: args.now, ulid: args.ulid, mint: args.mint, foundryDir: 'foundry', defaultModel: args.defaultModel,
|
|
162
|
-
baseBranch: args.baseBranch ?? 'main',
|
|
168
|
+
baseBranch: args.baseBranch ?? 'main', cwd: args.cwd ?? process.cwd(),
|
|
163
169
|
feedback: buildFeedback(cycleId, stageId, io) };
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -167,7 +173,7 @@ function buildAppraiseCtx(cycleId, args, io) {
|
|
|
167
173
|
const stageId = `appraise:${cycleId}`;
|
|
168
174
|
return { cycleId, io, git: args.git, finalize: buildFinalizeWrapper(cycleId, args, io),
|
|
169
175
|
foundryDir: 'foundry', defaultModel: args.defaultModel,
|
|
170
|
-
baseBranch: args.baseBranch ?? 'main',
|
|
176
|
+
baseBranch: args.baseBranch ?? 'main', cwd: args.cwd ?? process.cwd(),
|
|
171
177
|
activeStage: readActiveStage(io), lastStage: readLastStage(io),
|
|
172
178
|
feedback: buildFeedback(cycleId, stageId, io) };
|
|
173
179
|
}
|
|
@@ -184,6 +190,42 @@ function writeStageRecord(io, cycleId, route) {
|
|
|
184
190
|
writeActiveStage(io, { cycle: cycleId, stage: `${route}`, token: null, baseSha: resolveBaseSha(io) });
|
|
185
191
|
}
|
|
186
192
|
|
|
193
|
+
const FORGE_CTX = '.foundry/forge-context.json';
|
|
194
|
+
|
|
195
|
+
async function captureForgeContext(sortResult, args, preCheck, io) {
|
|
196
|
+
const fgResult = await readForgeFilePatterns(preCheck.cycleId, io);
|
|
197
|
+
if (!fgResult) return;
|
|
198
|
+
const preVersion = await computeArtefactVersion('foundry', fgResult.outputType, io, args.cwd);
|
|
199
|
+
const items = openFeedbackStore('WORK.feedback.yaml', io).list();
|
|
200
|
+
if (!io.exists('.foundry')) io.mkdir('.foundry');
|
|
201
|
+
io.writeFile(FORGE_CTX, JSON.stringify({ forgePreVersion: preVersion, forgeItems: items.map(i => ({ id: i.id })) }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function countConsecutiveForgeFailures(io, cycleId) {
|
|
205
|
+
if (!io.exists('WORK.history.yaml')) return 0;
|
|
206
|
+
const entries = loadHistory('WORK.history.yaml', cycleId, io);
|
|
207
|
+
let count = 0;
|
|
208
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
209
|
+
if (stageBaseOf(entries[i].stage) !== 'forge') break;
|
|
210
|
+
if (entries[i].contract_passed === false) count++;
|
|
211
|
+
else break;
|
|
212
|
+
}
|
|
213
|
+
return count;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function enforceForgeStage(activeStage, fgResult, cycleId, io, cwd) {
|
|
217
|
+
const postVersion = await computeArtefactVersion('foundry', fgResult.outputType, io, cwd);
|
|
218
|
+
const feedbackStore = openFeedbackStore('WORK.feedback.yaml', io);
|
|
219
|
+
const { contractPassed } = enforceForgeContract({
|
|
220
|
+
items: activeStage.forgeItems, preVersion: activeStage.forgePreVersion,
|
|
221
|
+
postVersion, feedbackStore, cycleId,
|
|
222
|
+
});
|
|
223
|
+
if (!contractPassed && countConsecutiveForgeFailures(io, cycleId) + 1 >= 3) {
|
|
224
|
+
return { violation: 'forge contract failed 3 consecutive times — unable to satisfy feedback requirements' };
|
|
225
|
+
}
|
|
226
|
+
return { postVersion, contractPassed };
|
|
227
|
+
}
|
|
228
|
+
|
|
187
229
|
async function handleQuenchRoute(sortResult, preCheck, args, io) {
|
|
188
230
|
writeStageRecord(io, preCheck.cycleId, sortResult.route);
|
|
189
231
|
const quenchCtx = buildQuenchContext(preCheck.cycleId, args, io);
|
|
@@ -230,6 +272,7 @@ async function dispatchByRoute(sortResult, args, preCheck, io) {
|
|
|
230
272
|
? handleAppraiseConsolidateRoute(sortResult, preCheck, args, io)
|
|
231
273
|
: handleAppraiseGatherRoute(sortResult, preCheck, args, io);
|
|
232
274
|
}
|
|
275
|
+
if (base === 'forge') await captureForgeContext(sortResult, args, preCheck, io);
|
|
233
276
|
return handleSortResult(sortResult, buildSortContext(preCheck.cycleId, args, io));
|
|
234
277
|
}
|
|
235
278
|
|
|
@@ -278,12 +321,26 @@ async function runSetupIfNeeded(preCheck, args, io) {
|
|
|
278
321
|
return err || setupWorkfile(buildSetupArgs(preCheck, args, io));
|
|
279
322
|
}
|
|
280
323
|
|
|
324
|
+
async function runForgePostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
325
|
+
const fgResult = await readForgeFilePatterns(cycleId, io);
|
|
326
|
+
const base = { lastStage, activeStage, cycleId, io, finalize: args.finalize, git: args.git };
|
|
327
|
+
if (!fgResult) return finaliseStage(base);
|
|
328
|
+
if (!io.exists(FORGE_CTX)) return finaliseStage(base);
|
|
329
|
+
const forgeCtx = JSON.parse(io.readFile(FORGE_CTX));
|
|
330
|
+
io.unlink(FORGE_CTX);
|
|
331
|
+
if (!forgeCtx.forgePreVersion) return finaliseStage(base);
|
|
332
|
+
const result = await enforceForgeStage(forgeCtx, fgResult, cycleId, io, args.cwd);
|
|
333
|
+
if (result.violation) return violation(result.violation, []);
|
|
334
|
+
return finaliseStage({ ...base, postVersion: result.postVersion, contractPassed: result.contractPassed });
|
|
335
|
+
}
|
|
336
|
+
|
|
281
337
|
async function runPostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
282
338
|
if (!args.lastResult) return null;
|
|
283
339
|
if (args.lastResult.ok === false) {
|
|
284
340
|
return handleViolation({ lastResult: args.lastResult, activeStage, lastStage, cycleId, io });
|
|
285
341
|
}
|
|
286
342
|
const stageErr = guardMissingLastStage(lastStage);
|
|
287
|
-
|
|
288
|
-
return
|
|
343
|
+
if (stageErr) return stageErr;
|
|
344
|
+
if (stageBaseOf(lastStage.stage) === 'forge') return runForgePostDispatch(args, activeStage, lastStage, cycleId, io);
|
|
345
|
+
return finaliseStage({ lastStage, activeStage, cycleId, io, finalize: args.finalize, git: args.git });
|
|
289
346
|
}
|
|
@@ -8,9 +8,46 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { readActiveStage } from './lib/state.js';
|
|
11
|
-
import { getArtefactFiles } from './lib/artefacts.js';
|
|
11
|
+
import { getArtefactFiles, computeArtefactVersion } from './lib/artefacts.js';
|
|
12
12
|
import { getCycleDefinition } from './lib/config.js';
|
|
13
13
|
import { performValidation } from './lib/validation.js';
|
|
14
|
+
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve stale feedback items whose artefact version does not match the
|
|
18
|
+
* current on-disk version. Items from this stage's source base with a
|
|
19
|
+
* mismatched artefact_version are auto-resolved as superseded.
|
|
20
|
+
*/
|
|
21
|
+
export async function resolveStaleFeedback(items, currentVersion, stageBase, feedback, cycle) {
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
if (shouldSkipStaleResolve(item, currentVersion, stageBase)) continue;
|
|
24
|
+
const reason = `superseded by forge revision ${currentVersion}`;
|
|
25
|
+
feedback.autoResolve({ id: item.id, reason, cycle });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function shouldSkipStaleResolve(item, currentVersion, stageBase) {
|
|
30
|
+
if (item.history[0].state === 'resolved') return true;
|
|
31
|
+
const itemBase = typeof item.source === 'string' ? item.source.split(':')[0] : '';
|
|
32
|
+
if (itemBase !== stageBase) return true;
|
|
33
|
+
if (item.artefact_version === currentVersion) return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve stale quench-sourced feedback. Errors propagate to the caller
|
|
39
|
+
* (runQuench) which surfaces them as a failure.
|
|
40
|
+
*/
|
|
41
|
+
async function resolveStaleQuenchFeedback(ctx, outputType) {
|
|
42
|
+
try {
|
|
43
|
+
const store = openFeedbackStore('WORK.feedback.yaml', ctx.io);
|
|
44
|
+
const currentVersion = await computeArtefactVersion(ctx.foundryDir, outputType, ctx.io, ctx.cwd);
|
|
45
|
+
resolveStaleFeedback(store.list(), currentVersion, 'quench', store, ctx.cycleId);
|
|
46
|
+
} catch {
|
|
47
|
+
// Graceful degrade — stale resolution is best-effort.
|
|
48
|
+
// The orchestrator handles IO failures at the cycle level.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
14
51
|
|
|
15
52
|
/**
|
|
16
53
|
* Run quench (deterministic validation) for a cycle.
|
|
@@ -30,15 +67,20 @@ export async function runQuench(ctx) {
|
|
|
30
67
|
return { ok: false, error: `Cycle ${ctx.cycleId} has no output-type` };
|
|
31
68
|
}
|
|
32
69
|
|
|
70
|
+
return await runQuenchWithStale(ctx, activeStageRecord, outputType);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function runQuenchWithStale(ctx, activeStageRecord, outputType) {
|
|
74
|
+
await resolveStaleQuenchFeedback(ctx, outputType);
|
|
75
|
+
const artefactVersion = await computeArtefactVersion(
|
|
76
|
+
ctx.foundryDir, outputType, ctx.io, ctx.cwd,
|
|
77
|
+
).catch(() => undefined);
|
|
33
78
|
const discovery = await discoverArtefacts(ctx, outputType);
|
|
34
79
|
if (!discovery.ok) return discovery;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (artefacts.length === 0) {
|
|
80
|
+
if (discovery.artefacts.length === 0) {
|
|
38
81
|
return await handleNoArtefacts(ctx, activeStageRecord);
|
|
39
82
|
}
|
|
40
|
-
|
|
41
|
-
return await processArtefacts(ctx, artefacts, activeStageRecord, outputType);
|
|
83
|
+
return await processArtefacts(ctx, discovery.artefacts, activeStageRecord, outputType, artefactVersion);
|
|
42
84
|
}
|
|
43
85
|
|
|
44
86
|
async function discoverArtefacts(ctx, outputType) {
|
|
@@ -65,7 +107,7 @@ async function handleNoArtefacts(ctx, activeStageRecord) {
|
|
|
65
107
|
/**
|
|
66
108
|
* Process each artefact: run validation, post feedback, handle errors.
|
|
67
109
|
*/
|
|
68
|
-
async function processArtefacts(ctx, artefacts, activeStageRecord, outputType) {
|
|
110
|
+
async function processArtefacts(ctx, artefacts, activeStageRecord, outputType, artefactVersion) {
|
|
69
111
|
const perArtefact = [];
|
|
70
112
|
const currentFeedback = [];
|
|
71
113
|
let allOk = true;
|
|
@@ -78,7 +120,7 @@ async function processArtefacts(ctx, artefacts, activeStageRecord, outputType) {
|
|
|
78
120
|
artefacts: [artefact],
|
|
79
121
|
});
|
|
80
122
|
|
|
81
|
-
const outcome = handleArtefactResult(ctx, artefact, result, currentFeedback);
|
|
123
|
+
const outcome = handleArtefactResult(ctx, artefact, result, currentFeedback, artefactVersion);
|
|
82
124
|
perArtefact.push(outcome.text);
|
|
83
125
|
if (!outcome.ok) allOk = false;
|
|
84
126
|
}
|
|
@@ -103,7 +145,7 @@ async function processArtefacts(ctx, artefacts, activeStageRecord, outputType) {
|
|
|
103
145
|
* Returns { ok, text } where `ok` indicates whether the artefact passed
|
|
104
146
|
* validation and `text` is the per-artefact summary line.
|
|
105
147
|
*/
|
|
106
|
-
function handleArtefactResult(ctx, artefact, result, currentFeedback) {
|
|
148
|
+
function handleArtefactResult(ctx, artefact, result, currentFeedback, artefactVersion) {
|
|
107
149
|
if (isNoValidators(result)) {
|
|
108
150
|
return { ok: true, text: `${artefact.file}: OK: no validators` };
|
|
109
151
|
}
|
|
@@ -117,7 +159,7 @@ function handleArtefactResult(ctx, artefact, result, currentFeedback) {
|
|
|
117
159
|
return { ok: false, text: `${artefact.file}: ${messages}` };
|
|
118
160
|
}
|
|
119
161
|
|
|
120
|
-
postFeedbackItems(ctx, artefact, result, currentFeedback);
|
|
162
|
+
postFeedbackItems(ctx, artefact, result, currentFeedback, artefactVersion);
|
|
121
163
|
return { ok: true, text: `${artefact.file}: ${result.items.length} issues found` };
|
|
122
164
|
}
|
|
123
165
|
|
|
@@ -138,10 +180,10 @@ function isAllErrors(result) {
|
|
|
138
180
|
/**
|
|
139
181
|
* Post feedback items for validation results and track for resolution.
|
|
140
182
|
*/
|
|
141
|
-
function postFeedbackItems(ctx, artefact, result, currentFeedback) {
|
|
183
|
+
function postFeedbackItems(ctx, artefact, result, currentFeedback, artefactVersion) {
|
|
142
184
|
for (const item of result.items) {
|
|
143
185
|
const tag = `law:${item.lawId}:${item.validatorId}`;
|
|
144
|
-
ctx.feedback.add({ file: artefact.file, text: item.text, tag });
|
|
186
|
+
ctx.feedback.add({ file: artefact.file, text: item.text, tag, artefact_version: artefactVersion });
|
|
145
187
|
currentFeedback.push({ file: artefact.file, tag });
|
|
146
188
|
}
|
|
147
189
|
}
|
package/dist/scripts/sort.js
CHANGED
|
@@ -29,38 +29,6 @@ import {
|
|
|
29
29
|
getDirtyToolManagedFiles,
|
|
30
30
|
} from './lib/sort-fs-check.js';
|
|
31
31
|
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Top-level deadlock pass (spec §6.1)
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Walk the feedback store and write a `state=deadlocked` snapshot for every
|
|
38
|
-
* non-resolved item whose history depth has reached the configured threshold.
|
|
39
|
-
* One atomic batch write via `store.writeDeadlockedSnapshots(ids, ...)`.
|
|
40
|
-
*
|
|
41
|
-
* Sort is the only writer of `state=deadlocked` per spec §6.1.
|
|
42
|
-
*
|
|
43
|
-
* @returns {boolean} true iff at least one snapshot was written.
|
|
44
|
-
*/
|
|
45
|
-
function runDeadlockPass(store, { threshold, enabled, cycle }) {
|
|
46
|
-
if (!enabled) return false;
|
|
47
|
-
const qualifying = store.list().filter(item => {
|
|
48
|
-
// history[0] is the most recent state per the feedback-store invariant
|
|
49
|
-
// (entries are prepended to keep newest at head).
|
|
50
|
-
const head = item.history[0];
|
|
51
|
-
if (head.state === 'resolved' || head.state === 'deadlocked') return false;
|
|
52
|
-
return item.history.length >= threshold;
|
|
53
|
-
});
|
|
54
|
-
if (qualifying.length === 0) return false;
|
|
55
|
-
store.writeDeadlockedSnapshots(
|
|
56
|
-
qualifying.map(it => it.id),
|
|
57
|
-
`depth >= threshold=${threshold}`,
|
|
58
|
-
'sort',
|
|
59
|
-
cycle,
|
|
60
|
-
);
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
32
|
// ---------------------------------------------------------------------------
|
|
65
33
|
// runSort — structured result for programmatic use
|
|
66
34
|
// ---------------------------------------------------------------------------
|
|
@@ -85,13 +53,12 @@ function validateWorkMd(workPath, io) {
|
|
|
85
53
|
return { frontmatter, cycle: frontmatter.cycle, stages: frontmatter.stages };
|
|
86
54
|
}
|
|
87
55
|
|
|
88
|
-
function extractFrontmatterDefaults(frontmatter) {
|
|
56
|
+
export function extractFrontmatterDefaults(frontmatter) {
|
|
89
57
|
const maxIt = frontmatter['max-iterations'] ?? 3;
|
|
90
58
|
return {
|
|
91
59
|
maxIterations: maxIt,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
deadlockIterations: frontmatter['deadlock-iterations'] ?? maxIt,
|
|
60
|
+
alwaysHumanAppraise: frontmatter['always-human-appraise'] === true,
|
|
61
|
+
deadlockHumanAppraise: frontmatter['deadlock-human-appraise'] === true,
|
|
95
62
|
};
|
|
96
63
|
}
|
|
97
64
|
|
|
@@ -105,17 +72,33 @@ function checkDirtyFiles(history, io) {
|
|
|
105
72
|
+ `Re-run foundry_orchestrate or commit the listed files manually before retrying.`;
|
|
106
73
|
}
|
|
107
74
|
|
|
108
|
-
|
|
75
|
+
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
76
|
+
|
|
77
|
+
function isLegacyItem(item) {
|
|
78
|
+
return !item.artefact_version || !SHA256_RE.test(item.artefact_version);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadFeedback(io, cycle) {
|
|
109
82
|
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
83
|
+
const allItems = store.list();
|
|
84
|
+
const routed = [];
|
|
85
|
+
|
|
86
|
+
for (const item of allItems) {
|
|
87
|
+
if (isLegacyItem(item)) {
|
|
88
|
+
store.autoResolve({ id: item.id, reason: 'superseded (legacy, no artefact version)', cycle });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
routed.push({
|
|
92
|
+
id: item.id,
|
|
93
|
+
file: item.file,
|
|
94
|
+
state: item.history[0].state,
|
|
95
|
+
depth: item.history.length,
|
|
96
|
+
source: item.source,
|
|
97
|
+
artefact_version: item.artefact_version,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return routed;
|
|
119
102
|
}
|
|
120
103
|
|
|
121
104
|
function resolveCycleDef(cycleDef, frontmatter, foundryDir, cycle) {
|
|
@@ -138,15 +121,15 @@ function getCurrentNonSortStage(nonSortHistory) {
|
|
|
138
121
|
return nonSortHistory.length > 0 ? nonSortHistory[nonSortHistory.length - 1].stage : null;
|
|
139
122
|
}
|
|
140
123
|
|
|
141
|
-
function resolveDeadlockRoute(stages, nonSortHistory, cycle) {
|
|
142
|
-
const currentNonSort = getCurrentNonSortStage(nonSortHistory);
|
|
143
|
-
if (currentNonSort && baseStage(currentNonSort) === 'human-appraise') return 'blocked';
|
|
144
|
-
return findFirst(stages, 'human-appraise') || `human-appraise:${cycle}`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
124
|
function resolveRoute(ctx) {
|
|
148
|
-
|
|
149
|
-
|
|
125
|
+
return determineRoute(
|
|
126
|
+
ctx.stages, ctx.history, ctx.feedback, ctx.maxIterations,
|
|
127
|
+
{
|
|
128
|
+
alwaysHumanAppraise: ctx.alwaysHumanAppraise,
|
|
129
|
+
deadlockHumanAppraise: ctx.deadlockHumanAppraise,
|
|
130
|
+
cycle: ctx.cycle,
|
|
131
|
+
},
|
|
132
|
+
);
|
|
150
133
|
}
|
|
151
134
|
|
|
152
135
|
function firstModelValue(models) {
|
|
@@ -215,9 +198,7 @@ function preparePhases({ workPath, historyPath, foundryDir, cycleDef, io }) {
|
|
|
215
198
|
const history = loadHistory(historyPath, cycle, io);
|
|
216
199
|
const dirtyError = checkDirtyFiles(history, io);
|
|
217
200
|
if (dirtyError) return { kind: 'violation', details: dirtyError };
|
|
218
|
-
const
|
|
219
|
-
cycle, defaults.deadlockIterations, defaults.deadlockAppraise, io,
|
|
220
|
-
);
|
|
201
|
+
const feedback = loadFeedback(io, cycle);
|
|
221
202
|
const fileCheck = checkModifiedFilesAfterLastStage({
|
|
222
203
|
history, foundryDir, cycleDef, cycle, frontmatter, io,
|
|
223
204
|
});
|
|
@@ -225,7 +206,7 @@ function preparePhases({ workPath, historyPath, foundryDir, cycleDef, io }) {
|
|
|
225
206
|
if (violation) return { kind: 'violation', details: violation };
|
|
226
207
|
return {
|
|
227
208
|
kind: 'ok',
|
|
228
|
-
frontmatter, cycle, stages, defaults, history, feedback,
|
|
209
|
+
frontmatter, cycle, stages, defaults, history, feedback,
|
|
229
210
|
nonSortHistory: fileCheck.nonSortHistory,
|
|
230
211
|
};
|
|
231
212
|
}
|
|
@@ -253,8 +234,9 @@ function buildRouteCtx(prep) {
|
|
|
253
234
|
history: prep.history,
|
|
254
235
|
feedback: prep.feedback,
|
|
255
236
|
maxIterations: prep.defaults.maxIterations,
|
|
237
|
+
alwaysHumanAppraise: prep.defaults.alwaysHumanAppraise,
|
|
238
|
+
deadlockHumanAppraise: prep.defaults.deadlockHumanAppraise,
|
|
256
239
|
cycle: prep.cycle,
|
|
257
|
-
anyDeadlocked: prep.anyDeadlocked,
|
|
258
240
|
nonSortHistory: prep.nonSortHistory,
|
|
259
241
|
};
|
|
260
242
|
}
|
|
@@ -283,10 +265,8 @@ export { parseFrontmatter } from './lib/workfile.js';
|
|
|
283
265
|
export {
|
|
284
266
|
baseStage,
|
|
285
267
|
findFirst,
|
|
286
|
-
|
|
268
|
+
nextStageInChain,
|
|
287
269
|
determineRoute,
|
|
288
|
-
nextAfterQuench,
|
|
289
|
-
nextAfterAppraise,
|
|
290
270
|
} from './lib/sort-routing.js';
|
|
291
271
|
export {
|
|
292
272
|
globMatch,
|
|
@@ -38,7 +38,7 @@ Do not tell the user to call branch tools directly.
|
|
|
38
38
|
|
|
39
39
|
When invoked with pre-filled fields matching the `foundry_config_create_cycle` tool args, skip questions for provided fields. Missing fields trigger clarifying questions.
|
|
40
40
|
|
|
41
|
-
Context fields: `{id, name, outputType, description, inputs?, targets?,
|
|
41
|
+
Context fields: `{id, name, outputType, description, inputs?, targets?, alwaysHumanAppraise?, deadlockHumanAppraise?, maxIterations?, assay?, memory?, models?}`
|
|
42
42
|
|
|
43
43
|
`inputs` is optional. A source cycle that starts from the user's run goal and has no upstream artefact dependency omits `inputs` entirely. Empty input contracts are invalid: do not pass `inputs: {type: "any-of", artefacts: []}`.
|
|
44
44
|
|
|
@@ -85,12 +85,12 @@ If the parent flow or required artefact type is missing and the user's goal clea
|
|
|
85
85
|
**Optional clusters** — After each cluster, ask whether the user wants to configure it; if not, skip:
|
|
86
86
|
|
|
87
87
|
- **Routing**: `inputs` (input contract: `{type: "any-of"|"all-of", artefacts: string[]}`; omit for source cycles with no upstream artefact dependency), `targets` (cycle IDs to route to after completion), `maxIterations` (maximum iterations before forced progression)
|
|
88
|
-
- **Human-appraise**: `
|
|
88
|
+
- **Human-appraise**: `alwaysHumanAppraise` (boolean, default false) — human reviews every iteration; when true, `max-iterations` is not enforced. `deadlockHumanAppraise` (boolean, default true) — route to human for review when the iteration cap is reached, instead of blocking the cycle. Only applies when `alwaysHumanAppraise` is false.
|
|
89
89
|
- **Memory and models**: `assay` (assay configuration), `memory` (memory configuration), `models` (stage-specific model overrides, e.g. `{forge: "openai/gpt-4o", appraise: "openai/gpt-4o"}`). For models, offer each stage (forge, quench, appraise) individually. If the user has no preference, omit the `models` map and use the session defaults.
|
|
90
90
|
|
|
91
91
|
### 2. Plan
|
|
92
92
|
|
|
93
|
-
Present a structured summary of the cycle definition: id, name, outputType, description, and any configured optional fields (inputs, targets,
|
|
93
|
+
Present a structured summary of the cycle definition: id, name, outputType, description, and any configured optional fields (inputs, targets, alwaysHumanAppraise, deadlockHumanAppraise, maxIterations, assay, memory, models). Include only fields that have values.
|
|
94
94
|
|
|
95
95
|
Ask: "Does this capture the cycle correctly?" Iterate until the user is satisfied.
|
|
96
96
|
|
|
@@ -102,7 +102,7 @@ Ask: "Proceed with this plan?" — wait for user answer before building. If the
|
|
|
102
102
|
|
|
103
103
|
1. **Validate**: Call `foundry_config_validate_cycle({ name: "<id>", body: "<assembled markdown>" })`. Assemble the body from the fields using the frontmatter format the tool produces internally. If the result is `{ ok: false, errors: [...] }`, address each error and re-run until `{ ok: true }`. Common issues: missing required frontmatter keys, references to artefact types or flows that do not exist yet.
|
|
104
104
|
|
|
105
|
-
2. **Create**: Call `foundry_config_create_cycle({ id: "<id>", name: "<name>", outputType: "<type>", description: "<description>", targets: ...,
|
|
105
|
+
2. **Create**: Call `foundry_config_create_cycle({ id: "<id>", name: "<name>", outputType: "<type>", description: "<description>", targets: ..., alwaysHumanAppraise: ..., deadlockHumanAppraise: ..., maxIterations: ..., assay: ..., memory: ..., models: ... })`. Include `inputs` only when the cycle reads upstream artefacts, and include `models` whenever the user selected stage-specific model overrides. The tool:
|
|
106
106
|
- re-validates the body (TOCTOU);
|
|
107
107
|
- writes `foundry/cycles/<id>.md`;
|
|
108
108
|
- produces one git commit on the current `config/*` branch.
|
|
@@ -69,7 +69,7 @@ Create missing dependencies in validation order:
|
|
|
69
69
|
|
|
70
70
|
3. **Appraisers** (may reference models): For each new appraiser, gather `id`, `name`, `description`, and optional `model` preference. Context object: `{id, name, description, model?}`.
|
|
71
71
|
|
|
72
|
-
4. **Cycles** (reference artefact types, laws, appraisers): For each new cycle, gather `id`, `name`, `outputType`, `description`, and any optional settings (inputs, targets, appraise, assay, memory, models). Context object: `{id, name, outputType, description, inputs?, targets?,
|
|
72
|
+
4. **Cycles** (reference artefact types, laws, appraisers): For each new cycle, gather `id`, `name`, `outputType`, `description`, and any optional settings (inputs, targets, appraise, assay, memory, models). Context object: `{id, name, outputType, description, inputs?, targets?, alwaysHumanAppraise?, deadlockHumanAppraise?, maxIterations?, assay?, memory?, models?}`. For a source cycle that starts from the user's run goal and has no upstream artefact dependency, omit `inputs` entirely; never pass `inputs` with an empty `artefacts` array.
|
|
73
73
|
|
|
74
74
|
For the haiku example, default to a `haiku` artefact type, `haikus/*.md` file pattern, laws for form, imagery, and mood, a deterministic syllable validator where project dependencies allow it, two or three distinct appraisers, one cycle, and one flow.
|
|
75
75
|
|
|
@@ -6,7 +6,7 @@ description: Human quality gate. Presents the artefact to the human for review a
|
|
|
6
6
|
|
|
7
7
|
# Human Appraise
|
|
8
8
|
|
|
9
|
-
You are a human quality gate. Sort has routed to you
|
|
9
|
+
You are a human quality gate. Sort has routed to you for the human to review the current artefact and provide feedback or approve.
|
|
10
10
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
@@ -31,7 +31,7 @@ When invoked from orchestrate, you receive `{cycle, token, context}`:
|
|
|
31
31
|
- `cycle` — the current cycle id
|
|
32
32
|
- `token` — single-use token for `foundry_stage_begin`
|
|
33
33
|
- `context.artefact_file` — the target artefact
|
|
34
|
-
- `context.recent_feedback` — recent
|
|
34
|
+
- `context.recent_feedback` — recent unresolved feedback items to present to the user
|
|
35
35
|
|
|
36
36
|
Your FIRST tool call must be `foundry_stage_begin({stage: 'human-appraise:<cycle>', cycle, token})`.
|
|
37
37
|
|
|
@@ -63,31 +63,24 @@ Your LAST tool call must be `foundry_stage_end({summary: '<one-sentence descript
|
|
|
63
63
|
4. Present to the human:
|
|
64
64
|
- The current artefact content (full file content or multi-file diff)
|
|
65
65
|
- A summary of this iteration's feedback (resolved and open)
|
|
66
|
-
-
|
|
67
|
-
- Which feedback item(s) are stuck
|
|
68
|
-
- The appraiser's reasoning
|
|
69
|
-
- Forge's wont-fix or revision justification
|
|
70
|
-
- Ask the human to resolve the disagreement
|
|
66
|
+
- Ask the human to review, provide feedback, or approve
|
|
71
67
|
|
|
72
68
|
5. Wait for the human's response.
|
|
73
69
|
|
|
74
70
|
6. Act on the response (tag MUST be `human` on any added feedback — the tool rejects other tags during human-appraise):
|
|
75
71
|
- **Approve** — "looks good" / "continue" — no feedback added, sort will advance.
|
|
76
72
|
- **Provide feedback** — `foundry_feedback_add({ file, text, tag: 'human' })`. Sort will route back to forge.
|
|
77
|
-
- **Resolve feedback** — `foundry_feedback_resolve({ id, resolution, reason? })` for items in `{actioned, wont-fix
|
|
73
|
+
- **Resolve feedback** — `foundry_feedback_resolve({ id, resolution, reason? })` for items in `{actioned, wont-fix}`. See "Feedback handling" below for the legal transitions and authority rules.
|
|
78
74
|
- **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
75
|
|
|
80
76
|
7. `foundry_stage_end({summary})` — describe what the human decided so sort can log it.
|
|
81
77
|
|
|
82
78
|
## Feedback handling
|
|
83
79
|
|
|
84
|
-
As a human-appraise stage, you can add human feedback and resolve
|
|
85
|
-
items
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
limited to deadlocked items, though in practice most overrides today are
|
|
89
|
-
on deadlocked items because default sort routing only surfaces deadlocked
|
|
90
|
-
items to human-appraise (see §17 future-work note below).
|
|
80
|
+
As a human-appraise stage, you can add human feedback and resolve
|
|
81
|
+
feedback items. **Human-appraise can resolve any non-resolved
|
|
82
|
+
source-stage item regardless of source** — this is the universal
|
|
83
|
+
override authority recorded in spec §5.1 rule 5.
|
|
91
84
|
|
|
92
85
|
What human-appraise can NOT do:
|
|
93
86
|
|
|
@@ -109,37 +102,16 @@ What human-appraise CAN do:
|
|
|
109
102
|
found and no new snapshot was written, `deduped: false` indicates a new
|
|
110
103
|
item was created.
|
|
111
104
|
|
|
112
|
-
2. **Resolve any non-resolved
|
|
113
|
-
`{actioned, wont-fix}
|
|
114
|
-
human-appraise), call `foundry_feedback_resolve` with
|
|
105
|
+
2. **Resolve any non-resolved item.** For items in
|
|
106
|
+
`{actioned, wont-fix}`, call `foundry_feedback_resolve` with
|
|
115
107
|
`{ id, resolution: 'approved' | 'rejected', reason? }`. Human-appraise
|
|
116
108
|
may resolve any such item regardless of source, including items from
|
|
117
109
|
other stage ids.
|
|
118
110
|
|
|
119
|
-
3. **Resolve deadlocked items.** When items reach `state: deadlocked`
|
|
120
|
-
(written by sort when an item's history depth hits
|
|
121
|
-
`deadlock-iterations`), human-appraise is the ONLY stage authorised
|
|
122
|
-
to resolve them. Call `foundry_feedback_resolve` with
|
|
123
|
-
`{ id, resolution: 'approved' | 'rejected', reason: '...' }`.
|
|
124
|
-
`reason` is always required on deadlock override — it documents why
|
|
125
|
-
the deadlock is being broken. After human-appraise resolves every
|
|
126
|
-
deadlocked item, the cycle resumes normal forge/appraise routing. If
|
|
127
|
-
deadlocks remain after human-appraise, the cycle blocks (per spec §5.2).
|
|
128
|
-
|
|
129
111
|
**Reason rules.** `reason` is required when rejecting feedback
|
|
130
|
-
(`resolution: 'rejected'`)
|
|
131
|
-
Non-deadlocked approved resolution via
|
|
112
|
+
(`resolution: 'rejected'`). Approved resolution via
|
|
132
113
|
`foundry_feedback_resolve({ id, resolution: 'approved', reason? })` may
|
|
133
|
-
omit `reason
|
|
134
|
-
the deadlock is being broken.
|
|
135
|
-
|
|
136
|
-
**Future work.** Spec §17 notes that a cycle-level mode flag letting
|
|
137
|
-
human-appraise see all unresolved feedback (not just deadlocked items)
|
|
138
|
-
before sort routes is planned for a future release. In v2.6.0 the
|
|
139
|
-
authority is universal but reachability is limited — you typically only
|
|
140
|
-
see deadlocked items on the route from sort. If you do see non-deadlocked
|
|
141
|
-
items (e.g. you were invoked directly by the user), the same authority
|
|
142
|
-
applies.
|
|
114
|
+
omit `reason`.
|
|
143
115
|
|
|
144
116
|
## What you do NOT do
|
|
145
117
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@really-knows-ai/foundry",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
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",
|