@really-knows-ai/foundry 1.0.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 +106 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/docs/concepts.md +55 -0
- package/docs/getting-started.md +78 -0
- package/docs/work-spec.md +193 -0
- package/package.json +44 -0
- package/scripts/lib/tags.js +108 -0
- package/scripts/sort.js +410 -0
- package/scripts/validate-tags.js +54 -0
- package/skills/add-appraiser/SKILL.md +101 -0
- package/skills/add-artefact-type/SKILL.md +147 -0
- package/skills/add-cycle/SKILL.md +131 -0
- package/skills/add-flow/SKILL.md +84 -0
- package/skills/add-law/SKILL.md +99 -0
- package/skills/appraise/SKILL.md +142 -0
- package/skills/cycle/SKILL.md +111 -0
- package/skills/flow/SKILL.md +38 -0
- package/skills/forge/SKILL.md +73 -0
- package/skills/hitl/SKILL.md +65 -0
- package/skills/init-foundry/SKILL.md +51 -0
- package/skills/quench/SKILL.md +55 -0
- package/skills/sort/SKILL.md +77 -0
package/scripts/sort.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sort — deterministic routing for a Foundry Cycle.
|
|
5
|
+
*
|
|
6
|
+
* Reads WORK.md (frontmatter + feedback) and WORK.history.yaml to determine
|
|
7
|
+
* the next stage to execute, or signal completion/blocked.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/sort.js [--work WORK.md] [--history WORK.history.yaml]
|
|
11
|
+
*
|
|
12
|
+
* Output (stdout): a full stage alias (e.g., forge:write-haiku), 'done', or 'blocked'
|
|
13
|
+
* Exit code: 0 on success, 1 on error
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync } from 'fs';
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
import { parseArgs } from 'util';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
import yaml from 'js-yaml';
|
|
22
|
+
import { minimatch } from 'minimatch';
|
|
23
|
+
import { validateTags, extractAllTags } from './lib/tags.js';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Stage helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function baseStage(stage) {
|
|
30
|
+
return stage.split(':')[0];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findFirst(stages, base) {
|
|
34
|
+
for (const s of stages) {
|
|
35
|
+
if (baseStage(s) === base) return s;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nextInRoute(stages, current) {
|
|
41
|
+
const idx = stages.indexOf(current);
|
|
42
|
+
if (idx !== -1 && idx + 1 < stages.length) {
|
|
43
|
+
return stages[idx + 1];
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// I/O boundary — injectable for testing
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
const defaultIO = {
|
|
53
|
+
readFile: (p) => readFileSync(p, 'utf-8'),
|
|
54
|
+
exists: (p) => existsSync(p),
|
|
55
|
+
exec: (cmd) => execSync(cmd, { encoding: 'utf8' }),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Parsing
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
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
|
+
|
|
80
|
+
for (const line of text.split('\n')) {
|
|
81
|
+
const stripped = line.trim();
|
|
82
|
+
|
|
83
|
+
if (stripped === '# Feedback') {
|
|
84
|
+
inFeedback = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (inFeedback && stripped.startsWith('# ') && stripped !== '# Feedback') {
|
|
89
|
+
inFeedback = false;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!inFeedback) continue;
|
|
94
|
+
|
|
95
|
+
if (stripped.startsWith('## ')) {
|
|
96
|
+
currentFile = stripped.slice(3).trim();
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (cycleFiles.has(currentFile) && /^- \[/.test(stripped)) {
|
|
101
|
+
items.push(parseFeedbackItem(stripped));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return items;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseFeedbackItem(line) {
|
|
109
|
+
const item = { raw: line, state: 'unknown', tags: [], resolved: false };
|
|
110
|
+
|
|
111
|
+
if (line.startsWith('- [ ]')) {
|
|
112
|
+
item.state = 'open';
|
|
113
|
+
} else if (line.startsWith('- [x]')) {
|
|
114
|
+
item.state = 'actioned';
|
|
115
|
+
} else if (line.startsWith('- [~]')) {
|
|
116
|
+
item.state = 'wont-fix';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (line.includes('| approved')) {
|
|
120
|
+
item.resolved = true;
|
|
121
|
+
} else if (line.includes('| rejected')) {
|
|
122
|
+
item.state = 'rejected';
|
|
123
|
+
item.resolved = false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
item.tags = extractAllTags(line);
|
|
127
|
+
|
|
128
|
+
return item;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseArtefactsTable(text) {
|
|
132
|
+
const artefacts = [];
|
|
133
|
+
let inTable = false;
|
|
134
|
+
|
|
135
|
+
for (const line of text.split('\n')) {
|
|
136
|
+
const stripped = line.trim();
|
|
137
|
+
|
|
138
|
+
if (stripped.startsWith('| File')) {
|
|
139
|
+
inTable = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (inTable && stripped.startsWith('|---')) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (inTable && stripped.startsWith('|')) {
|
|
146
|
+
const cols = stripped.split('|').slice(1, -1).map(c => c.trim());
|
|
147
|
+
if (cols.length >= 4) {
|
|
148
|
+
artefacts.push({
|
|
149
|
+
file: cols[0],
|
|
150
|
+
type: cols[1],
|
|
151
|
+
cycle: cols[2],
|
|
152
|
+
status: cols[3],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} else if (inTable) {
|
|
156
|
+
inTable = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return artefacts;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadHistory(historyPath, cycle, io = defaultIO) {
|
|
164
|
+
if (!io.exists(historyPath)) return [];
|
|
165
|
+
const data = yaml.load(io.readFile(historyPath)) || [];
|
|
166
|
+
return data.filter(e => e.cycle === cycle);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Routing logic
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
function determineRoute(stages, history, feedback, maxIterations) {
|
|
174
|
+
const forgeCount = history.filter(e => baseStage(e.stage || '') === 'forge').length;
|
|
175
|
+
|
|
176
|
+
const nonSortHistory = history.filter(e => baseStage(e.stage || '') !== 'sort');
|
|
177
|
+
const lastEntry = nonSortHistory.length > 0 ? nonSortHistory[nonSortHistory.length - 1].stage : null;
|
|
178
|
+
const lastBase = lastEntry ? baseStage(lastEntry) : null;
|
|
179
|
+
|
|
180
|
+
if (lastBase === null) return stages[0];
|
|
181
|
+
|
|
182
|
+
if (lastBase === 'hitl') {
|
|
183
|
+
const next = nextInRoute(stages, lastEntry);
|
|
184
|
+
return next ?? 'done';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (lastBase === 'forge') {
|
|
188
|
+
const next = nextInRoute(stages, lastEntry);
|
|
189
|
+
return next ?? 'done';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (lastBase === 'quench') {
|
|
193
|
+
return nextAfterQuench(stages, lastEntry, feedback, forgeCount, maxIterations);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (lastBase === 'appraise') {
|
|
197
|
+
return nextAfterAppraise(stages, feedback, forgeCount, maxIterations);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return 'blocked';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function nextAfterQuench(stages, current, feedback, forgeCount, maxIterations) {
|
|
204
|
+
const needsForge = feedback.some(f => f.state === 'open' || f.state === 'rejected');
|
|
205
|
+
if (needsForge) {
|
|
206
|
+
if (forgeCount >= maxIterations) return 'blocked';
|
|
207
|
+
return findFirst(stages, 'forge') ?? 'blocked';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return nextInRoute(stages, current) ?? 'done';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function nextAfterAppraise(stages, feedback, forgeCount, maxIterations) {
|
|
214
|
+
const needsForge = feedback.some(f => f.state === 'open' || f.state === 'rejected');
|
|
215
|
+
if (needsForge) {
|
|
216
|
+
if (forgeCount >= maxIterations) return 'blocked';
|
|
217
|
+
return findFirst(stages, 'forge') ?? 'blocked';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const pendingApproval = feedback.some(
|
|
221
|
+
f => (f.state === 'actioned' || f.state === 'wont-fix') && !f.resolved
|
|
222
|
+
);
|
|
223
|
+
if (pendingApproval) {
|
|
224
|
+
return findFirst(stages, 'appraise') ?? 'blocked';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return 'done';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// File modification enforcement
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
function getModifiedFiles(cycle, io = defaultIO) {
|
|
235
|
+
try {
|
|
236
|
+
// Find the last sort commit for this cycle to use as the base.
|
|
237
|
+
// This captures ALL files modified since the last sort invocation,
|
|
238
|
+
// even if the stage made multiple commits.
|
|
239
|
+
const log = io.exec('git log --oneline -20');
|
|
240
|
+
const sortPattern = `[${cycle}] sort:`;
|
|
241
|
+
let commitCount = 1;
|
|
242
|
+
let foundSortCommit = false;
|
|
243
|
+
for (const line of log.trim().split('\n')) {
|
|
244
|
+
commitCount++;
|
|
245
|
+
if (line.includes(sortPattern)) {
|
|
246
|
+
foundSortCommit = true;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// If no sort commit found in recent history, fall back to HEAD~1
|
|
251
|
+
if (!foundSortCommit) commitCount = 1;
|
|
252
|
+
const output = io.exec(`git diff --name-only HEAD~${commitCount} HEAD`);
|
|
253
|
+
return output.trim().split('\n').filter(Boolean);
|
|
254
|
+
} catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function globMatch(filePath, pattern) {
|
|
260
|
+
return minimatch(filePath, pattern);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getAllowedPatterns(lastBase, foundryDir, cycleDef, io = defaultIO) {
|
|
264
|
+
const always = ['WORK.md', 'WORK.history.yaml'];
|
|
265
|
+
|
|
266
|
+
if (lastBase !== 'forge') {
|
|
267
|
+
return always;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// For forge: also allow the output artefact's file-patterns
|
|
271
|
+
try {
|
|
272
|
+
const cycleText = io.readFile(cycleDef);
|
|
273
|
+
const cycleFm = parseFrontmatter(cycleText);
|
|
274
|
+
const outputType = cycleFm.output;
|
|
275
|
+
if (!outputType) return always;
|
|
276
|
+
|
|
277
|
+
const artDefPath = `${foundryDir}/artefacts/${outputType}/definition.md`;
|
|
278
|
+
if (!io.exists(artDefPath)) return always;
|
|
279
|
+
|
|
280
|
+
const artText = io.readFile(artDefPath);
|
|
281
|
+
const artFm = parseFrontmatter(artText);
|
|
282
|
+
const filePatterns = artFm['file-patterns'] || [];
|
|
283
|
+
return [...always, ...filePatterns];
|
|
284
|
+
} catch {
|
|
285
|
+
return always;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function checkModifiedFiles(lastBase, foundryDir, cycleDef, cycle, io = defaultIO) {
|
|
290
|
+
const allowedPatterns = getAllowedPatterns(lastBase, foundryDir, cycleDef, io);
|
|
291
|
+
const modified = getModifiedFiles(cycle, io);
|
|
292
|
+
|
|
293
|
+
if (modified.length === 0) {
|
|
294
|
+
return { ok: true, violations: [] };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const violations = modified.filter(f =>
|
|
298
|
+
!allowedPatterns.some(pattern => globMatch(f, pattern))
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return { ok: violations.length === 0, violations };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Exports (for testing) — keep main() private
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
export {
|
|
309
|
+
baseStage,
|
|
310
|
+
findFirst,
|
|
311
|
+
nextInRoute,
|
|
312
|
+
parseFrontmatter,
|
|
313
|
+
parseFeedback,
|
|
314
|
+
parseFeedbackItem,
|
|
315
|
+
parseArtefactsTable,
|
|
316
|
+
loadHistory,
|
|
317
|
+
determineRoute,
|
|
318
|
+
nextAfterQuench,
|
|
319
|
+
nextAfterAppraise,
|
|
320
|
+
globMatch,
|
|
321
|
+
getModifiedFiles,
|
|
322
|
+
getAllowedPatterns,
|
|
323
|
+
checkModifiedFiles,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Main
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
function main() {
|
|
331
|
+
const { values } = parseArgs({
|
|
332
|
+
options: {
|
|
333
|
+
work: { type: 'string', default: 'WORK.md' },
|
|
334
|
+
history: { type: 'string', default: 'WORK.history.yaml' },
|
|
335
|
+
'foundry-dir': { type: 'string', default: 'foundry' },
|
|
336
|
+
'cycle-def': { type: 'string' },
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const workPath = values.work;
|
|
341
|
+
const historyPath = values.history;
|
|
342
|
+
const foundryDir = values['foundry-dir'];
|
|
343
|
+
|
|
344
|
+
if (!existsSync(workPath)) {
|
|
345
|
+
process.stderr.write('ERROR: WORK.md not found\n');
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const workText = readFileSync(workPath, 'utf-8');
|
|
350
|
+
const frontmatter = parseFrontmatter(workText);
|
|
351
|
+
|
|
352
|
+
const cycle = frontmatter.cycle;
|
|
353
|
+
const stages = frontmatter.stages;
|
|
354
|
+
const maxIterations = frontmatter['max-iterations'] ?? 3;
|
|
355
|
+
|
|
356
|
+
if (!cycle) {
|
|
357
|
+
process.stderr.write('ERROR: No cycle in WORK.md frontmatter\n');
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!stages || !Array.isArray(stages)) {
|
|
362
|
+
process.stderr.write('ERROR: No stages in WORK.md frontmatter\n');
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!findFirst(stages, 'forge')) {
|
|
367
|
+
process.stderr.write('ERROR: stages must include at least one forge stage\n');
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const artefacts = parseArtefactsTable(workText);
|
|
372
|
+
const history = loadHistory(historyPath, cycle);
|
|
373
|
+
const feedback = parseFeedback(workText, cycle, artefacts);
|
|
374
|
+
|
|
375
|
+
// --- File modification enforcement ---
|
|
376
|
+
const nonSortHistory = history.filter(e => baseStage(e.stage || '') !== 'sort');
|
|
377
|
+
if (nonSortHistory.length > 0) {
|
|
378
|
+
const lastEntry = nonSortHistory[nonSortHistory.length - 1];
|
|
379
|
+
const lastBase = baseStage(lastEntry.stage || '');
|
|
380
|
+
|
|
381
|
+
// Resolve cycle-def: CLI arg > WORK.md frontmatter field
|
|
382
|
+
const cycleDef = values['cycle-def']
|
|
383
|
+
|| frontmatter['cycle-def']
|
|
384
|
+
|| `${foundryDir}/cycles/${cycle}.md`;
|
|
385
|
+
|
|
386
|
+
const result = checkModifiedFiles(lastBase, foundryDir, cycleDef, cycle);
|
|
387
|
+
if (!result.ok) {
|
|
388
|
+
console.log('violation');
|
|
389
|
+
process.stderr.write(`File modification violation after ${lastBase} stage:\n`);
|
|
390
|
+
result.violations.forEach(f => process.stderr.write(` ${f}\n`));
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// --- Tag validation ---
|
|
396
|
+
const tagErrors = validateTags(workText, foundryDir);
|
|
397
|
+
if (tagErrors.length > 0) {
|
|
398
|
+
console.log('violation');
|
|
399
|
+
process.stderr.write(`Feedback tag validation failed (${tagErrors.length} issue${tagErrors.length > 1 ? 's' : ''}):\n`);
|
|
400
|
+
tagErrors.forEach(e => process.stderr.write(` line ${e.line}: ${e.message}\n`));
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const route = determineRoute(stages, history, feedback, maxIterations);
|
|
405
|
+
console.log(route);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
409
|
+
main();
|
|
410
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate feedback tags in WORK.md.
|
|
5
|
+
*
|
|
6
|
+
* Checks that every feedback item tag:
|
|
7
|
+
* 1. Matches allowed syntax: #validation, #law:<id>, or #hitl
|
|
8
|
+
* 2. For #law:<id>, the law id exists in foundry/laws/*.md or the
|
|
9
|
+
* relevant artefact type's laws.md
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/validate-tags.js [--work WORK.md] [--foundry-dir foundry]
|
|
13
|
+
*
|
|
14
|
+
* Exit 0 and prints "OK" if all tags are valid.
|
|
15
|
+
* Exit 1 and prints invalid items to stderr otherwise.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, existsSync } from 'fs';
|
|
19
|
+
import { parseArgs } from 'util';
|
|
20
|
+
import { validateTags } from './lib/tags.js';
|
|
21
|
+
|
|
22
|
+
function main() {
|
|
23
|
+
const { values } = parseArgs({
|
|
24
|
+
options: {
|
|
25
|
+
work: { type: 'string', default: 'WORK.md' },
|
|
26
|
+
'foundry-dir': { type: 'string', default: 'foundry' },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const workPath = values.work;
|
|
31
|
+
const foundryDir = values['foundry-dir'];
|
|
32
|
+
|
|
33
|
+
if (!existsSync(workPath)) {
|
|
34
|
+
process.stderr.write('ERROR: WORK.md not found\n');
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const workText = readFileSync(workPath, 'utf-8');
|
|
39
|
+
const errors = validateTags(workText, foundryDir);
|
|
40
|
+
|
|
41
|
+
if (errors.length === 0) {
|
|
42
|
+
console.log('OK');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
process.stderr.write(`Tag validation failed (${errors.length} issue${errors.length > 1 ? 's' : ''}):\n`);
|
|
47
|
+
for (const err of errors) {
|
|
48
|
+
process.stderr.write(` line ${err.line}: ${err.message}\n`);
|
|
49
|
+
process.stderr.write(` ${err.raw}\n`);
|
|
50
|
+
}
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-appraiser
|
|
3
|
+
type: atomic
|
|
4
|
+
description: Creates a new appraiser personality, checking for semantic overlap with existing appraisers.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Add Appraiser
|
|
8
|
+
|
|
9
|
+
You help the user create a new appraiser personality. You ensure it's genuinely distinct from existing appraisers and scaffold the definition file.
|
|
10
|
+
|
|
11
|
+
## Protocol
|
|
12
|
+
|
|
13
|
+
### 1. Gather basics
|
|
14
|
+
|
|
15
|
+
From the user's prompt, establish:
|
|
16
|
+
- `id` — lowercase, hyphenated identifier
|
|
17
|
+
- `name` — a short character name (e.g., "The Pedant", "The Pragmatist")
|
|
18
|
+
- `model` — (optional) a specific model ID to use for this appraiser (e.g., `openai/gpt-4o`). Overrides the cycle-level model for the appraise stage. If omitted, the appraiser uses whatever model the cycle's appraise stage is configured with.
|
|
19
|
+
- A prose description of the personality: how they think, what they prioritize, how they evaluate
|
|
20
|
+
|
|
21
|
+
If `id`, `name`, or the personality description are missing, ask. The `model` field is optional — only ask about it if the user mentions wanting a specific model for this appraiser.
|
|
22
|
+
|
|
23
|
+
### 2. Check for id conflicts
|
|
24
|
+
|
|
25
|
+
Read all existing appraiser definitions in `foundry/appraisers/*.md`.
|
|
26
|
+
|
|
27
|
+
- Exact id match → hard conflict, must choose a different id
|
|
28
|
+
|
|
29
|
+
### 3. Check for semantic overlap
|
|
30
|
+
|
|
31
|
+
For each existing appraiser, compare the new personality against it:
|
|
32
|
+
- What does this appraiser prioritize?
|
|
33
|
+
- What lens do they evaluate through?
|
|
34
|
+
- Would two artefacts get meaningfully different feedback from these appraisers?
|
|
35
|
+
|
|
36
|
+
If significant overlap is found, present it to the user:
|
|
37
|
+
|
|
38
|
+
> The new appraiser `<new-id>` seems to overlap with existing appraiser `<existing-id>`:
|
|
39
|
+
> - New: <name> — <personality summary>
|
|
40
|
+
> - Existing: <name> — <personality summary>
|
|
41
|
+
> - Overlap: <what makes them similar>
|
|
42
|
+
>
|
|
43
|
+
> Appraiser diversity matters — similar personalities produce redundant feedback.
|
|
44
|
+
>
|
|
45
|
+
> Options:
|
|
46
|
+
> 1. Proceed anyway (the distinction is meaningful enough)
|
|
47
|
+
> 2. Adjust the new personality to be more distinct
|
|
48
|
+
> 3. Replace the existing appraiser with a revised version
|
|
49
|
+
> 4. Cancel
|
|
50
|
+
|
|
51
|
+
Do not proceed until the user has decided.
|
|
52
|
+
|
|
53
|
+
### 4. Draft the definition
|
|
54
|
+
|
|
55
|
+
Present the definition to the user:
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
---
|
|
59
|
+
id: <id>
|
|
60
|
+
name: <name>
|
|
61
|
+
model: <model-id> # only include if specified
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
# <Name>
|
|
65
|
+
|
|
66
|
+
<personality description — 2-4 sentences describing how this appraiser thinks, what they care about, and how they approach evaluation>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Ask: does this capture the personality correctly?
|
|
70
|
+
|
|
71
|
+
### 5. Refine with the user
|
|
72
|
+
|
|
73
|
+
Iterate until the user is happy with the personality description. Key things to check:
|
|
74
|
+
- Is the personality distinct enough from existing appraisers?
|
|
75
|
+
- Does the description give the LLM enough direction to adopt a consistent voice?
|
|
76
|
+
- Is it clear what this appraiser would flag vs let pass?
|
|
77
|
+
|
|
78
|
+
### 6. Write the file
|
|
79
|
+
|
|
80
|
+
Create `foundry/appraisers/<id>.md` with the agreed definition.
|
|
81
|
+
|
|
82
|
+
### 7. Mention artefact type configuration
|
|
83
|
+
|
|
84
|
+
After creating the appraiser, remind the user:
|
|
85
|
+
|
|
86
|
+
> Appraiser `<id>` is now available. To use it for a specific artefact type, add it to the `appraisers.allowed` list in that type's `definition.md` frontmatter:
|
|
87
|
+
>
|
|
88
|
+
> ```yaml
|
|
89
|
+
> appraisers:
|
|
90
|
+
> count: 3
|
|
91
|
+
> allowed: [<id>, ...]
|
|
92
|
+
> ```
|
|
93
|
+
>
|
|
94
|
+
> If no `allowed` list is specified, all available appraisers (including this new one) are eligible.
|
|
95
|
+
|
|
96
|
+
## What you do NOT do
|
|
97
|
+
|
|
98
|
+
- You do not write files without showing the user first
|
|
99
|
+
- You do not skip the semantic overlap check
|
|
100
|
+
- You do not modify artefact type definitions — that is the user's choice
|
|
101
|
+
- You do not create appraisers with duplicate ids
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-artefact-type
|
|
3
|
+
type: atomic
|
|
4
|
+
description: Creates a new artefact type, checking for conflicts with existing types.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Add Artefact Type
|
|
8
|
+
|
|
9
|
+
You help the user create a new artefact type. You ensure it doesn't conflict with existing types, scaffold the directory structure, and walk the user through defining laws and validation.
|
|
10
|
+
|
|
11
|
+
## Protocol
|
|
12
|
+
|
|
13
|
+
### 1. Gather basics
|
|
14
|
+
|
|
15
|
+
From the user's prompt, establish:
|
|
16
|
+
- `id` — lowercase, hyphenated identifier
|
|
17
|
+
- `name` — human-readable name
|
|
18
|
+
- `file-patterns` — glob patterns for files this type produces
|
|
19
|
+
- `output` — output directory
|
|
20
|
+
- A prose description of what this artefact type is
|
|
21
|
+
|
|
22
|
+
If any of these are missing, ask.
|
|
23
|
+
|
|
24
|
+
### 2. Check for naming conflicts
|
|
25
|
+
|
|
26
|
+
Read all existing artefact type definitions in `foundry/artefacts/*/definition.md`.
|
|
27
|
+
|
|
28
|
+
- Exact id match → hard conflict, must choose a different id
|
|
29
|
+
- Semantically similar name or description → warn the user. Ask:
|
|
30
|
+
|
|
31
|
+
> An artefact type `<existing-id>` already exists that seems similar:
|
|
32
|
+
> - Existing: <name> — <description summary>
|
|
33
|
+
> - New: <name> — <description summary>
|
|
34
|
+
>
|
|
35
|
+
> Is the new type genuinely distinct, or should you extend the existing one?
|
|
36
|
+
|
|
37
|
+
### 3. Check for glob intersection
|
|
38
|
+
|
|
39
|
+
For each existing artefact type, check whether the new type's `file-patterns` could match the same files as any existing type's `file-patterns`.
|
|
40
|
+
|
|
41
|
+
Examples of intersections:
|
|
42
|
+
- `features/*.feature` vs `features/*.feature` — exact overlap
|
|
43
|
+
- `features/**` vs `features/*.feature` — subset overlap
|
|
44
|
+
- `output/*.md` vs `output/reports/*.md` — potential overlap if nested
|
|
45
|
+
|
|
46
|
+
If any intersection is found, this is a hard block:
|
|
47
|
+
|
|
48
|
+
> The file pattern `<new-pattern>` intersects with artefact type `<existing-id>` which uses `<existing-pattern>`.
|
|
49
|
+
>
|
|
50
|
+
> Overlapping file patterns break file modification enforcement — the foundry cycle cannot determine which artefact type owns a file change.
|
|
51
|
+
>
|
|
52
|
+
> Please choose a different file pattern that does not overlap with any existing type.
|
|
53
|
+
|
|
54
|
+
Do not proceed until the patterns are non-overlapping.
|
|
55
|
+
|
|
56
|
+
### 4. Draft the definition
|
|
57
|
+
|
|
58
|
+
Present the definition to the user:
|
|
59
|
+
|
|
60
|
+
```markdown
|
|
61
|
+
---
|
|
62
|
+
id: <id>
|
|
63
|
+
name: <name>
|
|
64
|
+
file-patterns:
|
|
65
|
+
- "<pattern>"
|
|
66
|
+
output: <output-dir>
|
|
67
|
+
appraisers:
|
|
68
|
+
count: 3
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
# <Name>
|
|
72
|
+
|
|
73
|
+
<description>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Ask: does this capture the artefact type correctly?
|
|
77
|
+
|
|
78
|
+
### 5. Laws (optional)
|
|
79
|
+
|
|
80
|
+
Ask:
|
|
81
|
+
|
|
82
|
+
> Do you want to define any type-specific laws for this artefact type? (Global laws in `foundry/laws/` will apply automatically.)
|
|
83
|
+
|
|
84
|
+
If yes, walk through each law using the same format as `add-law`:
|
|
85
|
+
- Draft each law
|
|
86
|
+
- Check for conflicts with global laws and any existing type-specific laws
|
|
87
|
+
- Confirm with the user
|
|
88
|
+
|
|
89
|
+
### 6. Appraisers (optional)
|
|
90
|
+
|
|
91
|
+
Ask:
|
|
92
|
+
|
|
93
|
+
> How should appraisers be configured for this artefact type?
|
|
94
|
+
> - How many appraisers per foundry cycle? (default: 3)
|
|
95
|
+
> - Restrict to specific appraiser personalities? (default: all available)
|
|
96
|
+
|
|
97
|
+
If the user specifies preferences, add an `appraisers` section to the definition frontmatter:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
appraisers:
|
|
101
|
+
count: 3 # how many appraisers (default: 3)
|
|
102
|
+
allowed: [pedantic, pragmatic] # which personalities (default: all available)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If the user is happy with the defaults (3 appraisers, any personality), add just:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
appraisers:
|
|
109
|
+
count: 3
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
List the available appraisers from `foundry/appraisers/*.md` so the user can see their options.
|
|
113
|
+
|
|
114
|
+
### 7. Validation (optional)
|
|
115
|
+
|
|
116
|
+
Ask:
|
|
117
|
+
|
|
118
|
+
> Do you want to define any deterministic validation commands for this artefact type?
|
|
119
|
+
|
|
120
|
+
If yes, walk through each validation entry:
|
|
121
|
+
- A `## heading` (identifier)
|
|
122
|
+
- A `Command:` line with `{file}` placeholder
|
|
123
|
+
- A `Failure means:` line explaining what a non-zero exit indicates
|
|
124
|
+
|
|
125
|
+
### 8. Scaffold
|
|
126
|
+
|
|
127
|
+
Create the directory and files:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
foundry/artefacts/<id>/
|
|
131
|
+
definition.md # always created
|
|
132
|
+
laws.md # created if laws were defined
|
|
133
|
+
validation.md # created if validation commands were defined
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If laws or validation were skipped, do not create empty files.
|
|
137
|
+
|
|
138
|
+
### 9. Confirm
|
|
139
|
+
|
|
140
|
+
Show the user the complete file listing and contents. Confirm before writing.
|
|
141
|
+
|
|
142
|
+
## What you do NOT do
|
|
143
|
+
|
|
144
|
+
- You do not create artefact types with overlapping file patterns — this is a hard block
|
|
145
|
+
- You do not write files without showing the user first
|
|
146
|
+
- You do not skip the naming or glob checks
|
|
147
|
+
- You do not create laws without checking for conflicts (delegate to add-law pattern)
|