@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.
|
|
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)
|
|
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.
|
|
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
|
-
|
|
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
|
|
454
|
-
const expanded =
|
|
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
|
+
"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",
|
package/scripts/lib/config.js
CHANGED
|
@@ -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
|
|
93
|
-
const
|
|
94
|
-
let
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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) {
|
package/scripts/lib/feedback.js
CHANGED
|
@@ -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({
|
package/scripts/lib/workfile.js
CHANGED
|
@@ -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
|
-
|
|
|
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:
|
package/skills/cycle/SKILL.md
CHANGED
|
@@ -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>`
|