@really-knows-ai/foundry 3.2.7 → 3.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.
@@ -4,8 +4,118 @@ import { makeCreator } from './factory.js';
4
4
 
5
5
  const KIND = 'cycle';
6
6
 
7
- export const create = makeCreator({
7
+ /** Render a YAML list block: `key:\n - item1\n - item2` */
8
+ function yamlList(key, items) {
9
+ if (!items || items.length === 0) return '';
10
+ const prefix = key.match(/^(\s*)/)[1];
11
+ let block = `${key}:\n`;
12
+ for (const item of items) {
13
+ block += `${prefix} - ${item}\n`;
14
+ }
15
+ return block;
16
+ }
17
+
18
+ /** Render the inputs mapping. */
19
+ function renderInputs(inputs) {
20
+ if (!inputs) return '';
21
+ let block = 'inputs:\n';
22
+ block += ` type: ${inputs.type}\n`;
23
+ block += yamlList(' artefacts', inputs.artefacts);
24
+ return block;
25
+ }
26
+
27
+ /** Render the assay mapping. */
28
+ function renderAssay(assay) {
29
+ if (!assay) return '';
30
+ return 'assay:\n' + yamlList(' extractors', assay.extractors);
31
+ }
32
+
33
+ /** Render the memory mapping. */
34
+ function renderMemory(memory) {
35
+ if (!memory) return '';
36
+ let block = 'memory:\n';
37
+ block += yamlList(' read', memory.read);
38
+ block += yamlList(' write', memory.write);
39
+ return block;
40
+ }
41
+
42
+ /** Render models mapping (flat string key → string value). */
43
+ function renderModels(models) {
44
+ if (!models) return '';
45
+ let block = 'models:\n';
46
+ for (const [key, value] of Object.entries(models)) {
47
+ block += ` ${key}: ${value}\n`;
48
+ }
49
+ return block;
50
+ }
51
+
52
+ /** Render boolean and numeric flags (camelCase to kebab-case). */
53
+ function renderFlags(args) {
54
+ let fm = '';
55
+ if (args.humanAppraise !== undefined) {
56
+ fm += `human-appraise: ${args.humanAppraise}\n`;
57
+ }
58
+ if (args.deadlockAppraise !== undefined) {
59
+ fm += `deadlock-appraise: ${args.deadlockAppraise}\n`;
60
+ }
61
+ if (args.deadlockIterations !== undefined) {
62
+ fm += `deadlock-iterations: ${args.deadlockIterations}\n`;
63
+ }
64
+ if (args.maxIterations !== undefined) {
65
+ fm += `max-iterations: ${args.maxIterations}\n`;
66
+ }
67
+ return fm;
68
+ }
69
+
70
+ /**
71
+ * Assemble the markdown body for a cycle definition from structured arguments.
72
+ *
73
+ * @param {object} args
74
+ * @param {string} args.id Slugged identifier; becomes frontmatter.id.
75
+ * @param {string} args.name Human-readable display name; becomes frontmatter.name.
76
+ * @param {string} args.outputType Artefact type ID this cycle produces.
77
+ * @param {{ type: 'any-of'|'all-of', artefacts: string[] }} [args.inputs] Input contract.
78
+ * @param {string[]} [args.targets] Downstream cycle IDs.
79
+ * @param {boolean} [args.humanAppraise] Include human-appraise in every iteration.
80
+ * @param {boolean} [args.deadlockAppraise] Route to human-appraise on deadlock.
81
+ * @param {number} [args.deadlockIterations] Iteration threshold for deadlock detection.
82
+ * @param {number} [args.maxIterations] Maximum forge iterations.
83
+ * @param {{ extractors: string[] }} [args.assay] Assay stage config.
84
+ * @param {{ read: string[], write: string[] }} [args.memory] Flow memory permissions.
85
+ * @param {object} [args.models] Per-stage model overrides.
86
+ * @param {string} [args.description] Prose placed after frontmatter under ## Cycle.
87
+ * @returns {string} Assembled markdown body.
88
+ */
89
+ export function assembleCycleMarkdown(args) {
90
+ const { id, name, outputType } = args;
91
+
92
+ let fm = `---\nid: ${id}\nname: ${name}\noutput-type: ${outputType}\n`;
93
+
94
+ fm += renderInputs(args.inputs);
95
+ fm += yamlList('targets', args.targets);
96
+ fm += renderFlags(args);
97
+ fm += renderAssay(args.assay);
98
+ fm += renderMemory(args.memory);
99
+ fm += renderModels(args.models);
100
+
101
+ fm += '---';
102
+
103
+ if (args.description) {
104
+ fm += `\n\n## Cycle\n\n${args.description}\n`;
105
+ } else {
106
+ fm += '\n';
107
+ }
108
+
109
+ return fm;
110
+ }
111
+
112
+ const _create = makeCreator({
8
113
  kind: KIND,
9
- pathFor: (args) => join('foundry', 'cycles', `${args.name}.md`),
114
+ pathFor: (args) => join('foundry', 'cycles', `${args.id}.md`),
10
115
  validator: validate,
11
116
  });
117
+
118
+ export async function create(args) {
119
+ const body = assembleCycleMarkdown(args);
120
+ return _create({ ...args, name: args.id, body });
121
+ }
@@ -4,8 +4,33 @@ import { makeCreator } from './factory.js';
4
4
 
5
5
  const KIND = 'flow';
6
6
 
7
- export const create = makeCreator({
7
+ /**
8
+ * Assemble the markdown body for a flow definition from structured arguments.
9
+ *
10
+ * @param {object} args
11
+ * @param {string} args.id Slugged identifier; becomes frontmatter.id.
12
+ * @param {string} args.name Human-readable display name; becomes frontmatter.name.
13
+ * @param {string[]} args.startingCycles Cycle IDs that can start this flow.
14
+ * @param {string} args.description Prose placed under ## Cycles.
15
+ * @returns {string} Assembled markdown body.
16
+ */
17
+ export function assembleFlowMarkdown(args) {
18
+ const { id, name, startingCycles, description } = args;
19
+ let body = `---\nid: ${id}\nname: ${name}\nstarting-cycles:\n`;
20
+ for (const c of startingCycles) {
21
+ body += ` - ${c}\n`;
22
+ }
23
+ body += `---\n\n## Cycles\n\n${description}\n`;
24
+ return body;
25
+ }
26
+
27
+ const _create = makeCreator({
8
28
  kind: KIND,
9
- pathFor: (args) => join('foundry', 'flows', `${args.name}.md`),
29
+ pathFor: (args) => join('foundry', 'flows', `${args.id}.md`),
10
30
  validator: validate,
11
31
  });
32
+
33
+ export async function create(args) {
34
+ const body = assembleFlowMarkdown(args);
35
+ return _create({ ...args, name: args.id, body });
36
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * @file Law markdown assembly helpers for add_law and edit_law tools.
3
+ *
4
+ * Laws have no YAML frontmatter — each law is a `## <id>` block within a
5
+ * markdown file. The block contains a combined name–description line,
6
+ * passing criteria, failing criteria, and an optional validators block.
7
+ */
8
+
9
+ /**
10
+ * Build a validators block string from an array of validator objects.
11
+ *
12
+ * @param {{ id: string, command: string, failureMeans?: string }[]} validators
13
+ * @returns {string} Validators block (empty string if none).
14
+ */
15
+ function buildValidatorsBlock(validators) {
16
+ if (!validators || validators.length === 0) return '';
17
+
18
+ let block = 'validators:\n';
19
+ for (const v of validators) {
20
+ block += ` - id: ${v.id}\n command: ${v.command}`;
21
+ if (v.failureMeans) {
22
+ block += `\n failure-means: ${v.failureMeans}`;
23
+ }
24
+ block += '\n';
25
+ }
26
+ return block.trimEnd();
27
+ }
28
+
29
+ /**
30
+ * Assemble a law markdown block from structured arguments.
31
+ *
32
+ * @param {object} args
33
+ * @param {string} args.id Law identifier; becomes `## <id>` heading.
34
+ * @param {string} args.name Human-readable name.
35
+ * @param {string} args.description Prose describing what the law covers.
36
+ * @param {string} args.passing Criteria defining a passing artefact.
37
+ * @param {string} args.failing Criteria defining a failing artefact.
38
+ * @param {{ id: string, command: string, failureMeans?: string }[]} [args.validators]
39
+ * @returns {string} Assembled law block (no trailing newline).
40
+ */
41
+ export function assembleLawMarkdown(args) {
42
+ const { id, name, description, passing, failing } = args;
43
+ let block = `## ${id}\n\n${name} — ${description}\n\n${passing}\n\n${failing}`;
44
+
45
+ if (args.validators && args.validators.length > 0) {
46
+ block += '\n\n' + buildValidatorsBlock(args.validators);
47
+ }
48
+
49
+ return block;
50
+ }
51
+
52
+ /**
53
+ * Parse a single law block from a body that contains one or more `## <id>`
54
+ * headings. Returns data for the first block found.
55
+ *
56
+ * @param {string} body Full file body.
57
+ * @returns {{ heading: string, headingIndex: number, blockEndIndex: number,
58
+ * proseContent: string, validatorsContent: string } | null}
59
+ */
60
+ function parseLawBlock(body) {
61
+ const headingMatch = body.match(/^(## .+)/m);
62
+ if (!headingMatch) return null;
63
+
64
+ const heading = headingMatch[1];
65
+ const headingIndex = headingMatch.index;
66
+
67
+ // Find the end of this block — next ## at same level or EOF
68
+ const afterHeading = body.slice(headingIndex);
69
+ const nextHeadingRe = /\n(?=## )/;
70
+ const nextMatch = afterHeading.match(nextHeadingRe);
71
+ const blockText = nextMatch
72
+ ? afterHeading.slice(0, nextMatch.index)
73
+ : afterHeading;
74
+ const blockEndIndex = nextMatch
75
+ ? headingIndex + nextMatch.index
76
+ : body.length;
77
+
78
+ // Locate the validators: block within this law block
79
+ const validatorsRe = /^validators:\n((?:[ \t]+\S.*(?:\n|$))*)/m;
80
+ const vMatch = blockText.match(validatorsRe);
81
+
82
+ let proseContent;
83
+ let validatorsContent;
84
+
85
+ if (vMatch) {
86
+ proseContent = blockText.slice(0, vMatch.index);
87
+ validatorsContent = blockText.slice(vMatch.index);
88
+ } else {
89
+ proseContent = blockText;
90
+ validatorsContent = '';
91
+ }
92
+
93
+ return {
94
+ heading, headingIndex, blockEndIndex, proseContent, validatorsContent,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Parse the name–description pair from a single line.
100
+ *
101
+ * Expected format: `<name> — <description>`
102
+ *
103
+ * @param {string} line
104
+ * @returns {{ name: string, description: string }}
105
+ */
106
+ function parseNameDescription(line) {
107
+ const sep = ' — ';
108
+ const sepIndex = line.indexOf(sep);
109
+ if (sepIndex !== -1) {
110
+ return {
111
+ name: line.slice(0, sepIndex).trim(),
112
+ description: line.slice(sepIndex + sep.length).trim(),
113
+ };
114
+ }
115
+ return { name: line.trim(), description: '' };
116
+ }
117
+
118
+ /**
119
+ * Advance index past consecutive blank lines.
120
+ *
121
+ * @param {string[]} lines
122
+ * @param {number} start
123
+ * @returns {number} Index of first non-blank line (or lines.length).
124
+ */
125
+ function skipBlankLines(lines, start) {
126
+ let i = start;
127
+ while (i < lines.length && lines[i].trim() === '') i++;
128
+ return i;
129
+ }
130
+
131
+ /**
132
+ * Collect consecutive non-blank lines starting at `start`.
133
+ *
134
+ * @param {string[]} lines
135
+ * @param {number} start
136
+ * @returns {{ lines: string[], nextIndex: number }}
137
+ */
138
+ function collectNonBlank(lines, start) {
139
+ const collected = [];
140
+ let i = start;
141
+ while (i < lines.length && lines[i].trim() !== '') {
142
+ collected.push(lines[i]);
143
+ i++;
144
+ }
145
+ return { lines: collected, nextIndex: i };
146
+ }
147
+
148
+ /**
149
+ * Parse the prose section of a law block into its constituent fields.
150
+ *
151
+ * Expected prose structure:
152
+ *
153
+ * <name> — <description>
154
+ * (blank line)
155
+ * <passing>
156
+ * (blank line)
157
+ * <failing>
158
+ *
159
+ * Each of passing/failing may be multi-line prose.
160
+ *
161
+ * @param {string} proseContent Block content from heading to validators.
162
+ * @returns {{ name: string, description: string, passing: string, failing: string }}
163
+ */
164
+ function parseLawProse(proseContent) {
165
+ const lines = proseContent.split('\n');
166
+ let i = 0;
167
+
168
+ // Skip heading line
169
+ if (lines[i] && lines[i].startsWith('## ')) i++;
170
+
171
+ // Skip blank lines after heading
172
+ i = skipBlankLines(lines, i);
173
+
174
+ // Name — description line
175
+ const nd = parseNameDescription(lines[i] || '');
176
+ i++;
177
+
178
+ // Skip blank lines, collect passing, skip blank lines again
179
+ i = skipBlankLines(lines, i);
180
+ const passing = collectNonBlank(lines, i);
181
+ i = passing.nextIndex;
182
+
183
+ i = skipBlankLines(lines, i);
184
+
185
+ // Remaining lines form failing
186
+ const failingLines = lines.slice(i);
187
+
188
+ return {
189
+ name: nd.name,
190
+ description: nd.description,
191
+ passing: passing.lines.join('\n'),
192
+ failing: failingLines.join('\n').trimEnd(),
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Build the result block for an edit operation by appending validators.
198
+ *
199
+ * @param {string} newProse Rebuilt prose section.
200
+ * @param {{ validators?: object[] | null }} updates
201
+ * @param {string} existingValidatorsContent Original validators block text.
202
+ * @returns {string} Full new block content.
203
+ */
204
+ function appendValidators(newProse, updates, existingValidatorsContent) {
205
+ if (updates.validators !== undefined) {
206
+ if (updates.validators !== null) {
207
+ return newProse + '\n\n' + buildValidatorsBlock(updates.validators);
208
+ }
209
+ return newProse;
210
+ }
211
+
212
+ const trimmed = existingValidatorsContent.trim();
213
+ if (trimmed) {
214
+ return newProse + '\n\n' + trimmed;
215
+ }
216
+ return newProse;
217
+ }
218
+
219
+ /**
220
+ * Pick a field value from updates, falling back to the existing value.
221
+ *
222
+ * @param {object} updates
223
+ * @param {object} existing
224
+ * @param {string} field
225
+ * @returns {*}
226
+ */
227
+ function pickField(updates, existing, field) {
228
+ return updates[field] !== undefined ? updates[field] : existing[field];
229
+ }
230
+
231
+ /**
232
+ * Update a law block in an existing body with new field values.
233
+ *
234
+ * Only the fields present in `updates` are replaced. Fields not in `updates`
235
+ * retain their original values. Pass `validators: null` to remove the
236
+ * validators block.
237
+ *
238
+ * @param {string} existingBody Full file content (may contain multiple law blocks).
239
+ * @param {{ name?: string, description?: string, passing?: string,
240
+ * failing?: string, validators?: object[] | null }} updates
241
+ * @returns {string} Updated full body with the first law block modified.
242
+ */
243
+ export function assembleEditLawMarkdown(existingBody, updates) {
244
+ const parsed = parseLawBlock(existingBody);
245
+ if (!parsed) {
246
+ throw new Error('Body must contain at least one ## law heading');
247
+ }
248
+
249
+ const { heading, headingIndex, blockEndIndex, proseContent } = parsed;
250
+ const existing = parseLawProse(proseContent);
251
+
252
+ const name = pickField(updates, existing, 'name');
253
+ const description = pickField(updates, existing, 'description');
254
+ const passing = pickField(updates, existing, 'passing');
255
+ const failing = pickField(updates, existing, 'failing');
256
+
257
+ const newProse = `${heading}\n\n${name} — ${description}\n\n${passing}\n\n${failing}`;
258
+ const result = appendValidators(newProse, updates, parsed.validatorsContent);
259
+
260
+ // If there are subsequent blocks, insert a newline separator so the gap
261
+ // between blocks remains `\n\n` (end of result + `\n` + leading `\n` of
262
+ // the remaining body).
263
+ if (blockEndIndex < existingBody.length) {
264
+ return existingBody.slice(0, headingIndex) + result + '\n'
265
+ + existingBody.slice(blockEndIndex);
266
+ }
267
+ return existingBody.slice(0, headingIndex) + result
268
+ + existingBody.slice(blockEndIndex);
269
+ }
@@ -76,19 +76,12 @@ Do not proceed until the user has decided.
76
76
 
77
77
  ### 4. Draft the definition
78
78
 
79
- Present the definition to the user:
79
+ Present the definition to the user with these structured fields:
80
80
 
81
- ```markdown
82
- ---
83
- id: <id>
84
- name: <name>
85
- model: <model-id> # only include if specified
86
- ---
87
-
88
- # <Name>
89
-
90
- <personality description — 2-4 sentences describing how this appraiser thinks, what they care about, and how they approach evaluation>
91
- ```
81
+ - `id` (string) — lowercase, hyphenated identifier
82
+ - `name` (string) — a short character name (e.g., "The Pedant", "The Pragmatist")
83
+ - `description` (string) — 2-4 sentences describing how this appraiser thinks, what they care about, and how they approach evaluation
84
+ - `model` (string, optional) — a specific model ID to use for this appraiser (e.g., `openai/gpt-4o`). Overrides the cycle-level model for the appraise stage. Omit this field to use the cycle's default model.
92
85
 
93
86
  Ask: does this capture the personality correctly?
94
87
 
@@ -101,13 +94,13 @@ Iterate until the user is happy with the personality description. Key things to
101
94
 
102
95
  ### 6. Validate the draft
103
96
 
104
- Call `foundry_config_validate_appraiser({ name: "<id>", body: "<full markdown>" })`.
97
+ Call `foundry_config_validate_appraiser({ name: "<id>", body: "<assembled markdown>" })`. Assemble the body from the fields using the frontmatter format the tool produces internally.
105
98
 
106
99
  If the result is `{ ok: false, errors: [...] }`, address each error (adjust the body) and re-run until you get `{ ok: true }`. Common issues: missing required frontmatter keys, references to artefact types or flows that don't exist yet.
107
100
 
108
101
  ### 7. Create the file
109
102
 
110
- Call `foundry_config_create_appraiser({ name: "<id>", body: "<full markdown>" })`. The tool:
103
+ Call `foundry_config_create_appraiser({ id: "<id>", name: "<name>", description: "<description>" })`. The tool:
111
104
 
112
105
  - re-validates the body (TOCTOU);
113
106
  - writes `foundry/appraisers/<id>.md`;
@@ -80,23 +80,17 @@ Do not proceed until the patterns are non-overlapping.
80
80
 
81
81
  ### 4. Draft the definition
82
82
 
83
- Present the definition to the user:
83
+ Present the definition to the user with these structured fields:
84
84
 
85
- ```markdown
86
- ---
87
- name: <id>
88
- file-patterns:
89
- - "<pattern>"
90
- ---
91
-
92
- ## Definition
85
+ - `id` (string) — lowercase, hyphenated identifier (e.g. `haiku`). Must be unique across artefact types.
86
+ - `name` (string) — human-readable label
87
+ - `filePatterns` (string[]) — glob patterns for files this type produces (forge's write scope is exactly these patterns)
88
+ - `description` (string) — prose description of what this artefact type is
89
+ - `appraisers` ({ count?: number, allowed?: string[] }, optional) — appraiser configuration
93
90
 
94
- <description>
95
- ```
96
-
97
- The `name:` value must exactly match the artefact type's `id`
91
+ The `id` value must exactly match the artefact type's identifier
98
92
  (lowercase, hyphenated). If you want a human-readable label, put it
99
- in the `## Definition` prose.
93
+ in the `name` field.
100
94
 
101
95
  Ask: does this capture the artefact type correctly?
102
96
 
@@ -139,32 +133,24 @@ Ask:
139
133
  > - How many appraisers per foundry cycle? (default: 3)
140
134
  > - Restrict to specific appraiser personalities? (default: all available)
141
135
 
142
- If the user specifies preferences, add an `appraisers` section to the definition frontmatter:
143
-
144
- ```yaml
145
- appraisers:
146
- count: 3 # how many appraisers (default: 3)
147
- allowed: [pedantic, pragmatic] # which personalities (default: all available)
148
- ```
136
+ If the user specifies preferences, include these fields:
149
137
 
150
- If the user is happy with the defaults (3 appraisers, any personality), add just:
138
+ - `appraisers.count` (number, optional, default: 3) how many appraisers per foundry cycle
139
+ - `appraisers.allowed` (string[], optional, default: all available) — whitelist of appraiser personality IDs
151
140
 
152
- ```yaml
153
- appraisers:
154
- count: 3
155
- ```
141
+ If the user is happy with the defaults (3 appraisers, any personality), omit the appraisers configuration entirely.
156
142
 
157
143
  List the available appraisers from `foundry/appraisers/*.md` so the user can see their options.
158
144
 
159
145
  ### 7. Validate the draft
160
146
 
161
- Call `foundry_config_validate_artefact_type({ name: "<id>", body: "<full markdown>" })`.
147
+ Call `foundry_config_validate_artefact_type({ name: "<id>", body: "<assembled markdown>" })`. Assemble the body from the fields using the frontmatter format the tool produces internally.
162
148
 
163
149
  If the result is `{ ok: false, errors: [...] }`, address each error (adjust the body) and re-run until you get `{ ok: true }`. Common issues: missing required frontmatter keys, references to artefact types or flows that don't exist yet.
164
150
 
165
151
  ### 8. Create the file
166
152
 
167
- Call `foundry_config_create_artefact_type({ name: "<id>", body: "<full markdown>" })`. The tool:
153
+ Call `foundry_config_create_artefact_type({ id: "<id>", name: "<name>", filePatterns: ["<pattern>"], description: "<description>" })`. The tool:
168
154
 
169
155
  - re-validates the body (TOCTOU);
170
156
  - writes `foundry/artefacts/<id>/definition.md`;
@@ -120,30 +120,21 @@ If overlap is found, present it and ask the user to confirm the distinction is r
120
120
 
121
121
  ### 9. Draft the definition
122
122
 
123
- Present the foundry cycle definition to the user:
124
-
125
- ```markdown
126
- ---
127
- id: <id>
128
- name: <name>
129
- output-type: <artefact-type-id>
130
- inputs:
131
- type: <any-of|all-of>
132
- artefacts:
133
- - <artefact-type-id>
134
- targets:
135
- - <cycle-id>
136
- human-appraise: <true|false>
137
- deadlock-appraise: <true|false>
138
- deadlock-iterations: <number>
139
- models:
140
- appraise: <model-id>
141
- ---
142
-
143
- # <Name>
144
-
145
- <description>
146
- ```
123
+ Present the foundry cycle definition to the user with these structured fields:
124
+
125
+ - `id` (string) — lowercase, hyphenated identifier
126
+ - `name` (string) — human-readable name
127
+ - `outputType` (string) — the artefact type this cycle produces (must exist in `foundry/artefacts/`)
128
+ - `description` (string) — prose description of what this cycle does
129
+ - `inputs` (object, optional) — input contract. Shape: `{ type: "any-of" | "all-of", artefacts: string[] }`. May be omitted for starting cycles.
130
+ - `targets` (string[], optional) — cycle IDs to route to after completion. May be omitted for terminal cycles.
131
+ - `humanAppraise` (boolean, optional, default: false) — whether a human reviews the artefact every iteration
132
+ - `deadlockAppraise` (boolean, optional, default: true) — whether a human is pulled in when LLM appraisers deadlock
133
+ - `deadlockIterations` (number, optional, default: 5) — deadlock threshold
134
+ - `maxIterations` (number, optional) — maximum iterations before forced progression
135
+ - `assay` (object, optional) — assay configuration
136
+ - `memory` (object, optional) — memory configuration
137
+ - `models` (object, optional) — stage-specific model overrides, e.g. `{ appraise: "openai/gpt-4o" }`
147
138
 
148
139
  Ask: does this capture the foundry cycle correctly?
149
140
 
@@ -160,13 +151,13 @@ For input validation:
160
151
 
161
152
  ### 11. Validate the draft
162
153
 
163
- Call `foundry_config_validate_cycle({ name: "<id>", body: "<full markdown>" })`.
154
+ Call `foundry_config_validate_cycle({ name: "<id>", body: "<assembled markdown>" })`. Assemble the body from the fields using the frontmatter format the tool produces internally.
164
155
 
165
156
  If the result is `{ ok: false, errors: [...] }`, address each error (adjust the body) and re-run until you get `{ ok: true }`. Common issues: missing required frontmatter keys, references to artefact types or flows that don't exist yet.
166
157
 
167
158
  ### 12. Create the cycle file
168
159
 
169
- Call `foundry_config_create_cycle({ name: "<id>", body: "<full markdown>" })`. The tool:
160
+ Call `foundry_config_create_cycle({ id: "<id>", name: "<name>", outputType: "<type>", description: "<description>", inputs: ..., targets: ..., humanAppraise: ..., deadlockAppraise: ..., deadlockIterations: ..., maxIterations: ..., assay: ..., memory: ..., models: ... })`. The tool:
170
161
 
171
162
  - re-validates the body (TOCTOU);
172
163
  - writes `foundry/cycles/<id>.md`;
@@ -67,6 +67,15 @@ Ask only for choices that affect the user's goal or safety. Reuse compatible exi
67
67
 
68
68
  For each definition, use the `foundry_config_validate_*` tool family to validate it first. Resolve any validation errors, then use the corresponding `foundry_config_create_*` tool to create it. Summarise each created file and commit hash in Foundry terms.
69
69
 
70
+ For the flow definition itself, use these structured fields:
71
+
72
+ - `id` (string) — lowercase, hyphenated identifier
73
+ - `name` (string) — human-readable name
74
+ - `description` (string) — prose description of the flow purpose
75
+ - `startingCycles` (string[]) — cycle IDs that begin the flow
76
+
77
+ Call `foundry_config_create_flow({ id: "<id>", name: "<name>", startingCycles: ["<id>"], description: "<description>" })` to create the flow file.
78
+
70
79
  ### 6. Final summary
71
80
 
72
81
  Report the flow, starting cycles, artefact type, laws, validators, appraisers, and files created. Tell the user they can now ask the Foundry agent to run the flow.