@really-knows-ai/foundry 3.5.1 → 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-create-tools.js +2 -2
- 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 +21 -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/dist/skills/add-cycle/SKILL.md +6 -2
- package/dist/skills/add-flow/SKILL.md +5 -2
- package/package.json +3 -3
|
@@ -160,7 +160,7 @@ function cycleArgs(s) { return {
|
|
|
160
160
|
inputs: s.object({
|
|
161
161
|
type: s.enum(['any-of', 'all-of']).describe('Contract type: any-of (at least one) or all-of (all must exist)'),
|
|
162
162
|
artefacts: s.array(s.string()).describe('Artefact type IDs this cycle reads'),
|
|
163
|
-
}).optional().describe('Input contract for this cycle'),
|
|
163
|
+
}).optional().describe('Input contract for this cycle. Omit for source cycles that start from the user goal; empty artefacts arrays are invalid.'),
|
|
164
164
|
targets: s.array(s.string()).optional().describe('Downstream cycle IDs this cycle can route to'),
|
|
165
165
|
humanAppraise: s.boolean().optional().describe('Include human-appraise in every iteration'),
|
|
166
166
|
deadlockAppraise: s.boolean().optional().describe('Route to human-appraise on LLM appraiser deadlock'),
|
|
@@ -173,7 +173,7 @@ function cycleArgs(s) { return {
|
|
|
173
173
|
read: s.array(s.string()).describe('Memory store keys this cycle can read'),
|
|
174
174
|
write: s.array(s.string()).describe('Memory store keys this cycle can write'),
|
|
175
175
|
}).optional().describe('Flow memory permissions'),
|
|
176
|
-
models: s.object({}).optional().describe('Per-stage model overrides (e.g. { forge: "
|
|
176
|
+
models: s.object({}).optional().describe('Per-stage model overrides (e.g. { forge: "opencode-go/deepseek-v4-flash", appraise: "opencode-go/qwen3.6-plus" }). Preserve user-selected stage models.'),
|
|
177
177
|
description: s.string().optional().describe('Prose description placed after frontmatter'),
|
|
178
178
|
}; }
|
|
179
179
|
|
|
@@ -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,26 @@
|
|
|
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
|
+
|
|
18
|
+
## [3.5.2] - 2026-05-23
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Guide agents to omit empty `inputs` for source cycles and preserve stage-specific model overrides when creating cycles.
|
|
23
|
+
|
|
3
24
|
## [3.5.1] - 2026-05-22
|
|
4
25
|
|
|
5
26
|
### 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
|
+
}
|
|
@@ -40,6 +40,10 @@ When invoked with pre-filled fields matching the `foundry_config_create_cycle` t
|
|
|
40
40
|
|
|
41
41
|
Context fields: `{id, name, outputType, description, inputs?, targets?, humanAppraise?, deadlockAppraise?, deadlockIterations?, maxIterations?, assay?, memory?, models?}`
|
|
42
42
|
|
|
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
|
+
|
|
45
|
+
`models` is a map of stage names to model IDs. Preserve user-selected model overrides exactly, for example `{forge: "opencode-go/deepseek-v4-flash", appraise: "opencode-go/qwen3.6-plus"}`.
|
|
46
|
+
|
|
43
47
|
When invoked with a context:
|
|
44
48
|
- If all required fields are present, skip the Understand phase and proceed to Plan → Confirm → Build.
|
|
45
49
|
- If only some fields are present, ask only for the missing ones.
|
|
@@ -80,7 +84,7 @@ If the parent flow or required artefact type is missing and the user's goal clea
|
|
|
80
84
|
|
|
81
85
|
**Optional clusters** — After each cluster, ask whether the user wants to configure it; if not, skip:
|
|
82
86
|
|
|
83
|
-
- **Routing**: `inputs` (input contract: `{type: "any-of"|"all-of", artefacts: string[]}
|
|
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)
|
|
84
88
|
- **Human-appraise**: `humanAppraise` (boolean, default false) — human reviews every iteration; `deadlockAppraise` (boolean, default true) — human is pulled in when LLM appraisers deadlock; `deadlockIterations` (number, default 5) — deadlock threshold. Only applies when either appraise is enabled.
|
|
85
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.
|
|
86
90
|
|
|
@@ -98,7 +102,7 @@ Ask: "Proceed with this plan?" — wait for user answer before building. If the
|
|
|
98
102
|
|
|
99
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.
|
|
100
104
|
|
|
101
|
-
2. **Create**: Call `foundry_config_create_cycle({ id: "<id>", name: "<name>", outputType: "<type>", description: "<description>",
|
|
105
|
+
2. **Create**: Call `foundry_config_create_cycle({ id: "<id>", name: "<name>", outputType: "<type>", description: "<description>", targets: ..., humanAppraise: ..., deadlockAppraise: ..., deadlockIterations: ..., 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:
|
|
102
106
|
- re-validates the body (TOCTOU);
|
|
103
107
|
- writes `foundry/cycles/<id>.md`;
|
|
104
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?, humanAppraise?, deadlockAppraise?, deadlockIterations?, maxIterations?, assay?, memory?, models?}`.
|
|
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?, humanAppraise?, deadlockAppraise?, deadlockIterations?, 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
|
|
|
@@ -92,6 +92,7 @@ Flow: <id> — <name>
|
|
|
92
92
|
· <id> — <description>
|
|
93
93
|
Cycles:
|
|
94
94
|
· <id> → <outputType> — <description>
|
|
95
|
+
inputs/models: <omitted or explicit settings>
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
Ask "Proceed with this plan?" — do not build anything until the user confirms.
|
|
@@ -121,9 +122,11 @@ Build order (dependency order):
|
|
|
121
122
|
|
|
122
123
|
4. **Cycles**: For each new cycle, invoke the `add-cycle` protocol with the captured context.
|
|
123
124
|
|
|
124
|
-
> Invoke the add-cycle protocol with context: `{id: "haiku-cycle", name: "Haiku Cycle", outputType: "haiku", description: "Generates haiku poems"}`.
|
|
125
|
+
> Invoke the add-cycle protocol with context: `{id: "haiku-cycle", name: "Haiku Cycle", outputType: "haiku", description: "Generates haiku poems", models: {forge: "opencode-go/deepseek-v4-flash", appraise: "opencode-go/qwen3.6-plus"}}`.
|
|
125
126
|
> If all required fields are present, proceed directly to Build. Otherwise ask for missing required fields only.
|
|
126
127
|
|
|
128
|
+
Preserve every user-selected stage model in the cycle context. If the cycle has no upstream artefact input, leave `inputs` absent from the context.
|
|
129
|
+
|
|
127
130
|
**Build-only mode**: When all required fields for a sub-skill are present in the context, the sub-skill skips Understand, Plan, and Confirm — proceeding directly to validate → create → commit. When only some required fields are present, the sub-skill enters its Understand phase to ask only for those missing required fields, then proceeds to Build (still skipping Plan and Confirm since the parent's combined plan already handled confirmation). Optional fields that are missing are silently skipped.
|
|
128
131
|
|
|
129
132
|
**Error handling during build**: If a sub-skill's Build phase fails (validation error or tool error), surface the error to the user:
|
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
|
}
|