@really-knows-ai/foundry 3.6.1 → 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,13 @@
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
+
3
11
  ## [3.6.1] - 2026-05-25
4
12
 
5
13
  ### Fixed
@@ -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
+ }
@@ -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 items = openFeedbackStore('WORK.feedback.yaml', io).list();
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
- io.writeFile(FORGE_CTX, JSON.stringify({ forgePreVersion: preVersion, forgeItems: items.map(i => ({ id: i.id })) }));
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.1",
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",