@really-knows-ai/foundry 3.5.2 → 3.5.3
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/config-tools.js +8 -2
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +2 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +52 -1
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +2 -0
- package/dist/CHANGELOG.md +15 -0
- package/dist/scripts/lib/feedback-store.js +15 -8
- package/dist/scripts/lib/stage-calls.js +56 -0
- package/dist/scripts/orchestrate-cycle.js +26 -3
- package/dist/scripts/orchestrate-phases.js +26 -55
- package/dist/scripts/orchestrate-terminals.js +49 -0
- package/package.json +3 -3
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { getCycleDefinition, getArtefactType, getLaws, getAppraisers, getFlow } from '../../../scripts/lib/config.js';
|
|
2
2
|
import { makeIO } from './helpers.js';
|
|
3
|
+
import { writeCall } from '../../../scripts/lib/stage-calls.js';
|
|
3
4
|
|
|
4
|
-
function makeConfigTool(tool, description, argSchema, invoke) {
|
|
5
|
+
function makeConfigTool(tool, description, argSchema, invoke, logName) {
|
|
5
6
|
return tool({
|
|
6
7
|
description,
|
|
7
8
|
args: argSchema,
|
|
8
9
|
async execute(args, context) {
|
|
9
10
|
const io = makeIO(context.worktree);
|
|
10
|
-
|
|
11
|
+
const result = await invoke(args, io);
|
|
12
|
+
if (logName) writeCall(io, logName);
|
|
13
|
+
return JSON.stringify(result);
|
|
11
14
|
},
|
|
12
15
|
});
|
|
13
16
|
}
|
|
@@ -18,16 +21,19 @@ export function createConfigTools({ tool }) {
|
|
|
18
21
|
tool, 'Get a cycle definition from foundry config',
|
|
19
22
|
{ cycleId: tool.schema.string().describe('Cycle ID') },
|
|
20
23
|
(args, io) => getCycleDefinition('foundry', args.cycleId, io),
|
|
24
|
+
'foundry_config_cycle',
|
|
21
25
|
),
|
|
22
26
|
foundry_config_artefact_type: makeConfigTool(
|
|
23
27
|
tool, 'Get an artefact type definition',
|
|
24
28
|
{ typeId: tool.schema.string().describe('Artefact type ID') },
|
|
25
29
|
(args, io) => getArtefactType('foundry', args.typeId, io),
|
|
30
|
+
'foundry_config_artefact_type',
|
|
26
31
|
),
|
|
27
32
|
foundry_config_laws: makeConfigTool(
|
|
28
33
|
tool, 'Get laws, optionally filtered by artefact type',
|
|
29
34
|
{ typeId: tool.schema.string().optional().describe('Artefact type ID') },
|
|
30
35
|
(args, io) => getLaws('foundry', io, { typeId: args.typeId }),
|
|
36
|
+
'foundry_config_laws',
|
|
31
37
|
),
|
|
32
38
|
foundry_config_appraisers: makeConfigTool(
|
|
33
39
|
tool, 'List all appraisers',
|
|
@@ -3,6 +3,7 @@ import { parseFrontmatter } from '../../../scripts/lib/workfile.js';
|
|
|
3
3
|
import { requireActiveStage, stageBaseOf } from '../../../scripts/lib/stage-guard.js';
|
|
4
4
|
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
5
5
|
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
6
|
+
import { writeCall } from '../../../scripts/lib/stage-calls.js';
|
|
6
7
|
|
|
7
8
|
const gateNotFailed = notFailedGuard(makeIO);
|
|
8
9
|
|
|
@@ -170,6 +171,7 @@ async function executeFeedbackList(args, context) {
|
|
|
170
171
|
}
|
|
171
172
|
try {
|
|
172
173
|
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
174
|
+
writeCall(io, 'foundry_feedback_list');
|
|
173
175
|
const items = store.list()
|
|
174
176
|
.filter(it => !args.file || it.file === args.file)
|
|
175
177
|
.map(it => {
|
|
@@ -8,10 +8,31 @@ import { syncStore } from '../../../scripts/lib/memory/store.js';
|
|
|
8
8
|
import { makeIO, makeMemoryIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
9
9
|
import { markWorkfileFailed, readFailedStatus, clearWorkfileFailed } from '../../../scripts/lib/failed-flow.js';
|
|
10
10
|
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
11
|
+
import { initForgeCallLog, verifyAndClearForgeCallLog } from '../../../scripts/lib/stage-calls.js';
|
|
12
|
+
import { openFeedbackStore } from '../../../scripts/lib/feedback-store.js';
|
|
13
|
+
|
|
14
|
+
const FORGE_REQUIRED_TOOLS = [
|
|
15
|
+
'foundry_config_cycle',
|
|
16
|
+
'foundry_workfile_get',
|
|
17
|
+
'foundry_config_artefact_type',
|
|
18
|
+
'foundry_config_laws',
|
|
19
|
+
'foundry_feedback_list',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function stageBase(stage) { return stage.split(':')[0]; }
|
|
11
23
|
|
|
12
24
|
const gateNotFailed = notFailedGuard(makeIO);
|
|
13
25
|
|
|
14
|
-
// -- Helpers for
|
|
26
|
+
// -- Helpers for forge tool call verification --
|
|
27
|
+
|
|
28
|
+
function verifyAndManageForgeTools(io, active) {
|
|
29
|
+
const verified = verifyAndClearForgeCallLog(io, FORGE_REQUIRED_TOOLS);
|
|
30
|
+
if (!verified.ok) {
|
|
31
|
+
postMissingToolsFeedback(io, active, verified.missing);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
resolveSystemFeedback(io, active);
|
|
35
|
+
}
|
|
15
36
|
|
|
16
37
|
function resolveBaseSha(worktree) {
|
|
17
38
|
try {
|
|
@@ -59,9 +80,14 @@ async function executeStageBegin(args, context, pending) {
|
|
|
59
80
|
startedAt: new Date().toISOString(),
|
|
60
81
|
};
|
|
61
82
|
writeActiveStage(io, active);
|
|
83
|
+
initForgeIfApplicable(io, active.stage);
|
|
62
84
|
return JSON.stringify({ ok: true, active });
|
|
63
85
|
}
|
|
64
86
|
|
|
87
|
+
function initForgeIfApplicable(io, stage) {
|
|
88
|
+
if (stageBase(stage) === 'forge') initForgeCallLog(io);
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
// -- Helpers for foundry_stage_end --
|
|
66
92
|
|
|
67
93
|
function markWorkfileFailedSilently(io, msg) {
|
|
@@ -82,6 +108,11 @@ async function executeStageEnd(args, context) {
|
|
|
82
108
|
if (!active) {
|
|
83
109
|
return JSON.stringify({ error: 'foundry_stage_end requires active stage; current: none' });
|
|
84
110
|
}
|
|
111
|
+
|
|
112
|
+
if (stageBase(active.stage) === 'forge') {
|
|
113
|
+
verifyAndManageForgeTools(io, active);
|
|
114
|
+
}
|
|
115
|
+
|
|
85
116
|
writeLastStage(io, {
|
|
86
117
|
cycle: active.cycle,
|
|
87
118
|
stage: active.stage,
|
|
@@ -101,6 +132,26 @@ async function executeStageEnd(args, context) {
|
|
|
101
132
|
return JSON.stringify({ ok: true, summary: args.summary });
|
|
102
133
|
}
|
|
103
134
|
|
|
135
|
+
function postMissingToolsFeedback(io, active, missing) {
|
|
136
|
+
try {
|
|
137
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
138
|
+
store.add({
|
|
139
|
+
file: '(forge)',
|
|
140
|
+
tag: 'system:missing-tool-calls',
|
|
141
|
+
text: `Missing required forge tools: ${missing.join(', ')}`,
|
|
142
|
+
source: active.stage,
|
|
143
|
+
cycle: active.cycle,
|
|
144
|
+
});
|
|
145
|
+
} catch { /* feedback file not initialised yet; non-critical */ }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveSystemFeedback(io, active) {
|
|
149
|
+
try {
|
|
150
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
151
|
+
store.resolveSystemItems(active.stage, active.cycle);
|
|
152
|
+
} catch { /* non-critical */ }
|
|
153
|
+
}
|
|
154
|
+
|
|
104
155
|
// -- Helpers for foundry_stage_retry --
|
|
105
156
|
|
|
106
157
|
function checkGitWorkingTreeClean(worktree) {
|
|
@@ -4,6 +4,7 @@ import { requireNoActiveStage } from '../../../scripts/lib/stage-guard.js';
|
|
|
4
4
|
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
5
5
|
import { parseFrontmatter, createWorkfile, enrichStages, parseModelsValue } from '../../../scripts/lib/workfile.js';
|
|
6
6
|
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
7
|
+
import { writeCall } from '../../../scripts/lib/stage-calls.js';
|
|
7
8
|
|
|
8
9
|
const gateNotFailed = notFailedGuard(makeIO);
|
|
9
10
|
|
|
@@ -84,6 +85,7 @@ export function createWorkfileTools({ tool }) {
|
|
|
84
85
|
const fm = parseFrontmatter(text);
|
|
85
86
|
const goalMatch = text.match(/# Goal\n\n([\s\S]*?)(?=\n\||\n##|$)/);
|
|
86
87
|
const goal = goalMatch ? goalMatch[1].trim() : '';
|
|
88
|
+
writeCall(makeIO(context.worktree), 'foundry_workfile_get');
|
|
87
89
|
return JSON.stringify({ ...fm, goal });
|
|
88
90
|
},
|
|
89
91
|
}),
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.5.3] - 2026-05-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Forge tool-call verification: `stage_end` checks that required tools were called and posts system feedback if not, with automatic retry via the existing feedback loop.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Forge dispatch prompts now include explicit required-tool instructions with the actual output type filled in.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `readForgeFilePatterns` returns `{ patterns, outputType }` instead of a bare array.
|
|
16
|
+
- Build quality gate now writes a seal; `prepublishOnly` verifies the seal instead of re-running lint and tests.
|
|
17
|
+
|
|
3
18
|
## [3.5.2] - 2026-05-23
|
|
4
19
|
|
|
5
20
|
### Fixed
|
|
@@ -60,14 +60,22 @@ function cloneItem(it) {
|
|
|
60
60
|
return { ...it, history: it.history.map(h => ({ ...h })) };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function resolveSystemItemsImpl({ items, stage, cycle, timestamp, persist }) {
|
|
64
|
+
const snapshot = { state: 'resolved', stage, cycle, timestamp: timestamp() };
|
|
65
|
+
const next = items.map(it =>
|
|
66
|
+
it.tag === 'system:missing-tool-calls' && it.history[0].state !== 'resolved'
|
|
67
|
+
? { ...it, history: [snapshot, ...it.history] }
|
|
68
|
+
: it
|
|
69
|
+
);
|
|
70
|
+
persist(next);
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
export function openFeedbackStore(path, io) {
|
|
64
74
|
let items = loadItems(path, io);
|
|
65
|
-
|
|
66
75
|
function persist(nextItems) {
|
|
67
76
|
saveItems(path, nextItems, io);
|
|
68
77
|
items = nextItems;
|
|
69
78
|
}
|
|
70
|
-
|
|
71
79
|
return {
|
|
72
80
|
list() { return items.map(cloneItem); },
|
|
73
81
|
get(id) {
|
|
@@ -89,14 +97,13 @@ export function openFeedbackStore(path, io) {
|
|
|
89
97
|
});
|
|
90
98
|
},
|
|
91
99
|
writeDeadlockedSnapshotForTest(params) {
|
|
92
|
-
return storeWriteDeadlockedSnapshot(params, items, {
|
|
93
|
-
timestamp: nowIso, persist,
|
|
94
|
-
});
|
|
100
|
+
return storeWriteDeadlockedSnapshot(params, items, { timestamp: nowIso, persist });
|
|
95
101
|
},
|
|
96
102
|
writeDeadlockedSnapshots(ids, reason, stage, cycle) {
|
|
97
|
-
return storeWriteDeadlockedSnapshots({ ids, reason, stage, cycle }, items, {
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
return storeWriteDeadlockedSnapshots({ ids, reason, stage, cycle }, items, { timestamp: nowIso, persist });
|
|
104
|
+
},
|
|
105
|
+
resolveSystemItems(stage, cycle) {
|
|
106
|
+
resolveSystemItemsImpl({ items, stage, cycle, timestamp: nowIso, persist });
|
|
100
107
|
},
|
|
101
108
|
};
|
|
102
109
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const LOG_PATH = '.foundry/.forge-tool-calls.jsonl';
|
|
2
|
+
const RETRIES_PATH = '.foundry/.forge-tool-retries';
|
|
3
|
+
|
|
4
|
+
export function initForgeCallLog(io) {
|
|
5
|
+
io.writeFile(LOG_PATH, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function writeCall(io, toolName) {
|
|
9
|
+
if (!io.exists(LOG_PATH)) return;
|
|
10
|
+
const entry = JSON.stringify({ tool: toolName, ts: Date.now() }) + '\n';
|
|
11
|
+
const existing = io.readFile(LOG_PATH);
|
|
12
|
+
io.writeFile(LOG_PATH, existing + entry);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function addCallFromLine(line, called) {
|
|
16
|
+
try {
|
|
17
|
+
const rec = JSON.parse(line);
|
|
18
|
+
if (rec.tool) called.add(rec.tool);
|
|
19
|
+
} catch { /* skip malformed lines */ }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readCallSet(io) {
|
|
23
|
+
const called = new Set();
|
|
24
|
+
if (!io.exists(LOG_PATH)) return called;
|
|
25
|
+
const content = io.readFile(LOG_PATH);
|
|
26
|
+
for (const line of content.split('\n')) {
|
|
27
|
+
if (line) addCallFromLine(line, called);
|
|
28
|
+
}
|
|
29
|
+
return called;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function verifyAndClearForgeCallLog(io, expected) {
|
|
33
|
+
const called = readCallSet(io);
|
|
34
|
+
const missing = expected.filter(t => !called.has(t));
|
|
35
|
+
io.unlink(LOG_PATH);
|
|
36
|
+
return missing.length ? { ok: false, missing } : { ok: true, missing: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readForgeRetryCount(io) {
|
|
40
|
+
if (!io.exists(RETRIES_PATH)) return 0;
|
|
41
|
+
try {
|
|
42
|
+
return parseInt(io.readFile(RETRIES_PATH).trim(), 10) || 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function incrementForgeRetryCount(io) {
|
|
49
|
+
const count = readForgeRetryCount(io) + 1;
|
|
50
|
+
io.writeFile(RETRIES_PATH, String(count));
|
|
51
|
+
return count;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resetForgeRetryCount(io) {
|
|
55
|
+
io.unlink(RETRIES_PATH);
|
|
56
|
+
}
|
|
@@ -39,7 +39,8 @@ export async function readForgeFilePatterns(cycleId, io) {
|
|
|
39
39
|
const output = extractOutputType(cd);
|
|
40
40
|
if (!output) return null;
|
|
41
41
|
try {
|
|
42
|
-
|
|
42
|
+
const patterns = await fetchFilePatterns(output, io);
|
|
43
|
+
return patterns ? { patterns, outputType: output } : null;
|
|
43
44
|
} catch {
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
@@ -228,9 +229,28 @@ export function buildDispatchMultiResponse(tasks, stage, cycle) {
|
|
|
228
229
|
// Dispatch prompt rendering (pure utility, used by handleSortResult and exported publicly).
|
|
229
230
|
// ---------------------------------------------------------------------------
|
|
230
231
|
|
|
231
|
-
|
|
232
|
+
function buildForgePromptLines({ cycle, outputType }) {
|
|
233
|
+
return [
|
|
234
|
+
``,
|
|
235
|
+
`Before producing output you MUST call these tools to understand the context:`,
|
|
236
|
+
outputType
|
|
237
|
+
? ` - foundry_config_cycle({ cycleId: "${cycle}" }) — to learn the cycle definition, including its output type "${outputType}"`
|
|
238
|
+
: ` - foundry_config_cycle({ cycleId: "${cycle}" }) — to learn the cycle definition`,
|
|
239
|
+
outputType
|
|
240
|
+
? ` - foundry_config_artefact_type({ typeId: "${outputType}" }) — to learn the artefact type definition and file patterns`
|
|
241
|
+
: ` - foundry_config_artefact_type({ typeId: "<output type>" }) — to learn the artefact type definition and file patterns`,
|
|
242
|
+
outputType
|
|
243
|
+
? ` - foundry_config_laws({ typeId: "${outputType}" }) — to learn all applicable quality laws`
|
|
244
|
+
: ` - foundry_config_laws({ typeId: "<output type>" }) — to learn all applicable quality laws`,
|
|
245
|
+
` - foundry_workfile_get({}) — to learn the goal`,
|
|
246
|
+
` - foundry_feedback_list({}) — to check for existing feedback from prior iterations`,
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns, outputType }) {
|
|
251
|
+
const base = stage.split(':')[0];
|
|
232
252
|
const lines = [
|
|
233
|
-
`You are a Foundry stage agent. Invoke the ${
|
|
253
|
+
`You are a Foundry stage agent. Invoke the ${base} skill and follow its instructions exactly.`,
|
|
234
254
|
``,
|
|
235
255
|
`Stage: ${stage}`,
|
|
236
256
|
`Cycle: ${cycle}`,
|
|
@@ -240,6 +260,9 @@ export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns })
|
|
|
240
260
|
if (filePatterns && filePatterns.length) {
|
|
241
261
|
lines.push(`File patterns (forge only): ${JSON.stringify(filePatterns)}`);
|
|
242
262
|
}
|
|
263
|
+
if (base === 'forge') {
|
|
264
|
+
lines.push(...buildForgePromptLines({ cycle, outputType }));
|
|
265
|
+
}
|
|
243
266
|
lines.push(
|
|
244
267
|
``,
|
|
245
268
|
`Your FIRST tool call MUST be foundry_stage_begin({stage, cycle, token}) using the values above.`,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Private phase functions used by runOrchestrate.
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
getCycleDefinition,
|
|
6
5
|
getArtefactType,
|
|
6
|
+
getCycleDefinition,
|
|
7
7
|
getLawsForQuench,
|
|
8
8
|
} from './lib/config.js';
|
|
9
9
|
import { parseFrontmatter, writeFrontmatter } from './lib/workfile.js';
|
|
@@ -13,73 +13,40 @@ import { stageBaseOf } from './lib/stage-guard.js';
|
|
|
13
13
|
import { allowedPatternsForStage } from './lib/git-policy.js';
|
|
14
14
|
import { loadExtractor } from './lib/assay/loader.js';
|
|
15
15
|
import { checkExtractorAgainstCycle } from './lib/assay/permissions.js';
|
|
16
|
-
import { getArtefactFiles } from './lib/artefacts.js';
|
|
17
16
|
import {
|
|
18
|
-
readCycleTargets,
|
|
19
17
|
readForgeFilePatterns,
|
|
20
|
-
readRecentFeedback,
|
|
21
18
|
computeOpenFeedback,
|
|
22
19
|
violation,
|
|
23
20
|
tryCommit,
|
|
24
21
|
synthesizeStages,
|
|
25
22
|
renderDispatchPrompt,
|
|
26
23
|
} from './orchestrate-cycle.js';
|
|
24
|
+
import {
|
|
25
|
+
doneAction,
|
|
26
|
+
blockedAction,
|
|
27
|
+
humanAppraiseAction,
|
|
28
|
+
missingModelViolation,
|
|
29
|
+
} from './orchestrate-terminals.js';
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!outputType) return null;
|
|
31
|
-
const artefacts = await getArtefactFiles(foundryDir, outputType, io, { baseBranch });
|
|
32
|
-
return artefacts.find(a => a.state !== 'deleted') || null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function doneAction(cycleId, io, foundryDir, baseBranch) {
|
|
36
|
-
const fd = foundryDir || 'foundry';
|
|
37
|
-
const base = baseBranch || 'main';
|
|
38
|
-
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
39
|
-
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
40
|
-
const artefactFile = artefact ? artefact.file : null;
|
|
41
|
-
return { action: 'done', cycle: cycleId, artefact_file: artefactFile, next_cycles: await readCycleTargets(cycleId, io) };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function blockedAction(cycleId, io, details, foundryDir, baseBranch) {
|
|
45
|
-
const fd = foundryDir || 'foundry';
|
|
46
|
-
const base = baseBranch || 'main';
|
|
47
|
-
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
48
|
-
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
49
|
-
const artefactFile = artefact ? artefact.file : null;
|
|
50
|
-
const reason = details || 'iteration limit reached with unresolved feedback';
|
|
51
|
-
return { action: 'blocked', cycle: cycleId, artefact_file: artefactFile, reason };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function humanAppraiseAction(route, token, ctx) {
|
|
55
|
-
const { cycleId, io, baseBranch } = ctx;
|
|
56
|
-
const fd = ctx.foundryDir || 'foundry';
|
|
57
|
-
const base = baseBranch || 'main';
|
|
58
|
-
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
59
|
-
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
60
|
-
const artefactFile = artefact ? artefact.file : null;
|
|
61
|
-
return { action: 'human_appraise', stage: route, token, context: { cycle: cycleId, artefact_file: artefactFile, recent_feedback: readRecentFeedback(io) } };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function missingModelViolation(cycleId, route, io, foundryDir, baseBranch) {
|
|
65
|
-
const fd = foundryDir || 'foundry';
|
|
66
|
-
const base = baseBranch || 'main';
|
|
67
|
-
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
68
|
-
const outputType = cfm ? cfm['output-type'] : undefined;
|
|
69
|
-
const artefacts = outputType ? await getArtefactFiles(fd, outputType, io, { baseBranch: base }) : [];
|
|
70
|
-
const affectedFiles = artefacts.filter(a => a.state !== 'deleted').map(a => a.file);
|
|
71
|
-
return violation(`cycle ${cycleId} stage ${route} has no model declared in cycle definition`, affectedFiles);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function makeDispatchPayload(route, cycleId, token, cwd, filePatterns) {
|
|
75
|
-
return { stage: route, cycle: cycleId, token, cwd, filePatterns };
|
|
31
|
+
function makeDispatchPayload({ route, cycleId, token, cwd, filePatterns, outputType }) {
|
|
32
|
+
return { stage: route, cycle: cycleId, token, cwd, filePatterns, outputType };
|
|
76
33
|
}
|
|
77
34
|
|
|
78
35
|
async function buildDispatchAction(route, model, token, ctx) {
|
|
79
36
|
if (!model) return missingModelViolation(ctx.cycleId, route, ctx.io, ctx.foundryDir, ctx.baseBranch ?? 'main');
|
|
80
37
|
const base = route.split(':')[0];
|
|
81
|
-
|
|
82
|
-
|
|
38
|
+
let filePatterns = null;
|
|
39
|
+
let outputType = null;
|
|
40
|
+
if (base === 'forge') {
|
|
41
|
+
const result = await readForgeFilePatterns(ctx.cycleId, ctx.io);
|
|
42
|
+
if (result) {
|
|
43
|
+
filePatterns = result.patterns;
|
|
44
|
+
outputType = result.outputType;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const payload = { route, cycleId: ctx.cycleId, token, cwd: ctx.cwd, filePatterns, outputType };
|
|
48
|
+
return { action: 'dispatch', stage: route, subagent_type: model,
|
|
49
|
+
prompt: renderDispatchPrompt(makeDispatchPayload(payload)) };
|
|
83
50
|
}
|
|
84
51
|
|
|
85
52
|
export function routeDispatch(route) {
|
|
@@ -282,7 +249,11 @@ function writeHistoryEntries(ctx) {
|
|
|
282
249
|
|
|
283
250
|
async function computeAllowedPatterns(lastStage, cycleId, io) {
|
|
284
251
|
const stageBase = stageBaseOf(lastStage.stage);
|
|
285
|
-
|
|
252
|
+
let forgeFilePatterns = [];
|
|
253
|
+
if (stageBase === 'forge') {
|
|
254
|
+
const result = await readForgeFilePatterns(cycleId, io);
|
|
255
|
+
forgeFilePatterns = result ? result.patterns : [];
|
|
256
|
+
}
|
|
286
257
|
return allowedPatternsForStage({ stageBase, forgeFilePatterns });
|
|
287
258
|
}
|
|
288
259
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getCycleDefinition } from './lib/config.js';
|
|
2
|
+
import { getArtefactFiles } from './lib/artefacts.js';
|
|
3
|
+
import { readCycleTargets, readRecentFeedback, violation } from './orchestrate-cycle.js';
|
|
4
|
+
|
|
5
|
+
async function findOutputArtefacts(cfm, io, foundryDir, baseBranch) {
|
|
6
|
+
const outputType = cfm ? cfm['output-type'] : undefined;
|
|
7
|
+
if (!outputType) return null;
|
|
8
|
+
const artefacts = await getArtefactFiles(foundryDir, outputType, io, { baseBranch });
|
|
9
|
+
return artefacts.find(a => a.state !== 'deleted') || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function doneAction(cycleId, io, foundryDir, baseBranch) {
|
|
13
|
+
const fd = foundryDir || 'foundry';
|
|
14
|
+
const base = baseBranch || 'main';
|
|
15
|
+
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
16
|
+
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
17
|
+
const artefactFile = artefact ? artefact.file : null;
|
|
18
|
+
return { action: 'done', cycle: cycleId, artefact_file: artefactFile, next_cycles: await readCycleTargets(cycleId, io) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function blockedAction(cycleId, io, details, foundryDir, baseBranch) {
|
|
22
|
+
const fd = foundryDir || 'foundry';
|
|
23
|
+
const base = baseBranch || 'main';
|
|
24
|
+
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
25
|
+
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
26
|
+
const artefactFile = artefact ? artefact.file : null;
|
|
27
|
+
const reason = details || 'iteration limit reached with unresolved feedback';
|
|
28
|
+
return { action: 'blocked', cycle: cycleId, artefact_file: artefactFile, reason };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function humanAppraiseAction(route, token, ctx) {
|
|
32
|
+
const { cycleId, io, baseBranch } = ctx;
|
|
33
|
+
const fd = ctx.foundryDir || 'foundry';
|
|
34
|
+
const base = baseBranch || 'main';
|
|
35
|
+
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
36
|
+
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
37
|
+
const artefactFile = artefact ? artefact.file : null;
|
|
38
|
+
return { action: 'human_appraise', stage: route, token, context: { cycle: cycleId, artefact_file: artefactFile, recent_feedback: readRecentFeedback(io) } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function missingModelViolation(cycleId, route, io, foundryDir, baseBranch) {
|
|
42
|
+
const fd = foundryDir || 'foundry';
|
|
43
|
+
const base = baseBranch || 'main';
|
|
44
|
+
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
45
|
+
const outputType = cfm ? cfm['output-type'] : undefined;
|
|
46
|
+
const artefacts = outputType ? await getArtefactFiles(fd, outputType, io, { baseBranch: base }) : [];
|
|
47
|
+
const affectedFiles = artefacts.filter(a => a.state !== 'deleted').map(a => a.file);
|
|
48
|
+
return violation(`cycle ${cycleId} stage ${route} has no model declared in cycle definition`, affectedFiles);
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@really-knows-ai/foundry",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.3",
|
|
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",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
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
|
-
"build:full": "pnpm run lint --fix && pnpm run test:all && pnpm run build",
|
|
62
|
-
"build:all": "pnpm run lint && pnpm run test:all && pnpm run build"
|
|
61
|
+
"build:full": "pnpm run lint --fix && pnpm run test:all && pnpm run build && node scripts/seal.js",
|
|
62
|
+
"build:all": "pnpm run lint && pnpm run test:all && pnpm run build && node scripts/seal.js"
|
|
63
63
|
}
|
|
64
64
|
}
|