@really-knows-ai/foundry 2.2.1 → 2.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/.opencode/plugins/foundry.js +101 -243
- package/CHANGELOG.md +35 -0
- package/docs/work-spec.md +4 -4
- package/package.json +1 -1
- package/scripts/orchestrate.js +418 -0
- package/skills/flow/SKILL.md +11 -9
- package/skills/forge/SKILL.md +15 -5
- package/skills/human-appraise/SKILL.md +12 -0
- package/skills/orchestrate/SKILL.md +69 -0
- package/skills/upgrade-foundry/SKILL.md +31 -0
- package/skills/cycle/SKILL.md +0 -87
- package/skills/sort/SKILL.md +0 -111
|
@@ -12,13 +12,12 @@ import fs from 'fs';
|
|
|
12
12
|
import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync, mkdirSync } from 'fs';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import { tool } from '@opencode-ai/plugin';
|
|
15
|
-
import { loadHistory
|
|
16
|
-
import { parseFrontmatter, createWorkfile,
|
|
15
|
+
import { loadHistory } from '../../scripts/lib/history.js';
|
|
16
|
+
import { parseFrontmatter, createWorkfile, enrichStages, parseModelsValue } from '../../scripts/lib/workfile.js';
|
|
17
17
|
import { parseArtefactsTable, addArtefactRow, setArtefactStatus } from '../../scripts/lib/artefacts.js';
|
|
18
18
|
import { addFeedbackItem, actionFeedbackItem, wontfixFeedbackItem, resolveFeedbackItem, listFeedback } from '../../scripts/lib/feedback.js';
|
|
19
19
|
import { getCycleDefinition, getArtefactType, getLaws, getValidation, getAppraisers, getFlow, selectAppraisers } from '../../scripts/lib/config.js';
|
|
20
20
|
import { slugify } from '../../scripts/lib/slug.js';
|
|
21
|
-
import { runSort } from '../../scripts/sort.js';
|
|
22
21
|
import { execSync, execFileSync } from 'child_process';
|
|
23
22
|
import { createHash, randomUUID } from 'node:crypto';
|
|
24
23
|
import { readOrCreateSecret } from '../../scripts/lib/secret.js';
|
|
@@ -103,7 +102,7 @@ new skills). It does NOT apply to running an existing, defined flow.
|
|
|
103
102
|
|
|
104
103
|
## Available skills
|
|
105
104
|
|
|
106
|
-
- **Pipeline:** forge, quench, appraise,
|
|
105
|
+
- **Pipeline:** forge, quench, appraise, orchestrate, flow, human-appraise
|
|
107
106
|
- **Authoring:** add-artefact-type, add-law, add-appraiser, add-cycle, add-flow, init-foundry
|
|
108
107
|
- **Maintenance:** upgrade-foundry, refresh-agents, list-agents
|
|
109
108
|
|
|
@@ -127,6 +126,12 @@ function makeIO(directory) {
|
|
|
127
126
|
readDir: (p) => readdirSync(resolve(p)),
|
|
128
127
|
mkdir: (p) => mkdirSync(resolve(p), { recursive: true }),
|
|
129
128
|
unlink: (p) => { if (existsSync(resolve(p))) unlinkSync(resolve(p)); },
|
|
129
|
+
// exec: run a shell command in the worktree and return stdout as a UTF-8 string.
|
|
130
|
+
// Used by sort.js (getDirtyToolManagedFiles, getModifiedFiles) for git enforcement.
|
|
131
|
+
// Call sites pass full shell strings (e.g. 'git status --porcelain ...'), so we
|
|
132
|
+
// must use execSync rather than execFileSync. Throws on non-zero exit; callers
|
|
133
|
+
// already wrap in try/catch.
|
|
134
|
+
exec: (cmd) => execSync(cmd, { cwd: directory, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }),
|
|
130
135
|
};
|
|
131
136
|
}
|
|
132
137
|
|
|
@@ -165,35 +170,6 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
165
170
|
|
|
166
171
|
tool: {
|
|
167
172
|
// ── History tools ──
|
|
168
|
-
foundry_history_append: tool({
|
|
169
|
-
description: 'Append an entry to the cycle history (WORK.history.yaml)',
|
|
170
|
-
args: {
|
|
171
|
-
cycle: tool.schema.string().describe('Cycle name'),
|
|
172
|
-
stage: tool.schema.string().describe('Stage name'),
|
|
173
|
-
comment: tool.schema.string().describe('Comment for this entry'),
|
|
174
|
-
route: tool.schema.string().optional().describe('When stage=sort, the stage alias the sort routed to'),
|
|
175
|
-
},
|
|
176
|
-
async execute(args, context) {
|
|
177
|
-
const io = makeIO(context.worktree);
|
|
178
|
-
const guard = requireNoActiveStage(io);
|
|
179
|
-
if (!guard.ok) return JSON.stringify({ error: `foundry_history_append ${guard.error}` });
|
|
180
|
-
const historyPath = path.join(context.worktree, 'WORK.history.yaml');
|
|
181
|
-
// Sort-route alias check: if this entry is NOT a sort, it must match
|
|
182
|
-
// the most recent sort's `route` for this cycle.
|
|
183
|
-
if (args.stage !== 'sort') {
|
|
184
|
-
const expected = readLastSortRoute(historyPath, args.cycle, io);
|
|
185
|
-
if (args.stage !== expected) {
|
|
186
|
-
return JSON.stringify({
|
|
187
|
-
error: `foundry_history_append: stage ${args.stage} does not match last sort route ${expected ?? 'none'}`,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
const iteration = getIteration(historyPath, args.cycle, io);
|
|
192
|
-
appendEntry(historyPath, { cycle: args.cycle, stage: args.stage, iteration, comment: args.comment, route: args.route }, io);
|
|
193
|
-
return JSON.stringify({ ok: true, iteration });
|
|
194
|
-
},
|
|
195
|
-
}),
|
|
196
|
-
|
|
197
173
|
foundry_history_list: tool({
|
|
198
174
|
description: 'List history entries for a cycle',
|
|
199
175
|
args: {
|
|
@@ -269,57 +245,6 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
269
245
|
},
|
|
270
246
|
}),
|
|
271
247
|
|
|
272
|
-
foundry_stage_finalize: tool({
|
|
273
|
-
description: 'Verify stage output matches allowed file patterns; register artefacts as drafts.',
|
|
274
|
-
args: {
|
|
275
|
-
cycle: tool.schema.string().describe('Cycle name'),
|
|
276
|
-
},
|
|
277
|
-
async execute(args, context) {
|
|
278
|
-
const io = makeIO(context.worktree);
|
|
279
|
-
const guard = requireNoActiveStage(io);
|
|
280
|
-
if (!guard.ok) return JSON.stringify({ error: guard.error });
|
|
281
|
-
const last = readLastStage(io);
|
|
282
|
-
if (!last) return JSON.stringify({ error: 'foundry_stage_finalize: no last stage recorded; call stage_end first' });
|
|
283
|
-
if (last.cycle !== args.cycle) {
|
|
284
|
-
return JSON.stringify({ error: `foundry_stage_finalize: cycle mismatch (last=${last.cycle}, got=${args.cycle})` });
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Load on-disk definitions and translate to finalizeStage's camelCase contract.
|
|
288
|
-
let cycleDoc;
|
|
289
|
-
try {
|
|
290
|
-
cycleDoc = await getCycleDefinition('foundry', args.cycle, io);
|
|
291
|
-
} catch (e) {
|
|
292
|
-
return JSON.stringify({ error: `foundry_stage_finalize: ${e.message}` });
|
|
293
|
-
}
|
|
294
|
-
const outputType = cycleDoc.frontmatter.output;
|
|
295
|
-
const cycleDef = { outputArtefactType: outputType };
|
|
296
|
-
const artefactTypes = {};
|
|
297
|
-
if (outputType) {
|
|
298
|
-
try {
|
|
299
|
-
const artDoc = await getArtefactType('foundry', outputType, io);
|
|
300
|
-
artefactTypes[outputType] = { filePatterns: artDoc.frontmatter['file-patterns'] || [] };
|
|
301
|
-
} catch {
|
|
302
|
-
artefactTypes[outputType] = { filePatterns: [] };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const workPath = path.join(context.worktree, 'WORK.md');
|
|
307
|
-
const result = finalizeStage({
|
|
308
|
-
cwd: context.worktree,
|
|
309
|
-
baseSha: last.baseSha,
|
|
310
|
-
stageBase: stageBaseOf(last.stage),
|
|
311
|
-
cycleDef,
|
|
312
|
-
artefactTypes,
|
|
313
|
-
registerArtefact: ({ file, type, status }) => {
|
|
314
|
-
const text = readFileSync(workPath, 'utf-8');
|
|
315
|
-
const updated = addArtefactRow(text, { file, type, cycle: args.cycle, status });
|
|
316
|
-
writeFileSync(workPath, updated, 'utf-8');
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
return JSON.stringify(result);
|
|
320
|
-
},
|
|
321
|
-
}),
|
|
322
|
-
|
|
323
248
|
// ── Workfile tools ──
|
|
324
249
|
foundry_workfile_create: tool({
|
|
325
250
|
description: 'Create WORK.md with frontmatter and goal',
|
|
@@ -371,137 +296,111 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
371
296
|
},
|
|
372
297
|
}),
|
|
373
298
|
|
|
374
|
-
|
|
375
|
-
description: '
|
|
299
|
+
foundry_workfile_delete: tool({
|
|
300
|
+
description: 'Delete WORK.md and WORK.history.yaml (requires confirm:true)',
|
|
376
301
|
args: {
|
|
377
|
-
|
|
378
|
-
value: tool.schema.string().describe('Value to set (use JSON for arrays/objects, e.g. \'["forge:a","quench:b"]\' or \'{"forge":"openai/gpt-4o"}\')'),
|
|
302
|
+
confirm: tool.schema.boolean().describe('Must be true to confirm deletion'),
|
|
379
303
|
},
|
|
380
304
|
async execute(args, context) {
|
|
381
305
|
const io = makeIO(context.worktree);
|
|
382
306
|
const guard = requireNoActiveStage(io);
|
|
383
|
-
if (!guard.ok) return JSON.stringify({ error: `
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return JSON.stringify({ error: `foundry_workfile_set: key must be one of cycle|stages|max-iterations|models; got ${args.key}` });
|
|
307
|
+
if (!guard.ok) return JSON.stringify({ error: `foundry_workfile_delete ${guard.error}` });
|
|
308
|
+
if (args.confirm !== true) {
|
|
309
|
+
return JSON.stringify({ error: 'foundry_workfile_delete requires {confirm: true}' });
|
|
387
310
|
}
|
|
388
311
|
const workPath = path.join(context.worktree, 'WORK.md');
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const text = readFileSync(workPath, 'utf-8');
|
|
393
|
-
// Parse JSON values for arrays/objects, keep strings as-is
|
|
394
|
-
let value = args.value;
|
|
395
|
-
if (args.key === 'stages') {
|
|
396
|
-
// Always parse stages into an array (handles JSON arrays and comma-separated strings)
|
|
397
|
-
value = parseStagesValue(args.value);
|
|
398
|
-
} else if (args.key === 'models') {
|
|
399
|
-
// Always parse models into an object (handles JSON objects and "key: value" strings)
|
|
400
|
-
value = parseModelsValue(args.value);
|
|
401
|
-
} else {
|
|
402
|
-
try {
|
|
403
|
-
const parsed = JSON.parse(args.value);
|
|
404
|
-
if (typeof parsed === 'object' || Array.isArray(parsed) || typeof parsed === 'number') {
|
|
405
|
-
value = parsed;
|
|
406
|
-
}
|
|
407
|
-
} catch {
|
|
408
|
-
// Not JSON, use as plain string
|
|
409
|
-
}
|
|
312
|
+
const historyPath = path.join(context.worktree, 'WORK.history.yaml');
|
|
313
|
+
if (existsSync(workPath)) {
|
|
314
|
+
unlinkSync(workPath);
|
|
410
315
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const fm = parseFrontmatter(text);
|
|
414
|
-
if (fm.cycle) {
|
|
415
|
-
value = enrichStages(value, fm.cycle);
|
|
416
|
-
}
|
|
316
|
+
if (existsSync(historyPath)) {
|
|
317
|
+
unlinkSync(historyPath);
|
|
417
318
|
}
|
|
418
|
-
const updated = setFrontmatterField(text, args.key, value);
|
|
419
|
-
writeFileSync(workPath, updated, 'utf-8');
|
|
420
319
|
return JSON.stringify({ ok: true });
|
|
421
320
|
},
|
|
422
321
|
}),
|
|
423
322
|
|
|
424
|
-
|
|
425
|
-
|
|
323
|
+
// ── Orchestrate tool ──
|
|
324
|
+
foundry_orchestrate: tool({
|
|
325
|
+
description: 'Run the next step of the current cycle. Call with no args on first invocation; call with lastResult={ok,error?} after a dispatch/human_appraise completes. Returns {action, ...} describing what the caller should do next.',
|
|
426
326
|
args: {
|
|
427
|
-
|
|
428
|
-
|
|
327
|
+
lastResult: tool.schema.object({
|
|
328
|
+
ok: tool.schema.boolean(),
|
|
329
|
+
error: tool.schema.string().optional(),
|
|
330
|
+
}).optional(),
|
|
331
|
+
cycleDef: tool.schema.string().optional().describe('Test-mode cycle definition override (path to cycle file)'),
|
|
429
332
|
},
|
|
430
333
|
async execute(args, context) {
|
|
334
|
+
const { runOrchestrate } = await import('../../scripts/orchestrate.js');
|
|
431
335
|
const io = makeIO(context.worktree);
|
|
432
|
-
const
|
|
433
|
-
if (!guard.ok) return JSON.stringify({ error: `foundry_workfile_configure_from_cycle ${guard.error}` });
|
|
336
|
+
const cwd = context.worktree;
|
|
434
337
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
338
|
+
// Mint: same pattern as removed foundry_sort.
|
|
339
|
+
const mint = ({ route, cycle, exp }) => {
|
|
340
|
+
const nonce = randomUUID();
|
|
341
|
+
const payload = { route, cycle, nonce, exp };
|
|
342
|
+
pending.add(nonce, payload);
|
|
343
|
+
return signToken(payload, secret);
|
|
344
|
+
};
|
|
439
345
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const fm = cycleDoc.frontmatter || {};
|
|
447
|
-
|
|
448
|
-
const stages = enrichStages(args.stages, args.cycleId);
|
|
449
|
-
|
|
450
|
-
// Apply defaults for each cycle-def-backed field.
|
|
451
|
-
const maxIterations = fm['max-iterations'] ?? 3;
|
|
452
|
-
const humanAppraise = fm['human-appraise'] === true;
|
|
453
|
-
const deadlockAppraise = fm['deadlock-appraise'] !== false; // default true
|
|
454
|
-
const deadlockIterations = fm['deadlock-iterations'] ?? 5;
|
|
455
|
-
const models = fm.models ?? null;
|
|
456
|
-
|
|
457
|
-
let text = readFileSync(workPath, 'utf-8');
|
|
458
|
-
text = setFrontmatterField(text, 'cycle', args.cycleId);
|
|
459
|
-
text = setFrontmatterField(text, 'stages', stages);
|
|
460
|
-
text = setFrontmatterField(text, 'max-iterations', maxIterations);
|
|
461
|
-
text = setFrontmatterField(text, 'human-appraise', humanAppraise);
|
|
462
|
-
text = setFrontmatterField(text, 'deadlock-appraise', deadlockAppraise);
|
|
463
|
-
text = setFrontmatterField(text, 'deadlock-iterations', deadlockIterations);
|
|
464
|
-
if (models && typeof models === 'object' && Object.keys(models).length > 0) {
|
|
465
|
-
text = setFrontmatterField(text, 'models', models);
|
|
466
|
-
}
|
|
467
|
-
writeFileSync(workPath, text, 'utf-8');
|
|
468
|
-
|
|
469
|
-
return JSON.stringify({
|
|
470
|
-
ok: true,
|
|
471
|
-
applied: {
|
|
472
|
-
cycle: args.cycleId,
|
|
473
|
-
stages,
|
|
474
|
-
'max-iterations': maxIterations,
|
|
475
|
-
'human-appraise': humanAppraise,
|
|
476
|
-
'deadlock-appraise': deadlockAppraise,
|
|
477
|
-
'deadlock-iterations': deadlockIterations,
|
|
478
|
-
...(models ? { models } : {}),
|
|
346
|
+
// Git bridge: commit staged changes with a cycle-prefixed message.
|
|
347
|
+
const git = {
|
|
348
|
+
commit: (msg) => {
|
|
349
|
+
execFileSync('git', ['add', '.'], { cwd, encoding: 'utf8' });
|
|
350
|
+
execFileSync('git', ['commit', '-m', msg], { cwd, encoding: 'utf8' });
|
|
351
|
+
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd, encoding: 'utf8' }).trim();
|
|
479
352
|
},
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
353
|
+
status: () => {
|
|
354
|
+
const out = execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8' }).trim();
|
|
355
|
+
return { clean: out === '', dirty: out.split('\n').filter(Boolean) };
|
|
356
|
+
},
|
|
357
|
+
};
|
|
483
358
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
359
|
+
// Finalize bridge: mimics the deleted foundry_stage_finalize body.
|
|
360
|
+
const finalize = async ({ cycleId, stage, baseSha }) => {
|
|
361
|
+
let cycleDoc;
|
|
362
|
+
try {
|
|
363
|
+
cycleDoc = await getCycleDefinition('foundry', cycleId, io);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
return { ok: false, error: e.message };
|
|
366
|
+
}
|
|
367
|
+
const outputType = cycleDoc.frontmatter.output;
|
|
368
|
+
const cycleDef = { outputArtefactType: outputType };
|
|
369
|
+
const artefactTypes = {};
|
|
370
|
+
if (outputType) {
|
|
371
|
+
try {
|
|
372
|
+
const artDoc = await getArtefactType('foundry', outputType, io);
|
|
373
|
+
artefactTypes[outputType] = { filePatterns: artDoc.frontmatter['file-patterns'] || [] };
|
|
374
|
+
} catch {
|
|
375
|
+
artefactTypes[outputType] = { filePatterns: [] };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const workPath = path.join(cwd, 'WORK.md');
|
|
379
|
+
const result = finalizeStage({
|
|
380
|
+
cwd,
|
|
381
|
+
baseSha,
|
|
382
|
+
stageBase: stageBaseOf(stage),
|
|
383
|
+
cycleDef,
|
|
384
|
+
artefactTypes,
|
|
385
|
+
registerArtefact: ({ file, type, status }) => {
|
|
386
|
+
const text = readFileSync(workPath, 'utf-8');
|
|
387
|
+
const updated = addArtefactRow(text, { file, type, cycle: cycleId, status });
|
|
388
|
+
writeFileSync(workPath, updated, 'utf-8');
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
return result;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const result = await runOrchestrate({
|
|
396
|
+
cwd, cycleDef: args.cycleDef, git, mint, finalize,
|
|
397
|
+
now: () => Date.now(),
|
|
398
|
+
lastResult: args.lastResult ?? null,
|
|
399
|
+
}, io);
|
|
400
|
+
return JSON.stringify(result);
|
|
401
|
+
} catch (e) {
|
|
402
|
+
return JSON.stringify({ action: 'violation', details: `orchestrate threw: ${e.message}`, recoverable: false, affected_files: [] });
|
|
503
403
|
}
|
|
504
|
-
return JSON.stringify({ ok: true });
|
|
505
404
|
},
|
|
506
405
|
}),
|
|
507
406
|
|
|
@@ -672,27 +571,6 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
672
571
|
},
|
|
673
572
|
}),
|
|
674
573
|
|
|
675
|
-
// ── Sort tool ──
|
|
676
|
-
foundry_sort: tool({
|
|
677
|
-
description: 'Determine the next stage for the current cycle and (if dispatchable) mint a single-use token.',
|
|
678
|
-
args: {
|
|
679
|
-
cycleDef: tool.schema.string().optional().describe('Path to cycle definition file'),
|
|
680
|
-
},
|
|
681
|
-
async execute(args, context) {
|
|
682
|
-
const io = makeIO(context.worktree);
|
|
683
|
-
const guard = requireNoActiveStage(io);
|
|
684
|
-
if (!guard.ok) return JSON.stringify({ error: `foundry_sort ${guard.error}` });
|
|
685
|
-
const mint = ({ route, cycle, exp }) => {
|
|
686
|
-
const nonce = randomUUID();
|
|
687
|
-
const payload = { route, cycle, nonce, exp };
|
|
688
|
-
pending.add(nonce, payload);
|
|
689
|
-
return signToken(payload, secret);
|
|
690
|
-
};
|
|
691
|
-
const result = runSort({ cycleDef: args.cycleDef, mint }, io);
|
|
692
|
-
return JSON.stringify(result);
|
|
693
|
-
},
|
|
694
|
-
}),
|
|
695
|
-
|
|
696
574
|
// ── Git tools ──
|
|
697
575
|
foundry_git_branch: tool({
|
|
698
576
|
description: 'Create and checkout a work branch for a flow',
|
|
@@ -712,25 +590,6 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
712
590
|
},
|
|
713
591
|
}),
|
|
714
592
|
|
|
715
|
-
foundry_git_commit: tool({
|
|
716
|
-
description: 'Stage all changes and commit with a cycle-prefixed message',
|
|
717
|
-
args: {
|
|
718
|
-
cycle: tool.schema.string().describe('Cycle name'),
|
|
719
|
-
stage: tool.schema.string().describe('Stage name'),
|
|
720
|
-
description: tool.schema.string().describe('Commit description'),
|
|
721
|
-
},
|
|
722
|
-
async execute(args, context) {
|
|
723
|
-
const io = makeIO(context.worktree);
|
|
724
|
-
const guard = requireNoActiveStage(io);
|
|
725
|
-
if (!guard.ok) return JSON.stringify({ error: `foundry_git_commit ${guard.error}` });
|
|
726
|
-
execSync('git add .', { cwd: context.worktree, encoding: 'utf8' });
|
|
727
|
-
const msg = `[${args.cycle}] ${args.stage}: ${args.description}`;
|
|
728
|
-
execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: context.worktree, encoding: 'utf8' });
|
|
729
|
-
const hash = execSync('git rev-parse --short HEAD', { cwd: context.worktree, encoding: 'utf8' }).trim();
|
|
730
|
-
return JSON.stringify({ ok: true, hash });
|
|
731
|
-
},
|
|
732
|
-
}),
|
|
733
|
-
|
|
734
593
|
foundry_git_finish: tool({
|
|
735
594
|
description: 'Clean up work files, squash merge to base branch, and delete the work branch',
|
|
736
595
|
args: {
|
|
@@ -746,7 +605,7 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
746
605
|
const opts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
747
606
|
|
|
748
607
|
// Get current branch name
|
|
749
|
-
const workBranch =
|
|
608
|
+
const workBranch = execFileSync('git', ['branch', '--show-current'], opts).trim();
|
|
750
609
|
if (workBranch === base) {
|
|
751
610
|
return JSON.stringify({ error: `Already on ${base} — nothing to merge` });
|
|
752
611
|
}
|
|
@@ -759,23 +618,22 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
759
618
|
|
|
760
619
|
// Commit cleanup if there are changes
|
|
761
620
|
try {
|
|
762
|
-
|
|
763
|
-
const status =
|
|
621
|
+
execFileSync('git', ['add', '-A'], opts);
|
|
622
|
+
const status = execFileSync('git', ['status', '--porcelain'], opts).trim();
|
|
764
623
|
if (status) {
|
|
765
624
|
const cleanupMsg = `[${workBranch.replace('work/', '')}] cleanup: remove work files`;
|
|
766
|
-
|
|
625
|
+
execFileSync('git', ['commit', '-m', cleanupMsg], opts);
|
|
767
626
|
}
|
|
768
627
|
} catch { /* no changes to commit */ }
|
|
769
628
|
|
|
770
629
|
// Switch to base and squash merge
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const hash = execSync('git rev-parse --short HEAD', opts).trim();
|
|
630
|
+
execFileSync('git', ['checkout', base], opts);
|
|
631
|
+
execFileSync('git', ['merge', '--squash', workBranch], opts);
|
|
632
|
+
execFileSync('git', ['commit', '-m', args.message], opts);
|
|
633
|
+
const hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], opts).trim();
|
|
776
634
|
|
|
777
635
|
// Force-delete work branch (required after squash)
|
|
778
|
-
|
|
636
|
+
execFileSync('git', ['branch', '-D', workBranch], opts);
|
|
779
637
|
|
|
780
638
|
return JSON.stringify({ ok: true, hash, branch: base });
|
|
781
639
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.1 — 2026-04-20
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `flow` skill: any cycle in a flow may now be the starting cycle (previously limited to `starting-cycles`). The list becomes a hint for ambiguous requests. A cycle whose `inputs` contract cannot be satisfied from files on disk is not eligible to start.
|
|
8
|
+
- `flow` skill: between-cycles logic no longer implies any carry-over ceremony. The next cycle's forge discovers the previous cycle's output via filesystem scan against its input types' `file-patterns`.
|
|
9
|
+
- `forge` skill: input discovery now explicitly uses filesystem scan against each input type's `file-patterns`, with the goal guiding which candidates are relevant.
|
|
10
|
+
- `forge` skill: the write invariant is restated accurately — forge may only write to files matching the output artefact type's `file-patterns` (plus the tool-managed files). All other files on disk are read-only. The previous "inputs are read-only" framing was a special case of this rule.
|
|
11
|
+
|
|
12
|
+
### Notes
|
|
13
|
+
|
|
14
|
+
- No tool, schema, or enforcement changes. Existing flows continue to work. `sort.js`'s `checkModifiedFiles` already enforces the write invariant.
|
|
15
|
+
|
|
16
|
+
## 2.3.0 — 2026-04-20
|
|
17
|
+
|
|
18
|
+
### Breaking
|
|
19
|
+
|
|
20
|
+
- **LLM orchestration replaced with deterministic `foundry_orchestrate` tool.** The `cycle` and `sort` skills are removed; replaced by a single thin `orchestrate` skill that drives a 3-line loop.
|
|
21
|
+
- **Six tools deregistered** from the plugin (still exist as internal imports for tests): `foundry_sort`, `foundry_history_append`, `foundry_stage_finalize`, `foundry_git_commit`, `foundry_workfile_configure_from_cycle`, `foundry_workfile_set`.
|
|
22
|
+
- Upgrade requires clean main + no in-flight workfile (see `upgrade-foundry` skill).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `foundry_orchestrate` — single tool that owns the sort → history → dispatch → finalize → history → commit loop. Atomic stage completion.
|
|
27
|
+
- `scripts/orchestrate.js` — deterministic orchestration logic, composes existing internal functions.
|
|
28
|
+
- Orphaned-stage detection: if orchestrate is called without `lastResult` but an active stage exists, returns `violation`. Fixes the ses_256c failure mode where an LLM skipped the post-dispatch history append and wedged the cycle.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Root cause of all deferred HARDEN.md bugs (B, C, D, E, G) and the ses_256c bug: LLM misfollowing a deterministic protocol. Protocol now lives inside the plugin tool.
|
|
33
|
+
|
|
34
|
+
### Migration
|
|
35
|
+
|
|
36
|
+
See `skills/upgrade-foundry/SKILL.md` for v2.3.0 pre-flight checks. No automated state migration — complete or discard in-flight cycles on v2.2.x before upgrading.
|
|
37
|
+
|
|
3
38
|
## 2.2.1 — 2026-04-20
|
|
4
39
|
|
|
5
40
|
Follow-up patch addressing the five bugs deferred from v2.2.0 (see `HARDEN.md` §Deferred).
|
package/docs/work-spec.md
CHANGED
|
@@ -25,8 +25,8 @@ The `stages` list is the happy path. Sort follows it but loops back to `forge` w
|
|
|
25
25
|
|
|
26
26
|
- `flow` — set by the foundry flow skill at foundry flow start, never changes
|
|
27
27
|
- `cycle` — set by the foundry flow skill when starting each foundry cycle
|
|
28
|
-
- `stages` — set by the
|
|
29
|
-
- `max-iterations` — set by the
|
|
28
|
+
- `stages` — set by the orchestrate skill when starting each foundry cycle (reads artefact type to determine if quench is needed)
|
|
29
|
+
- `max-iterations` — set by the orchestrate skill (default 3, could be overridden in foundry cycle definition)
|
|
30
30
|
|
|
31
31
|
## Sections
|
|
32
32
|
|
|
@@ -96,9 +96,9 @@ Grouped by artefact file path. Each item is a checklist entry with a tag indicat
|
|
|
96
96
|
| Section | Written by | Updated by |
|
|
97
97
|
|---------|-----------|------------|
|
|
98
98
|
| Frontmatter (`flow`) | `foundry_workfile_create` (flow skill) | nobody |
|
|
99
|
-
| Frontmatter (`cycle`, `stages`, `max-iterations`) | `foundry_workfile_set` (
|
|
99
|
+
| Frontmatter (`cycle`, `stages`, `max-iterations`) | `foundry_workfile_set` (orchestrate skill) | `foundry_workfile_set` (reset on each new cycle) |
|
|
100
100
|
| Goal | `foundry_workfile_create` (flow skill) | nobody |
|
|
101
|
-
| Artefacts | `foundry_artefacts_add` (forge skill) | `foundry_artefacts_set_status` (
|
|
101
|
+
| Artefacts | `foundry_artefacts_add` (forge skill) | `foundry_artefacts_set_status` (orchestrate skill) |
|
|
102
102
|
| Feedback | `foundry_feedback_add` (quench/appraise/hitl) | `foundry_feedback_action`/`foundry_feedback_wontfix` (forge), `foundry_feedback_resolve` (quench/appraise/hitl) |
|
|
103
103
|
|
|
104
104
|
## WORK.history.yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@really-knows-ai/foundry",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "A structured framework for AI-driven artefact creation with deterministic routing, quality gates, and iterative refinement cycles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": ".opencode/plugins/foundry.js",
|