@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.
@@ -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, stages: enrichStages(args.stages, args.cycle), maxIterations: args.maxIterations };
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 branch = `work/${args.flowId}-${args.description}`;
428
- execSync(`git checkout -b ${branch}`, { cwd: context.worktree, encoding: 'utf8' });
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.1",
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 tests/**/*.test.js"
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(/\//g, '-')}`;
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 } : {}) };
@@ -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-<provider>-<model-key>.md`
19
+ Filename: `.opencode/agents/foundry-<slug>.md`
20
20
 
21
- Where `<provider>-<model-key>` is the model ID with `/` replaced by `-`.
21
+ Where `<slug>` is the model ID with **both** `/` and `.` replaced by `-`. This keeps filenames shell-safe and unambiguous.
22
22
 
23
- Example: model `opencode/claude-sonnet-4` produces `.opencode/agents/foundry-opencode-claude-sonnet-4.md`
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
 
@@ -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 invoke that stage's skill.
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 the forge skill as a sub-agent. Use model dispatch (see below).
25
- - `quench:*` — dispatch the quench skill as a sub-agent. Use model dispatch.
26
- - `appraise:*` — dispatch the appraise skill as a sub-agent. Use model dispatch. Note: the appraise skill handles its own per-appraiser model resolution internally.
27
- - `human-appraise:*` — invoke the human-appraise skill (no model dispatch — human stage)
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 modification or tag validation violation detected (see `details`). The cycle halts — call `foundry_artefacts_set_status` with status `"blocked"`, and return to the cycle skill
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 flows
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
- ### 5. Migrate cycles
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
- ### 6. Migrate other config
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
- ### 7. Present migration plan
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
- ### 8. Apply changes
142
+ ### 9. Apply changes
129
143
 
130
144
  - Update all affected files
131
145
  - Commit with message: `[foundry] upgrade: migrate to current format`