@really-knows-ai/foundry 3.5.5 → 3.5.7
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/CHANGELOG.md +16 -0
- package/dist/scripts/lib/config-validators/cycle.js +10 -0
- package/dist/scripts/lib/config.js +12 -4
- package/dist/scripts/lib/sort-reason.js +55 -0
- package/dist/scripts/lib/sort-routing.js +2 -0
- package/dist/scripts/orchestrate-cycle.js +12 -0
- package/dist/scripts/orchestrate-phases.js +14 -4
- package/dist/scripts/orchestrate.js +9 -5
- package/dist/scripts/sort.js +7 -4
- package/dist/skills/add-cycle/SKILL.md +1 -1
- package/dist/skills/forge/SKILL.md +1 -1
- package/package.json +1 -1
package/dist/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.5.7] - 2026-05-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Artefact types can include an `example.md` alongside `definition.md`. When present, the forge skill treats its structure as normative — the forge agent follows the same format, preventing common formatting mistakes like unwanted title headings on haikus.
|
|
8
|
+
|
|
9
|
+
## [3.5.6] - 2026-05-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `deadlock-iterations` default changed from hardcoded 5 to the resolved `max-iterations` value, and defaults are clamped so deadlock is never unreachable. Deadlock validation rejects cycles where `deadlock-iterations > max-iterations` at setup time and cycle creation time.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Orchestrator routing responses now include a `reason` field explaining why sort chose the returned action (e.g. "found 1 unresolved feedback item(s) — routing to forge for revision (iteration 2 of 3)").
|
|
18
|
+
|
|
3
19
|
## [3.5.5] - 2026-05-23
|
|
4
20
|
|
|
5
21
|
### Fixed
|
|
@@ -29,6 +29,7 @@ export async function validate({ name, body, io }) {
|
|
|
29
29
|
await checkOutputType(fm, io),
|
|
30
30
|
...await checkInputs(fm, io),
|
|
31
31
|
...await checkTargets(fm, io),
|
|
32
|
+
checkIterationLimits(fm),
|
|
32
33
|
].filter(Boolean);
|
|
33
34
|
|
|
34
35
|
return errors.length ? { ok: false, errors } : { ok: true };
|
|
@@ -129,3 +130,12 @@ async function validateCycleRefs(targets, io) {
|
|
|
129
130
|
}
|
|
130
131
|
return errors;
|
|
131
132
|
}
|
|
133
|
+
|
|
134
|
+
function checkIterationLimits(fm) {
|
|
135
|
+
const maxIt = fm['max-iterations'];
|
|
136
|
+
const dlIt = fm['deadlock-iterations'];
|
|
137
|
+
if (maxIt !== undefined && dlIt !== undefined && dlIt > maxIt) {
|
|
138
|
+
return `deadlock-iterations (${dlIt}) must be <= max-iterations (${maxIt}); deadlock would never trigger before the cycle blocks`;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
@@ -22,12 +22,20 @@ export async function getCycleDefinition(foundryDir, cycleId, io) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function getArtefactType(foundryDir, typeId, io) {
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const dir = join(foundryDir, 'artefacts', typeId);
|
|
26
|
+
const defPath = join(dir, 'definition.md');
|
|
27
|
+
if (!(await io.exists(defPath))) {
|
|
27
28
|
throw new Error(`Artefact type not found: ${typeId}`);
|
|
28
29
|
}
|
|
29
|
-
const text = await io.readFile(
|
|
30
|
-
|
|
30
|
+
const text = await io.readFile(defPath);
|
|
31
|
+
const result = parseDoc(text);
|
|
32
|
+
|
|
33
|
+
const examplePath = join(dir, 'example.md');
|
|
34
|
+
if (await io.exists(examplePath)) {
|
|
35
|
+
result.example = (await io.readFile(examplePath)).trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/**
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { baseStage } from './sort-routing.js';
|
|
2
|
+
|
|
3
|
+
const REASON_HANDLERS = {
|
|
4
|
+
forge: forgeReason,
|
|
5
|
+
assay: (d) => `starting cycle — routing to assay`,
|
|
6
|
+
quench: () => 'routing to quench for deterministic validation',
|
|
7
|
+
appraise: appraiseReason,
|
|
8
|
+
'human-appraise': humanAppraiseReason,
|
|
9
|
+
done: () => 'all stages complete — no unresolved feedback',
|
|
10
|
+
blocked: blockedReason,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function reasonForRoute(route, prep) {
|
|
14
|
+
const data = buildReasonData(route, prep);
|
|
15
|
+
const handler = REASON_HANDLERS[data.base] || defaultReason;
|
|
16
|
+
return handler(data);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildReasonData(route, prep) {
|
|
20
|
+
const base = baseStage(route);
|
|
21
|
+
const forgeCount = prep.history.filter(e => baseStage(e.stage || '') === 'forge').length;
|
|
22
|
+
const maxIt = prep.defaults.maxIterations;
|
|
23
|
+
const feedback = prep.feedback || [];
|
|
24
|
+
const openCount = feedback.filter(
|
|
25
|
+
f => f.state !== 'resolved' && f.state !== 'deadlocked',
|
|
26
|
+
).length;
|
|
27
|
+
const dlCount = feedback.filter(f => f.state === 'deadlocked').length;
|
|
28
|
+
const needingForge = feedback.filter(
|
|
29
|
+
f => f.state === 'open' || f.state === 'rejected',
|
|
30
|
+
).length;
|
|
31
|
+
|
|
32
|
+
return { base, route, forgeCount, maxIt, openCount, dlCount, needingForge, anyDeadlocked: prep.anyDeadlocked };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function forgeReason(d) {
|
|
36
|
+
if (d.forgeCount === 0) return `starting cycle — routing to forge (iteration 1 of ${d.maxIt})`;
|
|
37
|
+
return `found ${d.needingForge} unresolved feedback item(s) — routing to forge for revision (iteration ${d.forgeCount + 1} of ${d.maxIt})`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function appraiseReason(d) {
|
|
41
|
+
if (d.anyDeadlocked) return `${d.dlCount} feedback item(s) deadlocked — routing to appraise for re-evaluation`;
|
|
42
|
+
return `quench passed with ${d.openCount} open feedback item(s) — routing to appraise`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function humanAppraiseReason(d) {
|
|
46
|
+
return `${d.dlCount} feedback item(s) deadlocked after ${d.forgeCount} forge iteration(s) — routing to human for override`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function blockedReason(d) {
|
|
50
|
+
return `max iterations (${d.maxIt}) reached after ${d.forgeCount} forge iteration(s) with ${d.openCount} unresolved feedback item(s)`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function defaultReason(d) {
|
|
54
|
+
return `routing to ${d.route}`;
|
|
55
|
+
}
|
|
@@ -272,3 +272,15 @@ export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns, o
|
|
|
272
272
|
);
|
|
273
273
|
return lines.join('\n');
|
|
274
274
|
}
|
|
275
|
+
|
|
276
|
+
export function checkIterationLimits(cfm, cycleId) {
|
|
277
|
+
const maxIt = cfm['max-iterations'];
|
|
278
|
+
const dlIt = cfm['deadlock-iterations'];
|
|
279
|
+
if (maxIt !== undefined && dlIt !== undefined && dlIt > maxIt) {
|
|
280
|
+
return violation(
|
|
281
|
+
`cycle ${cycleId}: deadlock-iterations (${dlIt}) cannot exceed max-iterations (${maxIt})`,
|
|
282
|
+
['WORK.md'],
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
tryCommit,
|
|
22
22
|
synthesizeStages,
|
|
23
23
|
renderDispatchPrompt,
|
|
24
|
+
checkIterationLimits,
|
|
24
25
|
} from './orchestrate-cycle.js';
|
|
25
26
|
import {
|
|
26
27
|
doneAction,
|
|
@@ -71,9 +72,15 @@ function getRouteBase(route) {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export async function handleSortResult(sortResult, ctx) {
|
|
74
|
-
const { route, model, token } = sortResult;
|
|
75
|
+
const { route, model, token, reason } = sortResult;
|
|
75
76
|
const routeBase = getRouteBase(route);
|
|
76
|
-
|
|
77
|
+
const result = await resolveRouteResult({ route, routeBase, model, token, ctx });
|
|
78
|
+
if (reason !== undefined) result.reason = reason;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resolveRouteResult({ route, routeBase, model, token, ctx }) {
|
|
83
|
+
if (isTerminalRoute(route)) return handleTerminalRoute(route, { route }, ctx);
|
|
77
84
|
if (routeBase === 'quench' || routeBase === 'appraise') return violation(routeBase + ' route reached handleSortResult');
|
|
78
85
|
if (routeBase === 'human-appraise') return humanAppraiseAction(route, token, ctx);
|
|
79
86
|
return buildDispatchAction(route, model, token, ctx);
|
|
@@ -159,10 +166,11 @@ function resolveStages(cfm, cycleId, hasValidation, assayExtractors) {
|
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
function applyFmDefaults(newFm, cfm, assayExtractors) {
|
|
162
|
-
|
|
169
|
+
const maxIt = cfm['max-iterations'] ?? 3;
|
|
170
|
+
newFm['max-iterations'] = maxIt;
|
|
163
171
|
newFm['human-appraise'] = cfm['human-appraise'] === true;
|
|
164
172
|
newFm['deadlock-appraise'] = cfm['deadlock-appraise'] !== false;
|
|
165
|
-
newFm['deadlock-iterations'] = cfm['deadlock-iterations'] ??
|
|
173
|
+
newFm['deadlock-iterations'] = cfm['deadlock-iterations'] ?? maxIt;
|
|
166
174
|
if (cfm.models) newFm.models = cfm.models;
|
|
167
175
|
if (assayExtractors) newFm.assay = { extractors: assayExtractors };
|
|
168
176
|
}
|
|
@@ -218,6 +226,8 @@ async function completeSetup(ctx) {
|
|
|
218
226
|
const hasValidation = ctx.lawsWithValidators && ctx.lawsWithValidators.length > 0;
|
|
219
227
|
const stagesResult = resolveStages(ctx.cfm, ctx.cycleId, hasValidation, ctx.assayResult.extractors);
|
|
220
228
|
if (stagesResult.error) return stagesResult.error;
|
|
229
|
+
const validityErr = checkIterationLimits(ctx.cfm, ctx.cycleId);
|
|
230
|
+
if (validityErr) return validityErr;
|
|
221
231
|
const newWork = buildNewFrontmatter(ctx.workContent, stagesResult, ctx.cfm, ctx.assayResult.extractors);
|
|
222
232
|
ctx.io.writeFile('WORK.md', newWork);
|
|
223
233
|
return trySetupCommit(ctx);
|
|
@@ -193,19 +193,23 @@ async function handleQuenchRoute(sortResult, preCheck, args, io) {
|
|
|
193
193
|
return dispatchByRoute(nextSort, args, preCheck, io);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
async function
|
|
197
|
-
writeStageRecord(io, preCheck.cycleId, sortResult.route);
|
|
198
|
-
const result = await gatherAppraiseContext(buildAppraiseCtx(preCheck.cycleId, args, io));
|
|
199
|
-
if (result.action === 'violation') { clearActiveStage(io); return result; }
|
|
196
|
+
async function dispatchAppraiseOrConsolidate(sortResult, preCheck, args, io, result) {
|
|
200
197
|
if (!result.tasks || result.tasks.length === 0) {
|
|
201
|
-
// No appraisers/artefacts — consolidate with empty results to advance
|
|
202
198
|
return handleAppraiseConsolidateRoute(sortResult, preCheck, { ...args, lastResults: [] }, io);
|
|
203
199
|
}
|
|
204
200
|
const validationErr = validateDispatchMulti(result);
|
|
205
201
|
if (validationErr) return validationErr;
|
|
202
|
+
if (sortResult.reason !== undefined) result.reason = sortResult.reason;
|
|
206
203
|
return result;
|
|
207
204
|
}
|
|
208
205
|
|
|
206
|
+
async function handleAppraiseGatherRoute(sortResult, preCheck, args, io) {
|
|
207
|
+
writeStageRecord(io, preCheck.cycleId, sortResult.route);
|
|
208
|
+
const result = await gatherAppraiseContext(buildAppraiseCtx(preCheck.cycleId, args, io));
|
|
209
|
+
if (result.action === 'violation') { clearActiveStage(io); return result; }
|
|
210
|
+
return dispatchAppraiseOrConsolidate(sortResult, preCheck, args, io, result);
|
|
211
|
+
}
|
|
212
|
+
|
|
209
213
|
async function handleAppraiseConsolidateRoute(sortResult, preCheck, args, io) {
|
|
210
214
|
const ctx = buildAppraiseCtx(preCheck.cycleId, args, io);
|
|
211
215
|
const result = await consolidateAppraise(ctx, args.lastResults);
|
package/dist/scripts/sort.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
findFirst,
|
|
23
23
|
determineRoute,
|
|
24
24
|
} from './lib/sort-routing.js';
|
|
25
|
+
import { reasonForRoute } from './lib/sort-reason.js';
|
|
25
26
|
import {
|
|
26
27
|
defaultIO,
|
|
27
28
|
checkModifiedFiles,
|
|
@@ -85,11 +86,12 @@ function validateWorkMd(workPath, io) {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function extractFrontmatterDefaults(frontmatter) {
|
|
89
|
+
const maxIt = frontmatter['max-iterations'] ?? 3;
|
|
88
90
|
return {
|
|
89
|
-
maxIterations:
|
|
91
|
+
maxIterations: maxIt,
|
|
90
92
|
humanAppraiseEnabled: frontmatter['human-appraise'] === true,
|
|
91
93
|
deadlockAppraise: frontmatter['deadlock-appraise'] !== false,
|
|
92
|
-
deadlockIterations: frontmatter['deadlock-iterations'] ??
|
|
94
|
+
deadlockIterations: frontmatter['deadlock-iterations'] ?? maxIt,
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -184,8 +186,8 @@ function checkModel(route, frontmatter, agentsDir, io, defaultModel) {
|
|
|
184
186
|
return { model: typeof modelResult === 'string' ? modelResult : null };
|
|
185
187
|
}
|
|
186
188
|
|
|
187
|
-
function mintToken({ route, model, mint, cycle, now, ulid }) {
|
|
188
|
-
const result = { route, ...(model ? { model } : {}) };
|
|
189
|
+
function mintToken({ route, model, mint, cycle, now, ulid, reason }) {
|
|
190
|
+
const result = { route, ...(model ? { model } : {}), reason };
|
|
189
191
|
if (mint && isDispatchableRoute(route)) {
|
|
190
192
|
const token = mint({ route, cycle, exp: now + 10 * 60 * 1000, nonce: ulid(now) });
|
|
191
193
|
if (token) result.token = token;
|
|
@@ -268,6 +270,7 @@ export function runSort(args = {}, io = defaultIO) {
|
|
|
268
270
|
|
|
269
271
|
return mintToken({
|
|
270
272
|
route, model: modelCheck.model, mint: opts.mint, cycle: prep.cycle, now: opts.now, ulid: opts.ulid,
|
|
273
|
+
reason: reasonForRoute(route, prep),
|
|
271
274
|
});
|
|
272
275
|
}
|
|
273
276
|
|
|
@@ -85,7 +85,7 @@ If the parent flow or required artefact type is missing and the user's goal clea
|
|
|
85
85
|
**Optional clusters** — After each cluster, ask whether the user wants to configure it; if not, skip:
|
|
86
86
|
|
|
87
87
|
- **Routing**: `inputs` (input contract: `{type: "any-of"|"all-of", artefacts: string[]}`; omit for source cycles with no upstream artefact dependency), `targets` (cycle IDs to route to after completion), `maxIterations` (maximum iterations before forced progression)
|
|
88
|
-
- **Human-appraise**: `humanAppraise` (boolean, default false) — human reviews every iteration; `deadlockAppraise` (boolean, default true) — human is pulled in when LLM appraisers deadlock; `deadlockIterations` (number,
|
|
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, defaults to `max-iterations` value) — deadlock threshold. Only applies when either appraise is enabled.
|
|
89
89
|
- **Memory and models**: `assay` (assay configuration), `memory` (memory configuration), `models` (stage-specific model overrides, e.g. `{forge: "openai/gpt-4o", appraise: "openai/gpt-4o"}`). For models, offer each stage (forge, quench, appraise) individually. If the user has no preference, omit the `models` map and use the session defaults.
|
|
90
90
|
|
|
91
91
|
### 2. Plan
|
|
@@ -40,7 +40,7 @@ Forge runs inside an enforced stage. Your **first** and **last** tool calls are
|
|
|
40
40
|
|
|
41
41
|
Then return control to the user and stop.
|
|
42
42
|
3. `foundry_config_cycle` — understand what to produce and what inputs are available.
|
|
43
|
-
4. `foundry_config_artefact_type` with the output type ID — get the artefact type definition,
|
|
43
|
+
4. `foundry_config_artefact_type` with the output type ID — get the artefact type definition, `file-patterns`, and any example. When the response includes an `example` field, its structure is normative — your output must follow the same format (no extra headings, metadata blocks, or free-form prose that the example does not include).
|
|
44
44
|
5. `foundry_config_laws` — get all applicable laws (global + type-specific).
|
|
45
45
|
6. If the cycle declares `inputs`, discover input files by filesystem scan:
|
|
46
46
|
- For each type listed in `inputs`, call `foundry_config_artefact_type` to get its `file-patterns`.
|
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.7",
|
|
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",
|