@really-knows-ai/foundry 1.2.2 → 1.3.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.
- package/.opencode/plugins/foundry.js +408 -1
- package/README.md +31 -5
- package/docs/concepts.md +5 -1
- package/docs/work-spec.md +7 -7
- package/package.json +2 -2
- package/scripts/lib/artefacts.js +118 -0
- package/scripts/lib/config.js +154 -0
- package/scripts/lib/feedback.js +285 -0
- package/scripts/lib/history.js +47 -0
- package/scripts/lib/workfile.js +53 -0
- package/scripts/sort.js +54 -196
- package/skills/appraise/SKILL.md +24 -83
- package/skills/cycle/SKILL.md +25 -62
- package/skills/flow/SKILL.md +12 -38
- package/skills/forge/SKILL.md +25 -41
- package/skills/hitl/SKILL.md +18 -41
- package/skills/quench/SKILL.md +15 -44
- package/skills/sort/SKILL.md +20 -53
package/scripts/sort.js
CHANGED
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
import { readFileSync, existsSync } from 'fs';
|
|
17
17
|
import { execSync } from 'child_process';
|
|
18
|
-
import { parseArgs } from 'util';
|
|
19
|
-
import { join } from 'path';
|
|
20
|
-
import { fileURLToPath } from 'url';
|
|
21
18
|
import yaml from 'js-yaml';
|
|
22
19
|
import { minimatch } from 'minimatch';
|
|
23
|
-
import { validateTags
|
|
20
|
+
import { validateTags } from './lib/tags.js';
|
|
21
|
+
import { parseFrontmatter } from './lib/workfile.js';
|
|
22
|
+
import { parseArtefactsTable } from './lib/artefacts.js';
|
|
23
|
+
import { loadHistory } from './lib/history.js';
|
|
24
|
+
import { parseFeedback, parseFeedbackItem } from './lib/feedback.js';
|
|
24
25
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Stage helpers
|
|
@@ -59,121 +60,6 @@ const defaultIO = {
|
|
|
59
60
|
// Parsing
|
|
60
61
|
// ---------------------------------------------------------------------------
|
|
61
62
|
|
|
62
|
-
function parseFrontmatter(text) {
|
|
63
|
-
const match = text.match(/^---\n(.+?)\n---/s);
|
|
64
|
-
if (!match) return {};
|
|
65
|
-
return yaml.load(match[1]) || {};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parseFeedback(text, cycle, artefacts) {
|
|
69
|
-
const cycleFiles = new Set();
|
|
70
|
-
for (const art of artefacts) {
|
|
71
|
-
if (art.cycle === cycle) {
|
|
72
|
-
cycleFiles.add(art.file || '');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const items = [];
|
|
77
|
-
let currentFile = null;
|
|
78
|
-
let inFeedback = false;
|
|
79
|
-
let feedbackLevel = 0; // 1 for '# Feedback', 2 for '## Feedback'
|
|
80
|
-
|
|
81
|
-
for (const line of text.split('\n')) {
|
|
82
|
-
const stripped = line.trim();
|
|
83
|
-
|
|
84
|
-
if (stripped === '# Feedback' || stripped === '## Feedback') {
|
|
85
|
-
inFeedback = true;
|
|
86
|
-
feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Exit feedback on a heading at the same or higher level
|
|
91
|
-
if (inFeedback && /^#{1,2} /.test(stripped)) {
|
|
92
|
-
const level = stripped.startsWith('## ') ? 2 : 1;
|
|
93
|
-
if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
|
|
94
|
-
inFeedback = false;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!inFeedback) continue;
|
|
100
|
-
|
|
101
|
-
// File sub-headings are one level below the Feedback heading
|
|
102
|
-
const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
|
|
103
|
-
if (stripped.startsWith(fileHeadingPrefix)) {
|
|
104
|
-
currentFile = stripped.slice(fileHeadingPrefix.length).trim();
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (cycleFiles.has(currentFile) && /^- \[/.test(stripped)) {
|
|
109
|
-
items.push(parseFeedbackItem(stripped));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return items;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function parseFeedbackItem(line) {
|
|
117
|
-
const item = { raw: line, state: 'unknown', tags: [], resolved: false };
|
|
118
|
-
|
|
119
|
-
if (line.startsWith('- [ ]')) {
|
|
120
|
-
item.state = 'open';
|
|
121
|
-
} else if (line.startsWith('- [x]')) {
|
|
122
|
-
item.state = 'actioned';
|
|
123
|
-
} else if (line.startsWith('- [~]')) {
|
|
124
|
-
item.state = 'wont-fix';
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (line.includes('| approved')) {
|
|
128
|
-
item.resolved = true;
|
|
129
|
-
} else if (line.includes('| rejected')) {
|
|
130
|
-
item.state = 'rejected';
|
|
131
|
-
item.resolved = false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
item.tags = extractAllTags(line);
|
|
135
|
-
|
|
136
|
-
return item;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function parseArtefactsTable(text) {
|
|
140
|
-
const artefacts = [];
|
|
141
|
-
let inTable = false;
|
|
142
|
-
|
|
143
|
-
for (const line of text.split('\n')) {
|
|
144
|
-
const stripped = line.trim();
|
|
145
|
-
|
|
146
|
-
if (stripped.startsWith('| File')) {
|
|
147
|
-
inTable = true;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (inTable && stripped.startsWith('|---')) {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (inTable && stripped.startsWith('|')) {
|
|
154
|
-
const cols = stripped.split('|').slice(1, -1).map(c => c.trim());
|
|
155
|
-
if (cols.length >= 4) {
|
|
156
|
-
artefacts.push({
|
|
157
|
-
file: cols[0],
|
|
158
|
-
type: cols[1],
|
|
159
|
-
cycle: cols[2],
|
|
160
|
-
status: cols[3],
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
} else if (inTable) {
|
|
164
|
-
inTable = false;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return artefacts;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function loadHistory(historyPath, cycle, io = defaultIO) {
|
|
172
|
-
if (!io.exists(historyPath)) return [];
|
|
173
|
-
const data = yaml.load(io.readFile(historyPath)) || [];
|
|
174
|
-
return data.filter(e => e.cycle === cycle);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
63
|
// ---------------------------------------------------------------------------
|
|
178
64
|
// Routing logic
|
|
179
65
|
// ---------------------------------------------------------------------------
|
|
@@ -310,109 +196,81 @@ function checkModifiedFiles(lastBase, foundryDir, cycleDef, cycle, io = defaultI
|
|
|
310
196
|
}
|
|
311
197
|
|
|
312
198
|
// ---------------------------------------------------------------------------
|
|
313
|
-
//
|
|
199
|
+
// Exported runSort — structured result for programmatic use
|
|
314
200
|
// ---------------------------------------------------------------------------
|
|
315
201
|
|
|
316
|
-
export {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
nextInRoute,
|
|
320
|
-
parseFrontmatter,
|
|
321
|
-
parseFeedback,
|
|
322
|
-
parseFeedbackItem,
|
|
323
|
-
parseArtefactsTable,
|
|
324
|
-
loadHistory,
|
|
325
|
-
determineRoute,
|
|
326
|
-
nextAfterQuench,
|
|
327
|
-
nextAfterAppraise,
|
|
328
|
-
globMatch,
|
|
329
|
-
getModifiedFiles,
|
|
330
|
-
getAllowedPatterns,
|
|
331
|
-
checkModifiedFiles,
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
// Main
|
|
336
|
-
// ---------------------------------------------------------------------------
|
|
337
|
-
|
|
338
|
-
function main() {
|
|
339
|
-
const { values } = parseArgs({
|
|
340
|
-
options: {
|
|
341
|
-
work: { type: 'string', default: 'WORK.md' },
|
|
342
|
-
history: { type: 'string', default: 'WORK.history.yaml' },
|
|
343
|
-
'foundry-dir': { type: 'string', default: 'foundry' },
|
|
344
|
-
'cycle-def': { type: 'string' },
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const workPath = values.work;
|
|
349
|
-
const historyPath = values.history;
|
|
350
|
-
const foundryDir = values['foundry-dir'];
|
|
351
|
-
|
|
352
|
-
if (!existsSync(workPath)) {
|
|
353
|
-
process.stderr.write('ERROR: WORK.md not found\n');
|
|
354
|
-
process.exit(1);
|
|
202
|
+
export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml', foundryDir = 'foundry', cycleDef } = {}, io = defaultIO) {
|
|
203
|
+
if (!io.exists(workPath)) {
|
|
204
|
+
return { route: 'blocked', details: 'WORK.md not found' };
|
|
355
205
|
}
|
|
356
206
|
|
|
357
|
-
const workText =
|
|
207
|
+
const workText = io.readFile(workPath);
|
|
358
208
|
const frontmatter = parseFrontmatter(workText);
|
|
359
209
|
|
|
360
210
|
const cycle = frontmatter.cycle;
|
|
361
211
|
const stages = frontmatter.stages;
|
|
362
212
|
const maxIterations = frontmatter['max-iterations'] ?? 3;
|
|
363
213
|
|
|
364
|
-
if (!cycle) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!stages || !Array.isArray(stages)) {
|
|
370
|
-
process.stderr.write('ERROR: No stages in WORK.md frontmatter\n');
|
|
371
|
-
process.exit(1);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (!findFirst(stages, 'forge')) {
|
|
375
|
-
process.stderr.write('ERROR: stages must include at least one forge stage\n');
|
|
376
|
-
process.exit(1);
|
|
377
|
-
}
|
|
214
|
+
if (!cycle) return { route: 'blocked', details: 'No cycle in WORK.md frontmatter' };
|
|
215
|
+
if (!stages || !Array.isArray(stages)) return { route: 'blocked', details: 'No stages in WORK.md frontmatter' };
|
|
216
|
+
if (!findFirst(stages, 'forge')) return { route: 'blocked', details: 'stages must include at least one forge stage' };
|
|
378
217
|
|
|
379
218
|
const artefacts = parseArtefactsTable(workText);
|
|
380
|
-
const history = loadHistory(historyPath, cycle);
|
|
219
|
+
const history = loadHistory(historyPath, cycle, io);
|
|
381
220
|
const feedback = parseFeedback(workText, cycle, artefacts);
|
|
382
221
|
|
|
383
|
-
//
|
|
222
|
+
// File modification enforcement
|
|
384
223
|
const nonSortHistory = history.filter(e => baseStage(e.stage || '') !== 'sort');
|
|
385
224
|
if (nonSortHistory.length > 0) {
|
|
386
225
|
const lastEntry = nonSortHistory[nonSortHistory.length - 1];
|
|
387
226
|
const lastBase = baseStage(lastEntry.stage || '');
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const cycleDef = values['cycle-def']
|
|
391
|
-
|| frontmatter['cycle-def']
|
|
392
|
-
|| `${foundryDir}/cycles/${cycle}.md`;
|
|
393
|
-
|
|
394
|
-
const result = checkModifiedFiles(lastBase, foundryDir, cycleDef, cycle);
|
|
227
|
+
const resolvedCycleDef = cycleDef || frontmatter['cycle-def'] || `${foundryDir}/cycles/${cycle}.md`;
|
|
228
|
+
const result = checkModifiedFiles(lastBase, foundryDir, resolvedCycleDef, cycle, io);
|
|
395
229
|
if (!result.ok) {
|
|
396
|
-
|
|
397
|
-
process.stderr.write(`File modification violation after ${lastBase} stage:\n`);
|
|
398
|
-
result.violations.forEach(f => process.stderr.write(` ${f}\n`));
|
|
399
|
-
process.exit(0);
|
|
230
|
+
return { route: 'violation', details: `File modification violation after ${lastBase} stage: ${result.violations.join(', ')}` };
|
|
400
231
|
}
|
|
401
232
|
}
|
|
402
233
|
|
|
403
|
-
//
|
|
234
|
+
// Tag validation
|
|
404
235
|
const tagErrors = validateTags(workText, foundryDir);
|
|
405
236
|
if (tagErrors.length > 0) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
tagErrors.forEach(e => process.stderr.write(` line ${e.line}: ${e.message}\n`));
|
|
409
|
-
process.exit(0);
|
|
237
|
+
const details = tagErrors.map(e => `line ${e.line}: ${e.message}`).join('; ');
|
|
238
|
+
return { route: 'violation', details: `Feedback tag validation failed: ${details}` };
|
|
410
239
|
}
|
|
411
240
|
|
|
412
241
|
const route = determineRoute(stages, history, feedback, maxIterations);
|
|
413
|
-
console.log(route);
|
|
414
|
-
}
|
|
415
242
|
|
|
416
|
-
|
|
417
|
-
|
|
243
|
+
// Model resolution
|
|
244
|
+
let model = null;
|
|
245
|
+
const routeBase = baseStage(route);
|
|
246
|
+
if (frontmatter.models && frontmatter.models[routeBase]) {
|
|
247
|
+
const modelId = frontmatter.models[routeBase];
|
|
248
|
+
model = `foundry-${modelId.replace(/\//g, '-')}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { route, ...(model ? { model } : {}) };
|
|
418
252
|
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Exports (for testing) — keep main() private
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
export { parseArtefactsTable } from './lib/artefacts.js';
|
|
259
|
+
export { loadHistory } from './lib/history.js';
|
|
260
|
+
export { parseFeedback, parseFeedbackItem } from './lib/feedback.js';
|
|
261
|
+
|
|
262
|
+
export {
|
|
263
|
+
baseStage,
|
|
264
|
+
findFirst,
|
|
265
|
+
nextInRoute,
|
|
266
|
+
parseFrontmatter,
|
|
267
|
+
determineRoute,
|
|
268
|
+
nextAfterQuench,
|
|
269
|
+
nextAfterAppraise,
|
|
270
|
+
globMatch,
|
|
271
|
+
getModifiedFiles,
|
|
272
|
+
getAllowedPatterns,
|
|
273
|
+
checkModifiedFiles,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
|
package/skills/appraise/SKILL.md
CHANGED
|
@@ -6,7 +6,7 @@ description: Subjective evaluation of an artefact against laws via multiple inde
|
|
|
6
6
|
|
|
7
7
|
# Appraise
|
|
8
8
|
|
|
9
|
-
You orchestrate subjective appraisal of an artefact by dispatching independent sub-agent appraisers, then consolidating their feedback
|
|
9
|
+
You orchestrate subjective appraisal of an artefact by dispatching independent sub-agent appraisers, then consolidating their feedback.
|
|
10
10
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
@@ -14,88 +14,49 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
|
|
|
14
14
|
|
|
15
15
|
> Foundry is not initialized in this project. Run the `init-foundry` skill first to create the foundry/ directory structure.
|
|
16
16
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
Appraiser personalities are defined in `foundry/appraisers/` (the appraiser directory). Each markdown file defines:
|
|
20
|
-
- `id` — identifier
|
|
21
|
-
- `model` — (optional) specific model ID to use for this appraiser, overriding the cycle-level appraise model
|
|
22
|
-
|
|
23
|
-
The artefact type definition (`foundry/artefacts/<type>/definition.md`) controls how appraisers are assigned via its `appraisers` frontmatter:
|
|
24
|
-
|
|
25
|
-
```yaml
|
|
26
|
-
appraisers:
|
|
27
|
-
count: 3 # how many appraisers (default: 3)
|
|
28
|
-
allowed: [pedantic, pragmatic] # which personalities (default: all available)
|
|
29
|
-
```
|
|
17
|
+
## Protocol
|
|
30
18
|
|
|
31
|
-
|
|
19
|
+
1. Gather context:
|
|
20
|
+
- Call `foundry_workfile_get` — identify the artefact to appraise and its type
|
|
21
|
+
- Call `foundry_config_laws` — get all applicable laws (global + type-specific)
|
|
22
|
+
- Call `foundry_config_artefact_type` with the type ID — get the artefact type definition
|
|
23
|
+
- Call `foundry_appraisers_select` with the type ID — returns selected appraiser personalities with their raw model IDs
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
2. If `allowed` is specified, filter to only those personalities. Otherwise use all in `foundry/appraisers/`.
|
|
35
|
-
3. If `count` is omitted, default to 3
|
|
36
|
-
4. Distribute evenly across available personalities for maximum diversity:
|
|
37
|
-
- 3 appraisers, 3 personalities → 1 of each
|
|
38
|
-
- 6 appraisers, 3 personalities → 2 of each
|
|
39
|
-
- 4 appraisers, 3 personalities → 2, 1, 1 (round-robin)
|
|
40
|
-
5. If count > available personalities, wrap around (same personality, still independent sub-agents)
|
|
25
|
+
2. Dispatch each appraiser as an independent sub-agent (see Dispatch below)
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
3. Collect results from all appraisers
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
1. Read `WORK.md` — identify the artefact to appraise and its type
|
|
47
|
-
2. Read all files in `foundry/laws/` — identify global laws
|
|
48
|
-
3. Read `foundry/artefacts/<type>/laws.md` — identify type-specific laws (if it exists)
|
|
49
|
-
4. Read `foundry/artefacts/<type>/definition.md` — for context and appraiser config
|
|
50
|
-
5. Select appraisers (see Appraiser selection above)
|
|
51
|
-
6. Dispatch each appraiser as a sub-agent (see Dispatch below)
|
|
52
|
-
7. Collect results from all appraisers
|
|
53
|
-
8. Consolidate:
|
|
29
|
+
4. Consolidate (this is judgment):
|
|
54
30
|
- Union of all issues — if any one appraiser flags it, it's feedback
|
|
55
31
|
- De-duplicate: merge overlapping observations into a single feedback item
|
|
56
32
|
- Preserve which appraiser(s) raised each issue (for traceability)
|
|
57
|
-
9. Write consolidated feedback to WORK.md under the artefact's file heading:
|
|
58
33
|
|
|
59
|
-
|
|
34
|
+
5. For each consolidated issue: call `foundry_feedback_add` with the artefact file path, the issue description, and tag `law:<law-id>`
|
|
35
|
+
|
|
36
|
+
6. If no appraiser found any issues, the artefact clears appraisal
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
## Feedback
|
|
38
|
+
## Reviewing actioned and wont-fix feedback
|
|
63
39
|
|
|
64
|
-
|
|
65
|
-
- [ ] The imagery lacks originality #law:vivid-imagery
|
|
66
|
-
```
|
|
40
|
+
On subsequent passes, review previously actioned and wont-fix items:
|
|
67
41
|
|
|
68
|
-
|
|
69
|
-
|
|
42
|
+
1. Call `foundry_feedback_list` to find `actioned` and `wontfix` items for this artefact
|
|
43
|
+
2. For each item, the appraiser sub-agents evaluate whether the change addresses the issue (actioned) or the justification is sound (wont-fix)
|
|
44
|
+
3. Call `foundry_feedback_resolve` with disposition `"approved"` or `"rejected"` (with reason) for each
|
|
70
45
|
|
|
71
46
|
## Dispatch
|
|
72
47
|
|
|
73
48
|
Each appraiser is dispatched as an independent sub-agent. The sub-agent receives a prompt containing:
|
|
74
|
-
- The appraiser's personality (from their definition
|
|
49
|
+
- The appraiser's personality (from their definition)
|
|
75
50
|
- The artefact content
|
|
76
51
|
- All applicable laws (global + type-specific)
|
|
77
52
|
- Instructions to evaluate the artefact against each law and return issues as a structured list
|
|
78
53
|
|
|
79
54
|
### Model resolution
|
|
80
55
|
|
|
81
|
-
|
|
82
|
-
1. **Appraiser `model` field** — if the appraiser definition specifies a `model`, use it
|
|
83
|
-
2. **Cycle `models.appraise`** — if the cycle definition specifies a model for the appraise stage, use it (read from WORK.md frontmatter or the cycle definition)
|
|
84
|
-
3. **Default** — use `subagent_type: "general"` (inherits the session's model)
|
|
85
|
-
|
|
86
|
-
If a model is resolved (options 1 or 2), convert it to an agent name: `foundry-<provider-id>-<model-key>` (e.g., `openai/gpt-4o` → `foundry-openai-gpt-4o`). If no agent with that name exists, **hard fail** with an error:
|
|
87
|
-
|
|
88
|
-
> Appraiser `<appraiser-id>` specifies model `<model-id>` but no matching agent `foundry-<agent-name>` is registered. Check your OpenCode provider config.
|
|
56
|
+
`foundry_appraisers_select` returns raw model IDs for each appraiser. Convert each to an agent name: `foundry-<model.replace(/\//g, '-')>` (e.g., `openai/gpt-4o` becomes `foundry-openai-gpt-4o`).
|
|
89
57
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Use the Task tool to dispatch each appraiser:
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
Task tool call for each appraiser:
|
|
96
|
-
- subagent_type: "<resolved agent name>" or "general" if no model specified
|
|
97
|
-
- prompt: contains personality, artefact, laws, evaluation instructions
|
|
98
|
-
```
|
|
58
|
+
- If a model is specified: dispatch with `subagent_type: "foundry-<converted-name>"`. If no agent with that name exists, **hard fail**.
|
|
59
|
+
- If no model is specified: dispatch with `subagent_type: "general"` (inherits session model).
|
|
99
60
|
|
|
100
61
|
Dispatch all appraisers in parallel (multiple Task calls in a single response).
|
|
101
62
|
|
|
@@ -104,7 +65,7 @@ Dispatch all appraisers in parallel (multiple Task calls in a single response).
|
|
|
104
65
|
```
|
|
105
66
|
You are an appraiser. Your personality:
|
|
106
67
|
|
|
107
|
-
<contents of
|
|
68
|
+
<contents of appraiser personality>
|
|
108
69
|
|
|
109
70
|
Evaluate the following artefact against each law below. For each law, either:
|
|
110
71
|
- Note no issues (pass)
|
|
@@ -128,32 +89,12 @@ Return a list of issues. For each issue:
|
|
|
128
89
|
If there are no issues, return an empty list.
|
|
129
90
|
```
|
|
130
91
|
|
|
131
|
-
## Reviewing actioned and wont-fix feedback
|
|
132
|
-
|
|
133
|
-
On subsequent passes, appraisers also evaluate previously actioned and wont-fix items under the artefact's `### <file-path>` heading:
|
|
134
|
-
|
|
135
|
-
- `[x]` actioned items: appraiser checks whether the change actually addresses the issue
|
|
136
|
-
- If yes: mark `| approved`
|
|
137
|
-
- If no: mark `| rejected: <reason>` (item is effectively re-opened)
|
|
138
|
-
- `[~]` wont-fix items: appraiser reads the justification
|
|
139
|
-
- If the justification is sound: mark `| approved`
|
|
140
|
-
- If not: mark `| rejected` (item is effectively re-opened)
|
|
141
|
-
|
|
142
92
|
## History
|
|
143
93
|
|
|
144
|
-
After completing the appraisal consolidation,
|
|
145
|
-
|
|
146
|
-
```yaml
|
|
147
|
-
- timestamp: "<ISO 8601 UTC>"
|
|
148
|
-
cycle: <current-cycle-id>
|
|
149
|
-
stage: <alias>
|
|
150
|
-
iteration: <current iteration from history>
|
|
151
|
-
comment: <brief summary, e.g., "3 issues found across 2 appraisers" or "No issues found, cycle complete">
|
|
152
|
-
```
|
|
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").
|
|
153
95
|
|
|
154
96
|
## What you do NOT do
|
|
155
97
|
|
|
156
98
|
- You do not revise the artefact
|
|
157
99
|
- You do not check deterministic rules — that is the quench skill's job
|
|
158
100
|
- You do not filter out feedback because only one appraiser raised it — one is enough
|
|
159
|
-
- You do not write feedback items without a file sub-heading under `## Feedback`
|
package/skills/cycle/SKILL.md
CHANGED
|
@@ -7,7 +7,7 @@ composes: [sort, forge, quench, appraise, hitl]
|
|
|
7
7
|
|
|
8
8
|
# Cycle
|
|
9
9
|
|
|
10
|
-
A foundry cycle reads its definition
|
|
10
|
+
A foundry cycle reads its definition, sets up the work file for routing, then hands control to the sort skill which drives the forge/quench/appraise loop.
|
|
11
11
|
|
|
12
12
|
## Prerequisites
|
|
13
13
|
|
|
@@ -15,98 +15,61 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
|
|
|
15
15
|
|
|
16
16
|
> Foundry is not initialized in this project. Run the `init-foundry` skill first to create the foundry/ directory structure.
|
|
17
17
|
|
|
18
|
-
## Cycle definition
|
|
19
|
-
|
|
20
|
-
The cycle definition (`foundry/cycles/<cycle-id>.md`) specifies:
|
|
21
|
-
- `output` — the artefact type this foundry cycle produces (read-write)
|
|
22
|
-
- `inputs` — artefact types from previous foundry cycles that are read-only context
|
|
23
|
-
- `stages` — (optional) explicit stage list with aliases in `base:alias` format (e.g., `[forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]`)
|
|
24
|
-
- `hitl` — (optional) configuration for human-in-the-loop stages, including prompts
|
|
25
|
-
- `models` — (optional) map of stage base names to model IDs for multi-model routing (e.g., `{ appraise: openai/gpt-4o }`). Stages not listed use the session's default model. If a specified model has no matching `foundry-*` agent, the cycle fails with an error.
|
|
26
|
-
|
|
27
|
-
If `stages` is not provided, the cycle skill generates default aliases from the cycle id and artefact type.
|
|
28
|
-
|
|
29
18
|
## Starting a foundry cycle
|
|
30
19
|
|
|
31
|
-
1.
|
|
32
|
-
2.
|
|
33
|
-
3. Determine the stage route
|
|
34
|
-
-
|
|
35
|
-
- Otherwise
|
|
36
|
-
- Cycle definitions can include `hitl` entries
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- Set `cycle` to the cycle id
|
|
43
|
-
- Set `stages` to the determined route (e.g., `[forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]`)
|
|
44
|
-
- Set `max-iterations` (default 3, or from cycle definition if overridden)
|
|
45
|
-
- If the cycle definition has a `models` map, set `models` in WORK.md frontmatter (e.g., `models: { appraise: openai/gpt-4o }`)
|
|
20
|
+
1. Call `foundry_config_cycle` with the cycle ID — get the cycle definition
|
|
21
|
+
2. Call `foundry_config_artefact_type` with the output type ID — get the artefact type definition
|
|
22
|
+
3. Determine the stage route:
|
|
23
|
+
- Use the cycle definition's `stages` field if present
|
|
24
|
+
- Otherwise generate defaults: always `forge`, add `quench` if `foundry_config_validation` returns non-null for the type, always `appraise`
|
|
25
|
+
- Cycle definitions can include `hitl` entries for human-in-the-loop checkpoints
|
|
26
|
+
4. Call `foundry_workfile_set` to configure the work file:
|
|
27
|
+
- `key: "cycle"`, `value: <cycle-id>`
|
|
28
|
+
- `key: "stages"`, `value: <determined stages list>`
|
|
29
|
+
- `key: "max-iterations"`, `value: <default 3 or from cycle definition>`
|
|
30
|
+
- If the cycle definition has a `models` map: `key: "models"`, `value: <models map>`
|
|
46
31
|
5. Invoke the sort skill
|
|
47
32
|
|
|
48
33
|
## Sort drives everything
|
|
49
34
|
|
|
50
|
-
Once sort is invoked, it
|
|
35
|
+
Once sort is invoked, it calls `foundry_sort` to determine the next stage, invokes the corresponding skill, then calls sort again. This repeats until sort returns `done` or `blocked`.
|
|
51
36
|
|
|
52
37
|
The cycle skill does not contain routing logic — sort owns all of that.
|
|
53
38
|
|
|
54
39
|
## Completing a foundry cycle
|
|
55
40
|
|
|
56
41
|
When sort returns `done`:
|
|
57
|
-
-
|
|
58
|
-
- Return control to the
|
|
42
|
+
- Call `foundry_artefacts_set_status` with status `"done"`
|
|
43
|
+
- Return control to the flow skill
|
|
59
44
|
|
|
60
45
|
When sort returns `blocked`:
|
|
61
|
-
-
|
|
62
|
-
- Return control to the
|
|
46
|
+
- Call `foundry_artefacts_set_status` with status `"blocked"`
|
|
47
|
+
- Return control to the flow skill (the flow decides how to handle it)
|
|
63
48
|
|
|
64
49
|
## HITL stages
|
|
65
50
|
|
|
66
|
-
Cycle definitions can include `hitl` entries in their stages list to pause for human input.
|
|
67
|
-
|
|
68
|
-
When sort routes to a `hitl` stage:
|
|
69
|
-
- The hitl skill presents the configured prompt to the human
|
|
70
|
-
- The human provides feedback, which is recorded in WORK.md and WORK.history.yaml
|
|
71
|
-
- Sort then determines the next stage based on the feedback
|
|
72
|
-
|
|
73
|
-
HITL stages follow the same file modification rules as quench/appraise — only WORK.md and WORK.history.yaml may be modified.
|
|
51
|
+
Cycle definitions can include `hitl` entries in their stages list to pause for human input. When sort routes to a `hitl` stage, the hitl skill presents the configured prompt and records the human's response.
|
|
74
52
|
|
|
75
53
|
## Micro commits
|
|
76
54
|
|
|
77
|
-
Every stage must end with a micro commit.
|
|
55
|
+
Every stage must end with a micro commit. Call `foundry_git_commit` with message format: `[<cycle-id>] <base>:<alias>: <brief description>`
|
|
78
56
|
|
|
79
57
|
Examples:
|
|
80
58
|
- `[haiku-creation] forge:write-haiku: initial draft`
|
|
81
59
|
- `[haiku-creation] quench:check-syllables: checked syllable pattern`
|
|
82
60
|
- `[haiku-creation] forge:write-haiku: addressed validation feedback`
|
|
83
|
-
- `[haiku-creation] hitl:review-draft: recorded human feedback`
|
|
84
|
-
|
|
85
|
-
## File modification enforcement
|
|
86
|
-
|
|
87
|
-
File modification enforcement is handled automatically by the sort script (`scripts/sort.js`). Before routing to the next stage, sort checks the git diff from the last commit against allowed file patterns:
|
|
88
|
-
|
|
89
|
-
- After forge: output artefact file patterns + WORK.md + WORK.history.yaml
|
|
90
|
-
- After quench/appraise/hitl: only WORK.md + WORK.history.yaml
|
|
91
|
-
- Input artefact files are never allowed (read-only)
|
|
92
|
-
|
|
93
|
-
Sort reads the cycle definition and artefact type definition to determine allowed patterns. If a violation is detected, sort returns `violation` (with details on stderr) and the cycle halts.
|
|
94
|
-
|
|
95
|
-
A violation is a hard stop. The foundry cycle sets artefact status to `blocked` and surfaces the issue to the human.
|
|
96
61
|
|
|
97
62
|
## Feedback states
|
|
98
63
|
|
|
99
64
|
```
|
|
100
|
-
open -
|
|
101
|
-
actioned -
|
|
102
|
-
wont-fix -
|
|
103
|
-
approved -
|
|
104
|
-
|
|
105
|
-
rejected - [x] issue #tag | rejected: <reason> → re-opened
|
|
106
|
-
rejected - [~] issue #tag | wont-fix: <reason> | rejected → re-opened
|
|
65
|
+
open - needs generator action
|
|
66
|
+
actioned - needs approval
|
|
67
|
+
wont-fix - needs approval (appraisal only)
|
|
68
|
+
approved - resolved
|
|
69
|
+
rejected - re-opened
|
|
107
70
|
```
|
|
108
71
|
|
|
109
|
-
Tag types:
|
|
72
|
+
Tag types: `validation` (from quench), `law:<law-id>` (from appraise), `hitl` (from human) — indicates the source and category of feedback.
|
|
110
73
|
|
|
111
74
|
## What you do NOT do
|
|
112
75
|
|
package/skills/flow/SKILL.md
CHANGED
|
@@ -7,7 +7,7 @@ composes: [cycle]
|
|
|
7
7
|
|
|
8
8
|
# Flow
|
|
9
9
|
|
|
10
|
-
A foundry flow reads a flow definition
|
|
10
|
+
A foundry flow reads a flow definition, creates a work branch, initialises the work file, and executes each foundry cycle in sequence.
|
|
11
11
|
|
|
12
12
|
## Prerequisites
|
|
13
13
|
|
|
@@ -17,42 +17,16 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
|
|
|
17
17
|
|
|
18
18
|
## Starting a foundry flow
|
|
19
19
|
|
|
20
|
-
1.
|
|
21
|
-
2.
|
|
22
|
-
3.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
flow: <flow-id>
|
|
27
|
-
cycle: <first-cycle-id>
|
|
28
|
-
stages: [<determined by cycle skill>]
|
|
29
|
-
max-iterations: 3
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
# Goal
|
|
33
|
-
|
|
34
|
-
<goal from flow definition + human context>
|
|
35
|
-
|
|
36
|
-
## Artefacts
|
|
37
|
-
|
|
38
|
-
| File | Type | Cycle | Status |
|
|
39
|
-
|------|------|-------|--------|
|
|
40
|
-
|
|
41
|
-
## Feedback
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
- `flow` — set once, never changes
|
|
45
|
-
- `cycle` — current cycle id, updated when each cycle starts
|
|
46
|
-
- `stages` — the ordered route for the cycle, set by the cycle skill. Each entry uses `base:alias` format (e.g. `forge:write-haiku`, `quench:check-syllables`). Determined from the artefact type: if `validation.md` exists, include `quench`; always include `forge` and `appraise`. `hitl` stages are optional.
|
|
47
|
-
- `max-iterations` — how many forge passes before the cycle is blocked (default: 3, can be overridden in cycle definition)
|
|
48
|
-
- Feedback is grouped under `### <file-path>` sub-headings matching the artefact's File column. See the quench and appraise skills for the format.
|
|
49
|
-
4. Execute each foundry cycle in order by reading its definition from `foundry/cycles/<cycle-id>.md`
|
|
50
|
-
5. Update the frontmatter cursor as each foundry cycle starts (set `cycle` to the new cycle id)
|
|
51
|
-
6. When all foundry cycles are done, delete WORK.md — the artefacts and git history are the record
|
|
20
|
+
1. Call `foundry_config_flow` with the flow ID — get the flow definition
|
|
21
|
+
2. Call `foundry_git_branch` with name `work/<flow-id>-<short-description>` — create the work branch
|
|
22
|
+
3. Call `foundry_workfile_create` with the flow ID, first cycle ID, and goal from the flow definition + human context
|
|
23
|
+
4. Execute each cycle in order by invoking the cycle skill
|
|
24
|
+
5. Between cycles: call `foundry_workfile_set` with `key: "cycle"`, `value: <next-cycle-id>`
|
|
25
|
+
6. When all cycles are done: call `foundry_workfile_delete` — the artefacts and git history are the record
|
|
52
26
|
|
|
53
27
|
## Completing a foundry flow
|
|
54
28
|
|
|
55
|
-
When the
|
|
29
|
+
When the flow is complete, the branch contains:
|
|
56
30
|
- The finished artefacts
|
|
57
31
|
- The full git history of micro commits showing every stage
|
|
58
32
|
|
|
@@ -60,7 +34,7 @@ The human decides whether to merge, open a PR, or discard.
|
|
|
60
34
|
|
|
61
35
|
## What you do NOT do
|
|
62
36
|
|
|
63
|
-
- You do not skip
|
|
64
|
-
- You do not reorder
|
|
65
|
-
- You do not modify artefacts directly — only
|
|
66
|
-
- You do not delete or rewrite feedback history
|
|
37
|
+
- You do not skip cycles
|
|
38
|
+
- You do not reorder cycles
|
|
39
|
+
- You do not modify artefacts directly — only cycles modify artefacts
|
|
40
|
+
- You do not delete or rewrite feedback history during the flow
|