@really-knows-ai/foundry 3.6.0 → 3.6.2
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
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.6.2] - 2026-05-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `captureForgeContext` now only captures unresolved feedback items (`open`/`rejected`) instead of all items. Previously it captured resolved items too, causing `enforceForgeContract` to reject them and `revertAll` to force-reopen the entire batch — creating an infinite forge loop.
|
|
8
|
+
- Extracted validation guard functions into `src/scripts/lib/orchestrate-guards.js` to stay within the `max-lines` limit.
|
|
9
|
+
- Fixed a missing `isDuplicateConsolidation` function that was referenced but never defined.
|
|
10
|
+
|
|
11
|
+
## [3.6.1] - 2026-05-25
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Forge contract enforcement now correctly handles the first forge run with no prior feedback items (empty batch). Previously the contract failed with "forge changed artefacts but did not mark any feedback as actioned", creating a system feedback item that cascaded into an infinite forge loop.
|
|
16
|
+
- Forge history entries now include the actual list of changed files detected by git diff, instead of showing an empty `changed_files: []` for every entry.
|
|
17
|
+
|
|
3
18
|
## [3.6.0] - 2026-05-25
|
|
4
19
|
|
|
5
20
|
### Added
|
|
@@ -76,15 +76,24 @@ function checkBatchVersion(items, feedbackStore, cycleId, postVersion, preVersio
|
|
|
76
76
|
/**
|
|
77
77
|
* Enforce the forge contract on a batch of items presented to forge.
|
|
78
78
|
*
|
|
79
|
-
*
|
|
79
|
+
* When `items` is not a non-empty array (null, undefined, or []), the
|
|
80
|
+
* contract passes immediately without side-effects. This covers the
|
|
81
|
+
* initial forge run where no feedback exists yet.
|
|
82
|
+
*
|
|
83
|
+
* Two-level check when items are present:
|
|
80
84
|
* 1. Per-item: every item must end in 'actioned' or 'wont-fix'.
|
|
81
85
|
* 2. Batch-level: artefact version semantics must be consistent.
|
|
82
86
|
*
|
|
83
|
-
* @param {{ items
|
|
84
|
-
*
|
|
87
|
+
* @param {{ items?: Array<{id: string}> | null, preVersion: string,
|
|
88
|
+
* postVersion: string, feedbackStore: object, cycleId: string }} params
|
|
85
89
|
* @returns {{ contractPassed: boolean }}
|
|
86
90
|
*/
|
|
87
91
|
export function enforceForgeContract({ items, preVersion, postVersion, feedbackStore, cycleId }) {
|
|
92
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
93
|
+
// Empty batch means forge had no prior feedback to respond to.
|
|
94
|
+
// This is the first forge run or all items were already resolved.
|
|
95
|
+
return { contractPassed: true };
|
|
96
|
+
}
|
|
88
97
|
if (!checkPerItemResponse(items, feedbackStore, cycleId, postVersion)) {
|
|
89
98
|
return { contractPassed: false };
|
|
90
99
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Foundry v2.3.0 orchestrate guards: validation guards for cycle orchestration.
|
|
2
|
+
|
|
3
|
+
import { parseFrontmatter } from './workfile.js';
|
|
4
|
+
import { stageBaseOf } from './stage-guard.js';
|
|
5
|
+
import { violation } from '../orchestrate-cycle.js';
|
|
6
|
+
|
|
7
|
+
function isDuplicateConsolidation(lastStage, activeStage) {
|
|
8
|
+
return lastStage && lastStage.stage === activeStage.stage;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function guardNoWorkMd(io) {
|
|
12
|
+
if (!io.exists('WORK.md')) return violation('no WORK.md; flow skill must create it first');
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function guardMissingCycleId(io) {
|
|
17
|
+
const workContent = io.readFile('WORK.md');
|
|
18
|
+
const fm = parseFrontmatter(workContent);
|
|
19
|
+
if (!fm.cycle) return violation('WORK.md frontmatter missing cycle field', ['WORK.md']);
|
|
20
|
+
return { cycleId: fm.cycle, workContent };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function guardSetupInconsistent(lastResult) {
|
|
24
|
+
if (lastResult) return violation('inconsistent state: lastResult provided but WORK.md still needs setup', ['WORK.md']);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function guardOrphanedStage(activeStage, lastResult) {
|
|
29
|
+
if (activeStage && !lastResult) {
|
|
30
|
+
return violation(
|
|
31
|
+
`prior stage ${activeStage.stage} orphaned — no lastResult provided but active stage exists. ` +
|
|
32
|
+
`Likely cause: previous orchestrate call returned dispatch but caller did not follow up.`,
|
|
33
|
+
[],
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function guardMissingLastStage(lastStage) {
|
|
40
|
+
if (!lastStage) return violation('lastResult provided but no last stage recorded — orphaned state');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function checkLastResultsConflict(args) {
|
|
45
|
+
if (args.lastResult !== undefined && args.lastResults !== undefined) return violation('lastResult and lastResults are mutually exclusive');
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function checkLastResultsShape(args) {
|
|
50
|
+
if (args.lastResults === undefined) return null;
|
|
51
|
+
if (!Array.isArray(args.lastResults)) return violation('lastResults must be an array');
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkLastResultsStageContext(args, activeStage, lastStage) {
|
|
56
|
+
if (args.lastResults === undefined) return null;
|
|
57
|
+
if (!activeStage) return violation('lastResults provided but no active stage exists');
|
|
58
|
+
if (stageBaseOf(activeStage.stage) !== 'appraise') return violation(`lastResults provided but active stage "${activeStage.stage}" is not an appraise stage`);
|
|
59
|
+
if (isDuplicateConsolidation(lastStage, activeStage)) return violation(`duplicate lastResults: consolidation already completed for this appraise stage "${activeStage.stage}"`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function guardLastResults(args, activeStage, lastStage) {
|
|
64
|
+
return checkLastResultsConflict(args)
|
|
65
|
+
?? checkLastResultsShape(args)
|
|
66
|
+
?? checkLastResultsStageContext(args, activeStage, lastStage);
|
|
67
|
+
}
|
|
@@ -307,7 +307,9 @@ export async function finaliseStage(args) {
|
|
|
307
307
|
const iteration = getIteration(historyPath, cycleId, io);
|
|
308
308
|
const openFeedback = computeOpenFeedback(io);
|
|
309
309
|
writeHistoryEntries({
|
|
310
|
-
historyPath, cycleId,
|
|
310
|
+
historyPath, cycleId,
|
|
311
|
+
lastStage: { ...lastStage, changedFiles: finalizeResult.changedFiles },
|
|
312
|
+
iteration, openFeedback, io,
|
|
311
313
|
artefactVersion: postVersion, contractPassed,
|
|
312
314
|
});
|
|
313
315
|
const commitErr = await tryStageCommit(git, lastStage, cycleId, io);
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// into a single entry point the LLM drives via a 3-line loop.
|
|
4
4
|
|
|
5
5
|
import { runSort } from './sort.js';
|
|
6
|
-
import { parseFrontmatter } from './lib/workfile.js';
|
|
7
6
|
import matter from 'gray-matter';
|
|
8
7
|
import { readActiveStage, readLastStage, writeActiveStage, clearActiveStage } from './lib/state.js';
|
|
9
8
|
import { stageBaseOf } from './lib/stage-guard.js';
|
|
@@ -32,6 +31,7 @@ import {
|
|
|
32
31
|
import { runQuench } from './quench-module.js';
|
|
33
32
|
import { gatherAppraiseContext, consolidateAppraise } from './appraise-module.js';
|
|
34
33
|
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
34
|
+
import { guardNoWorkMd, guardMissingCycleId, guardSetupInconsistent, guardOrphanedStage, guardMissingLastStage, guardLastResults } from './lib/orchestrate-guards.js';
|
|
35
35
|
|
|
36
36
|
export {
|
|
37
37
|
renderDispatchPrompt, synthesizeStages, computeOpenFeedback,
|
|
@@ -50,68 +50,6 @@ export function needsSetup(workMdContent) {
|
|
|
50
50
|
// Main entry point
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
52
|
|
|
53
|
-
function guardNoWorkMd(io) {
|
|
54
|
-
if (!io.exists('WORK.md')) return violation('no WORK.md; flow skill must create it first');
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function guardMissingCycleId(io) {
|
|
59
|
-
const workContent = io.readFile('WORK.md');
|
|
60
|
-
const fm = parseFrontmatter(workContent);
|
|
61
|
-
if (!fm.cycle) return violation('WORK.md frontmatter missing cycle field', ['WORK.md']);
|
|
62
|
-
return { cycleId: fm.cycle, workContent };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function guardSetupInconsistent(lastResult) {
|
|
66
|
-
if (lastResult) return violation('inconsistent state: lastResult provided but WORK.md still needs setup', ['WORK.md']);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function guardOrphanedStage(activeStage, lastResult) {
|
|
71
|
-
if (activeStage && !lastResult) {
|
|
72
|
-
return violation(
|
|
73
|
-
`prior stage ${activeStage.stage} orphaned — no lastResult provided but active stage exists. ` +
|
|
74
|
-
`Likely cause: previous orchestrate call returned dispatch but caller did not follow up.`,
|
|
75
|
-
[],
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function guardMissingLastStage(lastStage) {
|
|
82
|
-
if (!lastStage) return violation('lastResult provided but no last stage recorded — orphaned state');
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function checkLastResultsConflict(args) {
|
|
87
|
-
if (args.lastResult !== undefined && args.lastResults !== undefined) return violation('lastResult and lastResults are mutually exclusive');
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function checkLastResultsShape(args) {
|
|
92
|
-
if (args.lastResults === undefined) return null;
|
|
93
|
-
if (!Array.isArray(args.lastResults)) return violation('lastResults must be an array');
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function isDuplicateConsolidation(lastStage, activeStage) {
|
|
98
|
-
return lastStage && lastStage.stage === activeStage.stage;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function checkLastResultsStageContext(args, activeStage, lastStage) {
|
|
102
|
-
if (args.lastResults === undefined) return null;
|
|
103
|
-
if (!activeStage) return violation('lastResults provided but no active stage exists');
|
|
104
|
-
if (stageBaseOf(activeStage.stage) !== 'appraise') return violation(`lastResults provided but active stage "${activeStage.stage}" is not an appraise stage`);
|
|
105
|
-
if (isDuplicateConsolidation(lastStage, activeStage)) return violation(`duplicate lastResults: consolidation already completed for this appraise stage "${activeStage.stage}"`);
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function guardLastResults(args, activeStage, lastStage) {
|
|
110
|
-
return checkLastResultsConflict(args)
|
|
111
|
-
?? checkLastResultsShape(args)
|
|
112
|
-
?? checkLastResultsStageContext(args, activeStage, lastStage);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
53
|
function buildSortArgs(args, now) {
|
|
116
54
|
return {
|
|
117
55
|
cycleDef: args.cycleDef ?? null, mint: args.mint,
|
|
@@ -196,9 +134,16 @@ async function captureForgeContext(sortResult, args, preCheck, io) {
|
|
|
196
134
|
const fgResult = await readForgeFilePatterns(preCheck.cycleId, io);
|
|
197
135
|
if (!fgResult) return;
|
|
198
136
|
const preVersion = await computeArtefactVersion('foundry', fgResult.outputType, io, args.cwd);
|
|
199
|
-
const
|
|
137
|
+
const allItems = openFeedbackStore('WORK.feedback.yaml', io).list();
|
|
138
|
+
// Only capture unresolved items (open/rejected) — resolved items are terminal
|
|
139
|
+
// and presenting them to forge causes the contract to fail and revert them.
|
|
140
|
+
const unresolvedItems = allItems.filter(item => {
|
|
141
|
+
const state = item.history?.[0]?.state ?? 'open';
|
|
142
|
+
return state === 'open' || state === 'rejected';
|
|
143
|
+
});
|
|
200
144
|
if (!io.exists('.foundry')) io.mkdir('.foundry');
|
|
201
|
-
|
|
145
|
+
const ctx = { forgePreVersion: preVersion, forgeItems: unresolvedItems.map(i => ({ id: i.id })) };
|
|
146
|
+
io.writeFile(FORGE_CTX, JSON.stringify(ctx));
|
|
202
147
|
}
|
|
203
148
|
|
|
204
149
|
function countConsecutiveForgeFailures(io, cycleId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@really-knows-ai/foundry",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.2",
|
|
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",
|