@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.
@@ -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
- return JSON.stringify(await invoke(args, io));
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; call with lastResult={ok,error?} after a dispatch/human_appraise completes. Returns {action, ...} describing what the caller should do next.',
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 ?? null,
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 foundry_stage_begin --
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
- timestamp: nowIso, persist,
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
- return await fetchFilePatterns(output, io);
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
- export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns }) {
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 ${stage.split(':')[0]} skill and follow its instructions exactly.`,
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
- async function findOutputArtefacts(cfm, io, foundryDir, baseBranch) {
29
- const outputType = cfm ? cfm['output-type'] : undefined;
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
- const filePatterns = base === 'forge' ? await readForgeFilePatterns(ctx.cycleId, ctx.io) : null;
82
- return { action: 'dispatch', stage: route, subagent_type: model, prompt: renderDispatchPrompt(makeDispatchPayload(route, ctx.cycleId, token, ctx.cwd, filePatterns)) };
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
- const forgeFilePatterns = stageBase === 'forge' ? (await readForgeFilePatterns(cycleId, io)) ?? [] : [];
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.2",
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
  }