@really-knows-ai/foundry 3.5.2 → 3.5.4
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/orchestrate-tool.js +22 -22
- 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 +26 -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-flow/SKILL.md +2 -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 => {
|
|
@@ -87,18 +87,28 @@ async function injectDispatchPromptExtras(result, cwd) {
|
|
|
87
87
|
result.prompt = `${result.prompt}\n\n${extras}`;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function buildOrchestrateArgs(tool) {
|
|
91
|
+
return {
|
|
92
|
+
lastResult: tool.schema.object({
|
|
93
|
+
ok: tool.schema.boolean(),
|
|
94
|
+
error: tool.schema.string().optional(),
|
|
95
|
+
}).optional().describe('Result of a single-subagent dispatch or human-appraise stage'),
|
|
96
|
+
lastResults: tool.schema.array(tool.schema.object({
|
|
97
|
+
ok: tool.schema.boolean(),
|
|
98
|
+
output: tool.schema.string().optional(),
|
|
99
|
+
error: tool.schema.string().optional(),
|
|
100
|
+
})).optional().describe('Results of a dispatch_multi (appraise) — one entry per completed appraiser task'),
|
|
101
|
+
cycleDef: tool.schema.string().optional().describe('Test-mode cycle definition override (path to cycle file)'),
|
|
102
|
+
baseBranch: tool.schema.string().optional().describe('Git base branch for artefact diff comparison (default "main")'),
|
|
103
|
+
defaultModel: tool.schema.string().optional().describe('Fallback model for stages with no explicit model in the cycle definition (e.g. "opencode-go/deepseek-v4-flash")'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
export function createOrchestrateTool({ tool, pending }) {
|
|
91
108
|
return {
|
|
92
109
|
foundry_orchestrate: tool({
|
|
93
|
-
description: 'Run the next step of the current cycle. Call with no args on first invocation
|
|
94
|
-
args:
|
|
95
|
-
lastResult: tool.schema.object({
|
|
96
|
-
ok: tool.schema.boolean(),
|
|
97
|
-
error: tool.schema.string().optional(),
|
|
98
|
-
}).optional(),
|
|
99
|
-
cycleDef: tool.schema.string().optional().describe('Test-mode cycle definition override (path to cycle file)'),
|
|
100
|
-
defaultModel: tool.schema.string().optional().describe('Fallback model for stages with no explicit model in the cycle definition (e.g. "opencode-go/deepseek-v4-flash")'),
|
|
101
|
-
},
|
|
110
|
+
description: 'Run the next step of the current cycle. Call with no args on first invocation. After a dispatch or human_appraise, pass lastResult={ok,error?}. After a dispatch_multi (appraise), pass lastResults as an array of {ok,output?,error?} — one entry per completed task. Returns {action, ...} describing what the caller should do next.',
|
|
111
|
+
args: buildOrchestrateArgs(tool),
|
|
102
112
|
|
|
103
113
|
async execute(args, context) {
|
|
104
114
|
const { runOrchestrate } = await import('../../../scripts/orchestrate.js');
|
|
@@ -107,21 +117,9 @@ export function createOrchestrateTool({ tool, pending }) {
|
|
|
107
117
|
const secret = readOrCreateSecret(context.worktree);
|
|
108
118
|
|
|
109
119
|
try {
|
|
110
|
-
// Branch guard. Kept inline because the orchestrate tool surfaces all errors through its violation
|
|
111
|
-
// envelope (see comment on the failed-flow guard below). A
|
|
112
|
-
// wrong-branch refusal is a more fundamental error than failed
|
|
113
|
-
// flow, so it runs first.
|
|
114
120
|
const branchGuard = requireOnFlowBranch({ exec: makeExec(cwd) });
|
|
115
121
|
if (!branchGuard.ok) return JSON.stringify({ error: `foundry_orchestrate: ${branchGuard.error}` });
|
|
116
122
|
|
|
117
|
-
// Failed-flow guard. Kept inline to preserve the violation envelope.
|
|
118
|
-
// because requireNotFailed parses WORK.md frontmatter, which throws
|
|
119
|
-
// on malformed YAML. The surrounding try/catch (line 30) converts
|
|
120
|
-
// that throw into a violation-shaped envelope per the contract
|
|
121
|
-
// exercised by tests/plugin/orchestrate-wrapper.test.js. A guarded()
|
|
122
|
-
// wrapper would let the throw escape to a plain { error } envelope
|
|
123
|
-
// and break that contract. orchestrate-tool is the one Phase 1.5
|
|
124
|
-
// exception to the inline-gate refactor.
|
|
125
123
|
const failedGuard = requireNotFailed(io);
|
|
126
124
|
if (!failedGuard.ok) return JSON.stringify({ error: `foundry_orchestrate: ${failedGuard.error}` });
|
|
127
125
|
|
|
@@ -132,7 +130,9 @@ export function createOrchestrateTool({ tool, pending }) {
|
|
|
132
130
|
const result = await runOrchestrate({
|
|
133
131
|
cwd, cycleDef: args.cycleDef, git, mint, finalize,
|
|
134
132
|
now: () => Date.now(),
|
|
135
|
-
lastResult: args.lastResult
|
|
133
|
+
lastResult: args.lastResult,
|
|
134
|
+
lastResults: args.lastResults,
|
|
135
|
+
baseBranch: args.baseBranch,
|
|
136
136
|
defaultModel: args.defaultModel,
|
|
137
137
|
}, io);
|
|
138
138
|
|
|
@@ -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,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.5.4] - 2026-05-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `lastResults` was handled by the orchestration engine but invisible to the tool interface — the schema, description, and execute bridge all omitted it, making the appraise consolidation path unreachable through `foundry_orchestrate`.
|
|
8
|
+
- `baseBranch` was computed by the engine but dropped at the tool boundary.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Clarified the commit model in the add-flow skill so agents do not re-check whether config file edits were committed.
|
|
13
|
+
|
|
14
|
+
## [3.5.3] - 2026-05-23
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Forge dispatch prompts now include explicit required-tool instructions with the actual output type filled in.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `readForgeFilePatterns` returns `{ patterns, outputType }` instead of a bare array.
|
|
27
|
+
- Build quality gate now writes a seal; `prepublishOnly` verifies the seal instead of re-running lint and tests.
|
|
28
|
+
|
|
3
29
|
## [3.5.2] - 2026-05-23
|
|
4
30
|
|
|
5
31
|
### 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
|
+
}
|
|
@@ -103,6 +103,8 @@ If the user rejects the plan, return to the Understand phase and adjust.
|
|
|
103
103
|
|
|
104
104
|
For each dependency, invoke the sub-skill's protocol with the captured context object. The context object for each sub-skill matches the args of the corresponding `foundry_config_create_*` tool, with fields populated from the Understand and Gather phases.
|
|
105
105
|
|
|
106
|
+
Each `foundry_config_create_*` tool commits every pending change under `foundry/`, not just the file it creates. If you edit a config file directly between tool calls (for example, to add appraiser configuration to an artefact type after those appraisers are created), the next `foundry_config_create_*` call picks it up. After the final tool call `git status` is always clean — no further checks are needed.
|
|
107
|
+
|
|
106
108
|
Build order (dependency order):
|
|
107
109
|
|
|
108
110
|
1. **Artefact types**: For each new artefact type, invoke the `add-artefact-type` protocol with the captured context. Example:
|
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.4",
|
|
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
|
}
|