@really-knows-ai/foundry 1.2.2 → 1.3.1

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/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, extractAllTags } from './lib/tags.js';
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
- // Exports (for testing) keep main() private
199
+ // Exported runSortstructured result for programmatic use
314
200
  // ---------------------------------------------------------------------------
315
201
 
316
- export {
317
- baseStage,
318
- findFirst,
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 = readFileSync(workPath, 'utf-8');
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
- process.stderr.write('ERROR: No cycle in WORK.md frontmatter\n');
366
- process.exit(1);
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
- // --- File modification enforcement ---
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
- // Resolve cycle-def: CLI arg > WORK.md frontmatter field
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
- console.log('violation');
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
- // --- Tag validation ---
234
+ // Tag validation
404
235
  const tagErrors = validateTags(workText, foundryDir);
405
236
  if (tagErrors.length > 0) {
406
- console.log('violation');
407
- process.stderr.write(`Feedback tag validation failed (${tagErrors.length} issue${tagErrors.length > 1 ? 's' : ''}):\n`);
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
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
417
- main();
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
+
@@ -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 into WORK.md.
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
- ## Appraiser configuration
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
- ### Appraiser selection
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
- 1. Read the `appraisers` config from the artefact type definition
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
- Model diversity is configured at two levels: the cycle definition sets a default model for the appraise stage (which should differ from the forge model), and individual appraisers can optionally override with their own model. If no models are configured, the session's default model is used — personality diversity still adds value but model diversity is lost.
27
+ 3. Collect results from all appraisers
43
28
 
44
- ## Protocol
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
- Feedback MUST be scoped to the artefact file. Under `## Feedback`, create a `### <file-path>` sub-heading matching the artefact's File column from the artefacts table, then write feedback items beneath it:
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
- ```markdown
62
- ## Feedback
38
+ ## Reviewing actioned and wont-fix feedback
63
39
 
64
- ### foundry/output/haiku/pissed-off-spaghetti.md
65
- - [ ] The imagery lacks originality #law:vivid-imagery
66
- ```
40
+ On subsequent passes, review previously actioned and wont-fix items:
67
41
 
68
- If the `## Feedback` section or the file sub-heading already exists (e.g., quench already wrote validation feedback there), append items under the existing heading. Never write feedback items without a file sub-heading — the sort script cannot parse them.
69
- 10. If no appraiser found any issues, the artefact clears appraisal
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 file)
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
- For each appraiser being dispatched, resolve the model in this order:
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
- ### OpenCode dispatch
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 foundry/appraisers/<id>.md>
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, append an entry to `WORK.history.yaml`:
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`
@@ -7,7 +7,7 @@ composes: [sort, forge, quench, appraise, hitl]
7
7
 
8
8
  # Cycle
9
9
 
10
- A foundry cycle reads its definition from `foundry/cycles/<cycle-id>.md`, sets up WORK.md for routing, then hands control to the sort skill which drives the forgequenchappraise loop.
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. Read the cycle definition from `foundry/cycles/<cycle-id>.md`
32
- 2. Read the output artefact type definition from `foundry/artefacts/<type>/definition.md`
33
- 3. Determine the stage route using `base:alias` format:
34
- - Build the stages list from the cycle definition's `stages` field if present
35
- - Otherwise, generate defaults: always `forge`, add `quench` if `foundry/artefacts/<type>/validation.md` exists, always `appraise`
36
- - Cycle definitions can include `hitl` entries in the stages list for human-in-the-loop checkpoints
37
- - Examples:
38
- - `[forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]`
39
- - `[forge:write-petition, appraise:evaluate-petition]`
40
- - `[forge:draft-proposal, hitl:review-proposal, appraise:evaluate-proposal]`
41
- 4. Update WORK.md frontmatter:
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 runs `scripts/sort.js` to determine the next stage, invokes the corresponding skill, then runs sort again. This repeats until sort returns `done` or `blocked`.
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
- - Update the artefact status in WORK.md to `done`
58
- - Return control to the foundry flow skill
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
- - Update the artefact status in WORK.md to `blocked`
62
- - Return control to the foundry flow skill (the foundry flow decides how to handle it)
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. The cycle definition's `hitl:` config section specifies prompts shown to the human at each hitl checkpoint.
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. Commit message format: `[<cycle-id>] <base>:<alias>: <brief description>`
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 - [ ] issue #tag → needs generator action
101
- actioned - [x] issue #tag → needs approval
102
- wont-fix - [~] issue #tag | wont-fix: <reason> → needs approval (appraisal only)
103
- approved - [x] issue #tag | approved → resolved
104
- approved - [~] issue #tag | wont-fix: <reason> | approved → resolved
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: `#validation` (from quench), `#law:<law-id>` (from appraise), `#hitl` (from human) — indicates the source and category of feedback.
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
 
@@ -7,7 +7,7 @@ composes: [cycle]
7
7
 
8
8
  # Flow
9
9
 
10
- A foundry flow reads a flow definition from `foundry/flows/`, creates a work branch, initialises WORK.md, and executes each foundry cycle in sequence.
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. Read the flow definition from `foundry/flows/<flow-id>.md`
21
- 2. Create a branch off main: `work/<flow-id>-<short-description>`
22
- 3. Create `WORK.md` in the project root with this structure:
23
-
24
- ```markdown
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 foundry flow is complete, the branch contains:
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 foundry cycles
64
- - You do not reorder foundry cycles
65
- - You do not modify artefacts directly — only foundry cycles modify artefacts
66
- - You do not delete or rewrite feedback history in WORK.md during the foundry flow
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