@really-knows-ai/foundry 3.6.1 → 3.6.3

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.6.3] - 2026-05-26
4
+
5
+ ### Fixed
6
+
7
+ - Validator script crashes now surface a single clear error ("Validator produced no valid output — check the script for syntax errors") instead of 20+ individual "Invalid JSON" parse errors from the stack trace.
8
+
9
+ ## [3.6.2] - 2026-05-25
10
+
11
+ ### Fixed
12
+
13
+ - `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.
14
+ - Extracted validation guard functions into `src/scripts/lib/orchestrate-guards.js` to stay within the `max-lines` limit.
15
+ - Fixed a missing `isDuplicateConsolidation` function that was referenced but never defined.
16
+
3
17
  ## [3.6.1] - 2026-05-25
4
18
 
5
19
  ### 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
+ }
@@ -197,6 +197,23 @@ export function collectValidatorResult(parseResult, lawId, validatorId, results)
197
197
  }
198
198
  }
199
199
 
200
+ /**
201
+ * If every line of output was unparseable, the validator script itself is
202
+ * broken (syntax error, runtime crash, etc.). Replace the noise of 20+
203
+ * individual "Invalid JSON" errors with a single actionable message.
204
+ */
205
+ export function checkForValidatorCrash(result) {
206
+ if (result.items.length === 0 && result.parseErrors.length > 0) {
207
+ return {
208
+ ...result,
209
+ parseErrors: [
210
+ `Validator produced no valid output (${result.parseErrors.length} unparseable lines). Check the validator script for syntax errors.`,
211
+ ],
212
+ };
213
+ }
214
+ return result;
215
+ }
216
+
200
217
  /**
201
218
  * Execute a validator command and parse its JSONL output.
202
219
  */
@@ -209,14 +226,18 @@ export async function executeValidator(expanded, worktree, patterns) {
209
226
  });
210
227
  const { Readable } = await import('stream');
211
228
  const stream = Readable.from([output]);
212
- return await parseValidatorJsonl(stream, patterns);
229
+ return checkForValidatorCrash(
230
+ await parseValidatorJsonl(stream, patterns),
231
+ );
213
232
  } catch (err) {
214
233
  // Validator command failed — prefer stdout for JSONL
215
234
  // (tools like rg exit 1 with results on stdout)
216
235
  const output = (err.stdout || err.stderr || err.message || '').trim();
217
236
  const { Readable } = await import('stream');
218
237
  const stream = Readable.from([output]);
219
- return await parseValidatorJsonl(stream, patterns);
238
+ return checkForValidatorCrash(
239
+ await parseValidatorJsonl(stream, patterns),
240
+ );
220
241
  }
221
242
  }
222
243
 
@@ -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.3",
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",