@really-knows-ai/foundry 2.0.1 → 2.1.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 +15 -6
- package/package.json +2 -2
- package/scripts/lib/slug.js +33 -0
- package/scripts/sort.js +11 -2
- package/skills/flow/SKILL.md +1 -1
- package/skills/refresh-agents/SKILL.md +6 -3
- package/skills/sort/SKILL.md +47 -9
- package/skills/upgrade-foundry/SKILL.md +19 -5
|
@@ -17,8 +17,9 @@ import { parseFrontmatter, createWorkfile, setFrontmatterField, getFrontmatterFi
|
|
|
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
|
+
import { slugify } from '../../scripts/lib/slug.js';
|
|
20
21
|
import { runSort } from '../../scripts/sort.js';
|
|
21
|
-
import { execSync } from 'child_process';
|
|
22
|
+
import { execSync, execFileSync } from 'child_process';
|
|
22
23
|
|
|
23
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
const packageRoot = path.resolve(__dirname, '../..');
|
|
@@ -179,8 +180,8 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
179
180
|
args: {
|
|
180
181
|
flow: tool.schema.string().describe('Flow name'),
|
|
181
182
|
cycle: tool.schema.string().describe('Cycle name'),
|
|
182
|
-
stages: tool.schema.array(tool.schema.string()).describe('Ordered stage names'),
|
|
183
|
-
maxIterations: tool.schema.number().describe('Maximum iterations'),
|
|
183
|
+
stages: tool.schema.array(tool.schema.string()).optional().describe('Ordered stage names'),
|
|
184
|
+
maxIterations: tool.schema.number().optional().describe('Maximum iterations'),
|
|
184
185
|
goal: tool.schema.string().describe('Goal text'),
|
|
185
186
|
models: tool.schema.string().optional().describe('Per-stage model overrides as JSON object, e.g. \'{"forge":"openai/gpt-4o"}\''),
|
|
186
187
|
},
|
|
@@ -189,7 +190,13 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
189
190
|
if (existsSync(workPath)) {
|
|
190
191
|
return JSON.stringify({ error: 'WORK.md already exists' });
|
|
191
192
|
}
|
|
192
|
-
const fm = { flow: args.flow, cycle: args.cycle
|
|
193
|
+
const fm = { flow: args.flow, cycle: args.cycle };
|
|
194
|
+
if (args.stages) {
|
|
195
|
+
fm.stages = enrichStages(args.stages, args.cycle);
|
|
196
|
+
}
|
|
197
|
+
if (args.maxIterations !== undefined) {
|
|
198
|
+
fm.maxIterations = args.maxIterations;
|
|
199
|
+
}
|
|
193
200
|
if (args.models) {
|
|
194
201
|
fm.models = parseModelsValue(args.models);
|
|
195
202
|
}
|
|
@@ -424,8 +431,10 @@ export const FoundryPlugin = async ({ directory }) => {
|
|
|
424
431
|
description: tool.schema.string().describe('Branch description suffix'),
|
|
425
432
|
},
|
|
426
433
|
async execute(args, context) {
|
|
427
|
-
const
|
|
428
|
-
|
|
434
|
+
const flowSlug = slugify(args.flowId);
|
|
435
|
+
const descSlug = slugify(args.description);
|
|
436
|
+
const branch = `work/${flowSlug}-${descSlug}`;
|
|
437
|
+
execFileSync('git', ['checkout', '-b', branch], { cwd: context.worktree, encoding: 'utf8', stdio: 'pipe' });
|
|
429
438
|
return JSON.stringify({ ok: true, branch });
|
|
430
439
|
},
|
|
431
440
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@really-knows-ai/foundry",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"node": ">=18.3.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"test": "node --test
|
|
28
|
+
"test": "node --test"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@opencode-ai/plugin": "^1.4.0",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug utilities for generating shell-safe, git-ref-safe identifiers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert an arbitrary string into a URL/git-branch-friendly slug.
|
|
7
|
+
*
|
|
8
|
+
* Rules:
|
|
9
|
+
* - Strips diacritics (e.g. "café" → "cafe")
|
|
10
|
+
* - Lowercases
|
|
11
|
+
* - Replaces any run of non-[a-z0-9] characters with a single dash
|
|
12
|
+
* - Trims leading/trailing dashes
|
|
13
|
+
*
|
|
14
|
+
* Throws if the input is not a string or if the resulting slug is empty.
|
|
15
|
+
*/
|
|
16
|
+
export function slugify(input) {
|
|
17
|
+
if (typeof input !== 'string') {
|
|
18
|
+
throw new TypeError(`slugify: expected string, got ${typeof input}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const slug = input
|
|
22
|
+
.normalize('NFD')
|
|
23
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '');
|
|
27
|
+
|
|
28
|
+
if (slug.length === 0) {
|
|
29
|
+
throw new Error(`slugify: input produced empty slug (input: ${JSON.stringify(input)})`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return slug;
|
|
33
|
+
}
|
package/scripts/sort.js
CHANGED
|
@@ -209,7 +209,7 @@ function checkModifiedFiles(lastBase, foundryDir, cycleDef, cycle, io = defaultI
|
|
|
209
209
|
// Exported runSort — structured result for programmatic use
|
|
210
210
|
// ---------------------------------------------------------------------------
|
|
211
211
|
|
|
212
|
-
export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml', foundryDir = 'foundry', cycleDef } = {}, io = defaultIO) {
|
|
212
|
+
export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml', foundryDir = 'foundry', cycleDef, agentsDir = '.opencode/agents' } = {}, io = defaultIO) {
|
|
213
213
|
if (!io.exists(workPath)) {
|
|
214
214
|
return { route: 'blocked', details: 'WORK.md not found' };
|
|
215
215
|
}
|
|
@@ -255,7 +255,16 @@ export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml
|
|
|
255
255
|
const routeBase = baseStage(route);
|
|
256
256
|
if (frontmatter.models && frontmatter.models[routeBase]) {
|
|
257
257
|
const modelId = frontmatter.models[routeBase];
|
|
258
|
-
model = `foundry-${modelId.replace(
|
|
258
|
+
model = `foundry-${modelId.replace(/[/.]/g, '-')}`;
|
|
259
|
+
|
|
260
|
+
// Fail-fast: required subagent file must exist
|
|
261
|
+
const agentPath = `${agentsDir}/${model}.md`;
|
|
262
|
+
if (!io.exists(agentPath)) {
|
|
263
|
+
return {
|
|
264
|
+
route: 'violation',
|
|
265
|
+
details: `Missing required subagent: ${model}.md is not present in ${agentsDir}/. Run the refresh-agents skill to regenerate agent files, then restart.`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
return { route, ...(model ? { model } : {}) };
|
package/skills/flow/SKILL.md
CHANGED
|
@@ -23,7 +23,7 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
|
|
|
23
23
|
- If only one starting cycle, use it
|
|
24
24
|
- If multiple starting cycles, check whether the user's request makes the choice obvious (e.g., "write a haiku" clearly maps to `create-haiku`)
|
|
25
25
|
- If ambiguous, prompt the user to choose
|
|
26
|
-
4. Call `foundry_workfile_create` with the flow ID, chosen cycle ID, and goal
|
|
26
|
+
4. Call `foundry_workfile_create` with **only** the flow ID, chosen cycle ID, and goal — do **not** pass `stages` or `maxIterations`. The `cycle` skill will read the cycle definition and populate those via `foundry_workfile_set` in the next step.
|
|
27
27
|
5. Execute the cycle by invoking the cycle skill
|
|
28
28
|
|
|
29
29
|
## Between cycles
|
|
@@ -16,11 +16,14 @@ Regenerate `.opencode/agents/foundry-*.md` files from the currently available mo
|
|
|
16
16
|
|
|
17
17
|
### Agent file format
|
|
18
18
|
|
|
19
|
-
Filename: `.opencode/agents/foundry-<
|
|
19
|
+
Filename: `.opencode/agents/foundry-<slug>.md`
|
|
20
20
|
|
|
21
|
-
Where `<
|
|
21
|
+
Where `<slug>` is the model ID with **both** `/` and `.` replaced by `-`. This keeps filenames shell-safe and unambiguous.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Examples:
|
|
24
|
+
- `opencode/claude-sonnet-4` → `.opencode/agents/foundry-opencode-claude-sonnet-4.md`
|
|
25
|
+
- `github-copilot/claude-sonnet-4.6` → `.opencode/agents/foundry-github-copilot-claude-sonnet-4-6.md`
|
|
26
|
+
- `github-copilot/gpt-5.4` → `.opencode/agents/foundry-github-copilot-gpt-5-4.md`
|
|
24
27
|
|
|
25
28
|
Content:
|
|
26
29
|
|
package/skills/sort/SKILL.md
CHANGED
|
@@ -6,7 +6,7 @@ description: Deterministic routing for a foundry cycle. Runs the foundry_sort to
|
|
|
6
6
|
|
|
7
7
|
# Sort
|
|
8
8
|
|
|
9
|
-
You are the central dispatcher for a foundry cycle. You call the `foundry_sort` tool to determine what stage to execute next, then
|
|
9
|
+
You are the central dispatcher for a foundry cycle. You call the `foundry_sort` tool to determine what stage to execute next, then dispatch that stage to a fresh subagent.
|
|
10
10
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
@@ -21,21 +21,59 @@ Before running this skill, verify that the `foundry/` directory exists in the pr
|
|
|
21
21
|
2. Call `foundry_history_append` with the current cycle, stage `"sort"`, and a comment explaining the routing decision in natural language. This is your audit trail — if something goes wrong, this comment is what someone will read to understand what happened.
|
|
22
22
|
|
|
23
23
|
3. Act on the route:
|
|
24
|
-
- `forge:*` — dispatch
|
|
25
|
-
- `quench:*` — dispatch
|
|
26
|
-
- `appraise:*` — dispatch
|
|
27
|
-
- `human-appraise:*` — invoke the human-appraise skill (
|
|
24
|
+
- `forge:*` — **dispatch** (see §Dispatch below)
|
|
25
|
+
- `quench:*` — **dispatch**
|
|
26
|
+
- `appraise:*` — **dispatch**. Note: the appraise skill handles its own per-appraiser model resolution internally.
|
|
27
|
+
- `human-appraise:*` — invoke the human-appraise skill inline (human stage, no subagent)
|
|
28
28
|
- `done` — foundry cycle is complete, return to the cycle skill
|
|
29
29
|
- `blocked` — foundry cycle is blocked (iteration limit hit with unresolved feedback), return to the cycle skill
|
|
30
|
-
- `violation` — file
|
|
30
|
+
- `violation` — a validation, file-modification, or missing-subagent violation was detected (see `details`). The cycle halts — call `foundry_artefacts_set_status` with status `"blocked"` for each affected artefact, and return to the cycle skill. If `details` mentions a missing subagent, tell the user to run the `refresh-agents` skill and restart.
|
|
31
31
|
|
|
32
32
|
4. After the subagent completes, call `foundry_history_append` with the current cycle, the **dispatched stage alias** (e.g., `forge:write-haiku`), and a comment summarizing what the subagent reported doing. This is critical — sort is the only reliable writer of stage history. Subagents must NOT write their own history entries.
|
|
33
33
|
|
|
34
34
|
5. After logging the stage history, call `foundry_sort` again. Repeat from step 1 until it returns `done`, `blocked`, or `violation`.
|
|
35
35
|
|
|
36
|
+
## Dispatch
|
|
37
|
+
|
|
38
|
+
Every forge, quench, and appraise stage runs in a **fresh subagent**. Never inline the stage work in the orchestrator conversation — even if the chosen model happens to match the orchestrator's model. The orchestrator's job is to route and log, nothing else.
|
|
39
|
+
|
|
40
|
+
### Choosing the subagent
|
|
41
|
+
|
|
42
|
+
- If `foundry_sort` returned a `model` field in its response, use that value verbatim as `subagent_type`. It is already in `foundry-<slug>` form (the tool does the slug computation by replacing both `/` and `.` with `-` in the model ID).
|
|
43
|
+
- If `foundry_sort` returned **no** `model` field (the cycle has no `models:` map, or no entry for this stage base), dispatch to the default general-purpose subagent: `general`.
|
|
44
|
+
|
|
45
|
+
### Dispatch call shape
|
|
46
|
+
|
|
47
|
+
Use the `task` tool:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
task tool:
|
|
51
|
+
subagent_type: <model-slug-from-foundry_sort-response, or "general">
|
|
52
|
+
description: "Run <stage-alias> for <cycle-id>"
|
|
53
|
+
prompt: |
|
|
54
|
+
You are a Foundry stage agent. Invoke the <stage-base> skill and follow its instructions exactly.
|
|
55
|
+
|
|
56
|
+
Current cycle: <cycle-id>
|
|
57
|
+
Current stage: <stage-alias>
|
|
58
|
+
Working directory: <worktree>
|
|
59
|
+
|
|
60
|
+
When done, report back a brief summary of what you did. Do NOT call foundry_history_append — the orchestrator handles history.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Substitute:
|
|
64
|
+
- `<stage-alias>` — the full route string from `foundry_sort` (e.g., `forge:write-haiku`)
|
|
65
|
+
- `<stage-base>` — the base of the alias (e.g., `forge`, `quench`, `appraise`)
|
|
66
|
+
- `<cycle-id>` — the current cycle ID from WORK.md frontmatter
|
|
67
|
+
- `<worktree>` — the current working directory
|
|
68
|
+
|
|
69
|
+
### Missing subagent (fail-fast)
|
|
70
|
+
|
|
71
|
+
The `foundry_sort` tool verifies that the required `.opencode/agents/foundry-<slug>.md` file exists before returning a `model`. If it doesn't, sort returns `{route: 'violation', details: 'Missing required subagent: ...'}`. Handle this as described in step 3 above — halt the cycle, mark artefacts blocked, and instruct the user to run the `refresh-agents` skill.
|
|
72
|
+
|
|
36
73
|
## What you do NOT do
|
|
37
74
|
|
|
38
|
-
- You do not make routing decisions yourself — the tool decides
|
|
39
|
-
- You do not skip calling `foundry_sort
|
|
40
|
-
- You do not override the tool's output
|
|
75
|
+
- You do not make routing decisions yourself — the tool decides.
|
|
76
|
+
- You do not skip calling `foundry_sort`.
|
|
77
|
+
- You do not override the tool's output.
|
|
41
78
|
- You do not skip the history entry — every sort invocation gets a `sort` entry, and every completed stage gets a stage entry (e.g., `forge:write-haiku`). You are the sole writer of history.
|
|
79
|
+
- You do **not** inline forge/quench/appraise work — always dispatch to a subagent via the `task` tool, even when the resolved model matches the orchestrator's own model.
|
|
@@ -27,12 +27,17 @@ Read all configuration files:
|
|
|
27
27
|
- `foundry/laws/*.md` — global laws
|
|
28
28
|
- `foundry/appraisers/*.md` — appraiser definitions
|
|
29
29
|
|
|
30
|
+
Also scan `.opencode/agents/foundry-*.md` for agent-filename migration (see §2).
|
|
31
|
+
|
|
30
32
|
For each file, parse the frontmatter and body content.
|
|
31
33
|
|
|
32
34
|
### 2. Detect what needs migration
|
|
33
35
|
|
|
34
36
|
Check each file against the current expected format:
|
|
35
37
|
|
|
38
|
+
**Agent files (v2.1 migration):**
|
|
39
|
+
- Any `.opencode/agents/foundry-*.md` filename containing a `.` character? → needs renaming to all-dashes format. The v2.1 naming convention replaces both `/` and `.` in the model ID with `-`. For example, `foundry-github-copilot-claude-sonnet-4.6.md` must become `foundry-github-copilot-claude-sonnet-4-6.md`. The inner `model:` frontmatter field is **not** changed — only the filename.
|
|
40
|
+
|
|
36
41
|
**Flows:**
|
|
37
42
|
- Has `starting-cycles` field? If not → needs DAG migration
|
|
38
43
|
- Has ordered numbered list under `## Cycles`? → needs conversion to unordered list
|
|
@@ -84,7 +89,16 @@ Present a grouped summary of all issues found:
|
|
|
84
89
|
|
|
85
90
|
If nothing needs migration, say so and stop.
|
|
86
91
|
|
|
87
|
-
### 4. Migrate
|
|
92
|
+
### 4. Migrate agent files (v2.1)
|
|
93
|
+
|
|
94
|
+
For each `.opencode/agents/foundry-*.md` file with a `.` in its filename:
|
|
95
|
+
- Compute the new filename by replacing all `.` with `-` (keep the `.md` extension)
|
|
96
|
+
- `git mv <old> <new>` to preserve history
|
|
97
|
+
- Do **not** modify the file contents — the `model:` field inside retains its original dots
|
|
98
|
+
|
|
99
|
+
After renaming, remind the user: **Restart OpenCode** for the new agent filenames to register.
|
|
100
|
+
|
|
101
|
+
### 5. Migrate flows
|
|
88
102
|
|
|
89
103
|
For each flow needing migration:
|
|
90
104
|
- Show the current ordered cycle list
|
|
@@ -93,7 +107,7 @@ For each flow needing migration:
|
|
|
93
107
|
- Present the proposed `starting-cycles` and confirm
|
|
94
108
|
- Convert numbered `## Cycles` list to unordered
|
|
95
109
|
|
|
96
|
-
###
|
|
110
|
+
### 6. Migrate cycles
|
|
97
111
|
|
|
98
112
|
For each cycle needing migration:
|
|
99
113
|
|
|
@@ -112,20 +126,20 @@ For each cycle needing migration:
|
|
|
112
126
|
|
|
113
127
|
Remove `hitl` from stages and add `human-appraise` config if enabled.
|
|
114
128
|
|
|
115
|
-
###
|
|
129
|
+
### 7. Migrate other config
|
|
116
130
|
|
|
117
131
|
For artefact types, appraisers, laws, and validation with issues:
|
|
118
132
|
- Present each issue with a suggested fix
|
|
119
133
|
- Ask the user to confirm or adjust
|
|
120
134
|
|
|
121
|
-
###
|
|
135
|
+
### 8. Present migration plan
|
|
122
136
|
|
|
123
137
|
Before writing anything, show the complete list of changes:
|
|
124
138
|
- Group by category
|
|
125
139
|
- Show each file and the specific changes
|
|
126
140
|
- Ask for confirmation
|
|
127
141
|
|
|
128
|
-
###
|
|
142
|
+
### 9. Apply changes
|
|
129
143
|
|
|
130
144
|
- Update all affected files
|
|
131
145
|
- Commit with message: `[foundry] upgrade: migrate to current format`
|