@really-knows-ai/foundry 1.3.1 → 1.4.0

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.
@@ -13,7 +13,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync } from
13
13
  import { fileURLToPath } from 'url';
14
14
  import { tool } from '@opencode-ai/plugin';
15
15
  import { loadHistory, appendEntry, getIteration } from '../../scripts/lib/history.js';
16
- import { parseFrontmatter, createWorkfile, setFrontmatterField, getFrontmatterField } from '../../scripts/lib/workfile.js';
16
+ import { parseFrontmatter, createWorkfile, setFrontmatterField, getFrontmatterField, enrichStages } from '../../scripts/lib/workfile.js';
17
17
  import { parseArtefactsTable, addArtefactRow, setArtefactStatus } from '../../scripts/lib/artefacts.js';
18
18
  import { addFeedbackItem, actionFeedbackItem, wontfixFeedbackItem, resolveFeedbackItem, listFeedback } from '../../scripts/lib/feedback.js';
19
19
  import { getCycleDefinition, getArtefactType, getLaws, getValidation, getAppraisers, getFlow, selectAppraisers } from '../../scripts/lib/config.js';
@@ -132,15 +132,17 @@ export const FoundryPlugin = async ({ directory }) => {
132
132
  stages: tool.schema.array(tool.schema.string()).describe('Ordered stage names'),
133
133
  maxIterations: tool.schema.number().describe('Maximum iterations'),
134
134
  goal: tool.schema.string().describe('Goal text'),
135
- models: tool.schema.record(tool.schema.string()).optional().describe('Per-stage model overrides'),
135
+ models: tool.schema.string().optional().describe('Per-stage model overrides as JSON object, e.g. \'{"forge":"openai/gpt-4o"}\''),
136
136
  },
137
137
  async execute(args, context) {
138
138
  const workPath = path.join(context.worktree, 'WORK.md');
139
139
  if (existsSync(workPath)) {
140
140
  return JSON.stringify({ error: 'WORK.md already exists' });
141
141
  }
142
- const fm = { flow: args.flow, cycle: args.cycle, stages: args.stages, maxIterations: args.maxIterations };
143
- if (args.models) fm.models = args.models;
142
+ const fm = { flow: args.flow, cycle: args.cycle, stages: enrichStages(args.stages, args.cycle), maxIterations: args.maxIterations };
143
+ if (args.models) {
144
+ try { fm.models = JSON.parse(args.models); } catch { fm.models = {}; }
145
+ }
144
146
  const content = createWorkfile(fm, args.goal);
145
147
  writeFileSync(workPath, content, 'utf-8');
146
148
  return JSON.stringify({ ok: true });
@@ -167,7 +169,7 @@ export const FoundryPlugin = async ({ directory }) => {
167
169
  description: 'Update a single frontmatter field in WORK.md',
168
170
  args: {
169
171
  key: tool.schema.string().describe('Frontmatter key'),
170
- value: tool.schema.any().describe('Value to set'),
172
+ value: tool.schema.string().describe('Value to set (use JSON for arrays/objects, e.g. \'["forge:a","quench:b"]\' or \'{"forge":"openai/gpt-4o"}\')'),
171
173
  },
172
174
  async execute(args, context) {
173
175
  const workPath = path.join(context.worktree, 'WORK.md');
@@ -175,7 +177,24 @@ export const FoundryPlugin = async ({ directory }) => {
175
177
  return JSON.stringify({ error: 'WORK.md not found' });
176
178
  }
177
179
  const text = readFileSync(workPath, 'utf-8');
178
- const updated = setFrontmatterField(text, args.key, args.value);
180
+ // Parse JSON values for arrays/objects, keep strings as-is
181
+ let value = args.value;
182
+ try {
183
+ const parsed = JSON.parse(args.value);
184
+ if (typeof parsed === 'object' || Array.isArray(parsed) || typeof parsed === 'number') {
185
+ value = parsed;
186
+ }
187
+ } catch {
188
+ // Not JSON, use as plain string
189
+ }
190
+ // Auto-enrich bare stage names with cycle ID alias
191
+ if (args.key === 'stages' && Array.isArray(value)) {
192
+ const fm = parseFrontmatter(text);
193
+ if (fm.cycle) {
194
+ value = enrichStages(value, fm.cycle);
195
+ }
196
+ }
197
+ const updated = setFrontmatterField(text, args.key, value);
179
198
  writeFileSync(workPath, updated, 'utf-8');
180
199
  return JSON.stringify({ ok: true });
181
200
  },
@@ -448,15 +467,15 @@ export const FoundryPlugin = async ({ directory }) => {
448
467
  async execute(args, context) {
449
468
  const io = makeIO(context.worktree);
450
469
  const commands = await getValidation('foundry', args.typeId, io);
451
- if (!commands) return JSON.stringify({ error: 'No validation defined for type: ' + args.typeId });
470
+ if (!commands || commands.length === 0) return JSON.stringify({ error: 'No validation defined for type: ' + args.typeId });
452
471
  const results = [];
453
- for (const cmd of commands) {
454
- const expanded = cmd.replace(/\{file\}/g, args.file);
472
+ for (const entry of commands) {
473
+ const expanded = entry.command.replace(/\{file\}/g, args.file);
455
474
  try {
456
475
  const output = execSync(expanded, { cwd: context.worktree, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
457
- results.push({ command: expanded, passed: true, output: output.trim() });
476
+ results.push({ id: entry.id, command: expanded, passed: true, output: output.trim() });
458
477
  } catch (err) {
459
- results.push({ command: expanded, passed: false, output: (err.stderr || err.stdout || err.message || '').trim() });
478
+ results.push({ id: entry.id, command: expanded, passed: false, output: (err.stderr || err.stdout || err.message || '').trim(), failureMeans: entry.failureMeans });
460
479
  }
461
480
  }
462
481
  return JSON.stringify(results);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@really-knows-ai/foundry",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "A structured framework for AI-driven artefact creation with deterministic routing, quality gates, and iterative refinement cycles.",
5
5
  "type": "module",
6
6
  "main": ".opencode/plugins/foundry.js",
@@ -89,16 +89,37 @@ export async function getValidation(foundryDir, typeId, io) {
89
89
  return null;
90
90
  }
91
91
  const text = await io.readFile(path);
92
- const commands = [];
93
- const regex = /```(?:bash|sh)\n([\s\S]*?)```/g;
94
- let match;
95
- while ((match = regex.exec(text)) !== null) {
96
- for (const line of match[1].trim().split('\n')) {
97
- const trimmed = line.trim();
98
- if (trimmed) commands.push(trimmed);
92
+ const entries = [];
93
+ const lines = text.split('\n');
94
+ let currentId = null;
95
+ let currentCommand = null;
96
+ let currentFailure = null;
97
+
98
+ function flush() {
99
+ if (currentId && currentCommand) {
100
+ const entry = { id: currentId, command: currentCommand };
101
+ if (currentFailure) entry.failureMeans = currentFailure;
102
+ entries.push(entry);
99
103
  }
104
+ currentId = null;
105
+ currentCommand = null;
106
+ currentFailure = null;
100
107
  }
101
- return commands;
108
+
109
+ for (const line of lines) {
110
+ const heading = line.match(/^## (.+)/);
111
+ if (heading) {
112
+ flush();
113
+ currentId = heading[1].trim();
114
+ } else if (currentId) {
115
+ const cmdMatch = line.match(/^Command:\s*(.+)/);
116
+ const failMatch = line.match(/^Failure means:\s*(.+)/);
117
+ if (cmdMatch) currentCommand = cmdMatch[1].trim();
118
+ if (failMatch) currentFailure = failMatch[1].trim();
119
+ }
120
+ }
121
+ flush();
122
+ return entries;
102
123
  }
103
124
 
104
125
  export async function getAppraisers(foundryDir, io) {
@@ -38,6 +38,7 @@ export function parseFeedback(text, cycle, artefacts) {
38
38
  cycleFiles.add(art.file || '');
39
39
  }
40
40
  }
41
+ const filterByFile = cycleFiles.size > 0;
41
42
 
42
43
  const items = [];
43
44
  let currentFile = null;
@@ -71,7 +72,7 @@ export function parseFeedback(text, cycle, artefacts) {
71
72
  continue;
72
73
  }
73
74
 
74
- if (cycleFiles.has(currentFile) && /^- \[/.test(stripped)) {
75
+ if ((!filterByFile || cycleFiles.has(currentFile)) && /^- \[/.test(stripped)) {
75
76
  items.push(parseFeedbackItem(stripped));
76
77
  }
77
78
  }
@@ -183,6 +184,7 @@ export function listFeedback(text, cycle, artefacts, filterFile) {
183
184
  cycleFiles.add(art.file || '');
184
185
  }
185
186
  }
187
+ const filterByCycle = cycleFiles.size > 0;
186
188
 
187
189
  const results = [];
188
190
  let currentFile = null;
@@ -216,7 +218,7 @@ export function listFeedback(text, cycle, artefacts, filterFile) {
216
218
  continue;
217
219
  }
218
220
 
219
- if (cycleFiles.has(currentFile) && /^- \[/.test(stripped)) {
221
+ if ((!filterByCycle || cycleFiles.has(currentFile)) && /^- \[/.test(stripped)) {
220
222
  if (!filterFile || filterFile === currentFile) {
221
223
  const item = parseFeedbackItem(stripped);
222
224
  results.push({
@@ -34,6 +34,19 @@ export function setFrontmatterField(text, key, value) {
34
34
  return body ? `${fmBlock}\n${body}` : fmBlock;
35
35
  }
36
36
 
37
+ // ---------------------------------------------------------------------------
38
+ // Stage alias enrichment
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Ensure each stage has a base:alias format.
43
+ * Bare names (e.g. "forge") become "forge:<cycleId>".
44
+ * Already-aliased names (e.g. "forge:write-haiku") pass through unchanged.
45
+ */
46
+ export function enrichStages(stages, cycleId) {
47
+ return stages.map(s => s.includes(':') ? s : `${s}:${cycleId}`);
48
+ }
49
+
37
50
  // ---------------------------------------------------------------------------
38
51
  // Workfile creation
39
52
  // ---------------------------------------------------------------------------
@@ -45,8 +58,8 @@ export function createWorkfile(frontmatter, goal) {
45
58
 
46
59
  ${goal}
47
60
 
48
- | Artefact | Status |
49
- |----------|--------|
61
+ | File | Type | Cycle | Status |
62
+ |------|------|-------|--------|
50
63
 
51
64
  ## Feedback
52
65
  `;
@@ -128,6 +128,11 @@ If yes, walk through each validation entry:
128
128
  - A `Command:` line with `{file}` placeholder
129
129
  - A `Failure means:` line explaining what a non-zero exit indicates
130
130
 
131
+ If the user wants validation scripts (not just inline commands), create them as separate files in the artefact type directory. Check the project's `package.json` for `"type": "module"`:
132
+ - If ESM (`"type": "module"`): use `import` syntax, or name scripts with `.mjs` extension
133
+ - If CommonJS (no `"type"` field or `"type": "commonjs"`): `require()` is fine, or use `.cjs` extension
134
+ - When in doubt, use `.mjs` or `.cjs` extensions to be explicit regardless of project settings
135
+
131
136
  ### 8. Scaffold
132
137
 
133
138
  Create the directory and files:
@@ -23,6 +23,7 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
23
23
  - Use the cycle definition's `stages` field if present
24
24
  - Otherwise generate defaults: always `forge`, add `quench` if `foundry_config_validation` returns non-null for the type, always `appraise`
25
25
  - Cycle definitions can include `hitl` entries for human-in-the-loop checkpoints
26
+ - Stages should use `base:alias` format (e.g. `forge:write-haiku`, `quench:check-syllables`). If you pass bare names, the tool will auto-append the cycle ID as the alias.
26
27
  4. Call `foundry_workfile_set` to configure the work file:
27
28
  - `key: "cycle"`, `value: <cycle-id>`
28
29
  - `key: "stages"`, `value: <determined stages list>`