@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.
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +91 -19
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +111 -108
- package/dist/CHANGELOG.md +30 -0
- package/dist/scripts/lib/config-creators/appraiser.js +29 -2
- package/dist/scripts/lib/config-creators/artefact-type.js +49 -2
- package/dist/scripts/lib/config-creators/cycle.js +112 -2
- package/dist/scripts/lib/config-creators/flow.js +27 -2
- package/dist/scripts/lib/config-creators/law.js +269 -0
- package/dist/skills/add-appraiser/SKILL.md +7 -14
- package/dist/skills/add-artefact-type/SKILL.md +14 -28
- package/dist/skills/add-cycle/SKILL.md +17 -26
- package/dist/skills/add-flow/SKILL.md +9 -0
- package/dist/skills/add-law/SKILL.md +17 -22
- package/package.json +1 -1
|
@@ -4,8 +4,118 @@ import { makeCreator } from './factory.js';
|
|
|
4
4
|
|
|
5
5
|
const KIND = 'cycle';
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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: "<
|
|
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({
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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: "<
|
|
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({
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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: "<
|
|
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({
|
|
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.
|