@really-knows-ai/foundry 1.3.2 → 1.5.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, parseStagesValue, parseModelsValue } 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';
@@ -139,9 +139,9 @@ export const FoundryPlugin = async ({ directory }) => {
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 };
142
+ const fm = { flow: args.flow, cycle: args.cycle, stages: enrichStages(args.stages, args.cycle), maxIterations: args.maxIterations };
143
143
  if (args.models) {
144
- try { fm.models = JSON.parse(args.models); } catch { fm.models = {}; }
144
+ fm.models = parseModelsValue(args.models);
145
145
  }
146
146
  const content = createWorkfile(fm, args.goal);
147
147
  writeFileSync(workPath, content, 'utf-8');
@@ -179,13 +179,28 @@ export const FoundryPlugin = async ({ directory }) => {
179
179
  const text = readFileSync(workPath, 'utf-8');
180
180
  // Parse JSON values for arrays/objects, keep strings as-is
181
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;
182
+ if (args.key === 'stages') {
183
+ // Always parse stages into an array (handles JSON arrays and comma-separated strings)
184
+ value = parseStagesValue(args.value);
185
+ } else if (args.key === 'models') {
186
+ // Always parse models into an object (handles JSON objects and "key: value" strings)
187
+ value = parseModelsValue(args.value);
188
+ } else {
189
+ try {
190
+ const parsed = JSON.parse(args.value);
191
+ if (typeof parsed === 'object' || Array.isArray(parsed) || typeof parsed === 'number') {
192
+ value = parsed;
193
+ }
194
+ } catch {
195
+ // Not JSON, use as plain string
196
+ }
197
+ }
198
+ // Auto-enrich bare stage names with cycle ID alias
199
+ if (args.key === 'stages' && Array.isArray(value)) {
200
+ const fm = parseFrontmatter(text);
201
+ if (fm.cycle) {
202
+ value = enrichStages(value, fm.cycle);
186
203
  }
187
- } catch {
188
- // Not JSON, use as plain string
189
204
  }
190
205
  const updated = setFrontmatterField(text, args.key, value);
191
206
  writeFileSync(workPath, updated, 'utf-8');
@@ -460,15 +475,15 @@ export const FoundryPlugin = async ({ directory }) => {
460
475
  async execute(args, context) {
461
476
  const io = makeIO(context.worktree);
462
477
  const commands = await getValidation('foundry', args.typeId, io);
463
- if (!commands) return JSON.stringify({ error: 'No validation defined for type: ' + args.typeId });
478
+ if (!commands || commands.length === 0) return JSON.stringify({ error: 'No validation defined for type: ' + args.typeId });
464
479
  const results = [];
465
- for (const cmd of commands) {
466
- const expanded = cmd.replace(/\{file\}/g, args.file);
480
+ for (const entry of commands) {
481
+ const expanded = entry.command.replace(/\{file\}/g, args.file);
467
482
  try {
468
483
  const output = execSync(expanded, { cwd: context.worktree, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
469
- results.push({ command: expanded, passed: true, output: output.trim() });
484
+ results.push({ id: entry.id, command: expanded, passed: true, output: output.trim() });
470
485
  } catch (err) {
471
- results.push({ command: expanded, passed: false, output: (err.stderr || err.stdout || err.message || '').trim() });
486
+ results.push({ id: entry.id, command: expanded, passed: false, output: (err.stderr || err.stdout || err.message || '').trim(), failureMeans: entry.failureMeans });
472
487
  }
473
488
  }
474
489
  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.2",
3
+ "version": "1.5.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().replace(/^`|`$/g, '');
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,58 @@ 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
+
50
+ /**
51
+ * Parse a stages value from tool input.
52
+ * Accepts JSON array string or comma-separated string.
53
+ * Always returns an array of trimmed, non-empty strings.
54
+ */
55
+ export function parseStagesValue(raw) {
56
+ // Try JSON first
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ if (Array.isArray(parsed)) return parsed;
60
+ } catch { /* not JSON */ }
61
+ // Fall back to comma-separated
62
+ return raw.split(',').map(s => s.trim()).filter(Boolean);
63
+ }
64
+
65
+ /**
66
+ * Parse a models value from tool input.
67
+ * Accepts JSON object string or "key: value, key: value" string.
68
+ * Always returns an object mapping stage base names to model IDs.
69
+ */
70
+ export function parseModelsValue(raw) {
71
+ if (!raw || !raw.trim()) return {};
72
+ // Try JSON first
73
+ try {
74
+ const parsed = JSON.parse(raw);
75
+ if (typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
76
+ } catch { /* not JSON */ }
77
+ // Fall back to "key: value, key: value" format
78
+ const result = {};
79
+ for (const part of raw.split(',')) {
80
+ const colonIdx = part.indexOf(':');
81
+ if (colonIdx === -1) continue;
82
+ const key = part.slice(0, colonIdx).trim();
83
+ const val = part.slice(colonIdx + 1).trim();
84
+ if (key && val) result[key] = val;
85
+ }
86
+ return result;
87
+ }
88
+
37
89
  // ---------------------------------------------------------------------------
38
90
  // Workfile creation
39
91
  // ---------------------------------------------------------------------------
@@ -45,8 +97,8 @@ export function createWorkfile(frontmatter, goal) {
45
97
 
46
98
  ${goal}
47
99
 
48
- | Artefact | Status |
49
- |----------|--------|
100
+ | File | Type | Cycle | Status |
101
+ |------|------|-------|--------|
50
102
 
51
103
  ## Feedback
52
104
  `;
@@ -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:
@@ -91,7 +91,7 @@ If there are no issues, return an empty list.
91
91
 
92
92
  ## History
93
93
 
94
- After completing the appraisal consolidation, call `foundry_history_append` with the current cycle, stage alias, and a brief summary (e.g., "3 issues found across 2 appraisers" or "No issues found").
94
+ Do NOT call `foundry_history_append` the sort skill (your caller) is responsible for writing history. Instead, return a clear summary of what you found (e.g., "3 issues found across 2 appraisers" or "No issues found") so sort can log it.
95
95
 
96
96
  ## What you do NOT do
97
97
 
@@ -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>`
@@ -40,7 +40,7 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
40
40
 
41
41
  ### After (both paths)
42
42
 
43
- Call `foundry_history_append` with the current cycle, the full stage alias (e.g., `forge:write-haiku`), and a brief description of what you did.
43
+ Do NOT call `foundry_history_append` the sort skill (your caller) is responsible for writing history. Instead, return a clear summary of what you did so sort can log it.
44
44
 
45
45
  ## Unresolved feedback
46
46
 
@@ -36,7 +36,7 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
36
36
  - **Provide context** — note in the history comment for future stages to reference
37
37
  - **Abort** — call `foundry_artefacts_set_status` with status `"blocked"`, cycle ends
38
38
 
39
- 5. Call `foundry_history_append` with the current cycle, stage alias, and a comment capturing the substance of what the human said or decided.
39
+ 5. Do NOT call `foundry_history_append` the sort skill (your caller) is responsible for writing history. Instead, return a clear summary of what the human said or decided so sort can log it.
40
40
 
41
41
  6. Return control to the sort skill.
42
42
 
@@ -35,7 +35,7 @@ There is no wont-fix for validation feedback. Deterministic rules are not negoti
35
35
 
36
36
  ## History
37
37
 
38
- After completing validation, call `foundry_history_append` with the current cycle, stage alias, and a brief summary (e.g., "2 validation issues found" or "Validation passed").
38
+ Do NOT call `foundry_history_append` the sort skill (your caller) is responsible for writing history. Instead, return a clear summary of what you found (e.g., "2 validation issues found" or "Validation passed") so sort can log it.
39
39
 
40
40
  ## What you do NOT do
41
41
 
@@ -29,22 +29,13 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
29
29
  - `blocked` — foundry cycle is blocked (iteration limit hit with unresolved feedback), return to the cycle skill
30
30
  - `violation` — file modification or tag validation violation detected (see `details`). The cycle halts — call `foundry_artefacts_set_status` with status `"blocked"`, and return to the cycle skill
31
31
 
32
- ### Model dispatch
32
+ 4. After the subagent completes, call `foundry_history_append` with the current cycle, the **dispatched stage alias** (e.g., `forge:write-haiku`), and a comment summarizing what the subagent reported doing. This is critical — sort is the only reliable writer of stage history. Subagents must NOT write their own history entries.
33
33
 
34
- Use the `model` field from the `foundry_sort` result to determine sub-agent routing:
35
-
36
- - If `model` is set (e.g., `openai/gpt-4o`):
37
- - Convert to agent name: `foundry-openai-gpt-4o`
38
- - Dispatch with `subagent_type: "foundry-openai-gpt-4o"`
39
- - If no agent with that name exists, **hard fail**: "Cycle specifies model `<model>` for stage `<base>` but no matching agent `foundry-<name>` is registered. Check your OpenCode provider config."
40
- - If `model` is null:
41
- - Dispatch with `subagent_type: "general"` (inherits session model)
42
-
43
- 4. After the invoked skill completes, call `foundry_sort` again. Repeat until it returns `done`, `blocked`, or `violation`.
34
+ 5. After logging the stage history, call `foundry_sort` again. Repeat from step 1 until it returns `done`, `blocked`, or `violation`.
44
35
 
45
36
  ## What you do NOT do
46
37
 
47
38
  - You do not make routing decisions yourself — the tool decides
48
39
  - You do not skip calling `foundry_sort`
49
40
  - You do not override the tool's output
50
- - You do not skip the history entry — every sort invocation must be logged via `foundry_history_append`
41
+ - You do not skip the history entry — every sort invocation gets a `sort` entry, and every completed stage gets a stage entry (e.g., `forge:write-haiku`). You are the sole writer of history.