@really-knows-ai/foundry 2.0.0 → 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,13 +17,39 @@ 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, '../..');
25
26
  const allSkillsDir = path.join(packageRoot, 'skills');
26
27
 
28
+ function listFlows(foundryDir) {
29
+ const flowsDir = path.join(foundryDir, 'flows');
30
+ if (!fs.existsSync(flowsDir)) return [];
31
+ const flows = [];
32
+ for (const entry of readdirSync(flowsDir)) {
33
+ if (!entry.endsWith('.md') || entry === '.gitkeep') continue;
34
+ try {
35
+ const text = readFileSync(path.join(flowsDir, entry), 'utf-8');
36
+ const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
37
+ if (!fmMatch) continue;
38
+ const fm = fmMatch[1];
39
+ const idMatch = fm.match(/^id:\s*(.+)$/m);
40
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
41
+ const startingMatch = fm.match(/^starting-cycles:\s*\n((?:\s*-\s*.+\n?)+)/m);
42
+ const id = idMatch ? idMatch[1].trim() : entry.replace(/\.md$/, '');
43
+ const name = nameMatch ? nameMatch[1].trim() : id;
44
+ const startingCycles = startingMatch
45
+ ? startingMatch[1].split('\n').map(l => l.replace(/^\s*-\s*/, '').trim()).filter(Boolean)
46
+ : [];
47
+ flows.push({ id, name, startingCycles });
48
+ } catch { /* skip bad files */ }
49
+ }
50
+ return flows;
51
+ }
52
+
27
53
  function getBootstrapContent(directory) {
28
54
  const foundryDir = path.join(directory, 'foundry');
29
55
  const foundryExists = fs.existsSync(foundryDir) && fs.statSync(foundryDir).isDirectory();
@@ -37,6 +63,14 @@ and guide you through defining artefact types, laws, appraisers, cycles, and flo
37
63
  </FOUNDRY_CONTEXT>`;
38
64
  }
39
65
 
66
+ const flows = listFlows(foundryDir);
67
+ const flowList = flows.length > 0
68
+ ? flows.map(f => {
69
+ const sc = f.startingCycles.length > 0 ? ` — starting cycles: ${f.startingCycles.join(', ')}` : '';
70
+ return `- \`${f.id}\` — ${f.name}${sc}`;
71
+ }).join('\n')
72
+ : '- (no flows defined yet — use the `add-flow` skill to create one)';
73
+
40
74
  return `<FOUNDRY_CONTEXT>
41
75
  Foundry is active in this project. The foundry/ directory contains the project's artefact definitions,
42
76
  laws, appraisers, cycles, and flows.
@@ -44,15 +78,32 @@ laws, appraisers, cycles, and flows.
44
78
  Foundry is a skill-driven framework for governed artefact generation and evaluation.
45
79
  The pipeline: forge (produce) → quench (deterministic checks) → appraise (subjective evaluation) → iterate.
46
80
 
47
- Available skills:
48
- - **Pipeline:** forge, quench, appraise, cycle, flow, sort, hitl
49
- - **Helpers:** add-artefact-type, add-law, add-appraiser, add-cycle, add-flow, init-foundry
81
+ ## Defined flows
82
+
83
+ ${flowList}
84
+
85
+ **CRITICAL ROUTING RULE:** When the user references any flow above — by id (e.g. "creative-flow"),
86
+ by name (e.g. "Creative Flow"), or by clear paraphrase (e.g. "the creative flow", "use the creative pipeline") —
87
+ invoke the \`flow\` skill DIRECTLY with that flow's id. Do NOT invoke brainstorming, do NOT explore the
88
+ codebase, do NOT ask clarifying questions about what to build. The flow's cycles already define the
89
+ work. The user's request text (e.g. "make a haiku about X") is the goal to pass to the flow.
50
90
 
51
- Multi-model routing: Foundry uses \`foundry-*\` sub-agents defined as markdown files in \`.opencode/agents/\`.
91
+ Brainstorming applies to NEW features being added to foundry itself (new cycles, new artefact types,
92
+ new skills). It does NOT apply to running an existing, defined flow.
93
+
94
+ ## Available skills
95
+
96
+ - **Pipeline:** forge, quench, appraise, cycle, flow, sort, human-appraise
97
+ - **Authoring:** add-artefact-type, add-law, add-appraiser, add-cycle, add-flow, init-foundry
98
+ - **Maintenance:** upgrade-foundry, refresh-agents, list-agents
99
+
100
+ ## Multi-model routing
101
+
102
+ Foundry uses \`foundry-*\` sub-agents defined as markdown files in \`.opencode/agents/\`.
52
103
  Run the \`refresh-agents\` skill to regenerate them after adding or removing providers.
53
104
  Cycle definitions can specify per-stage models via the \`models\` frontmatter map. Appraisers can override with their own \`model\` field.
54
105
 
55
- To start a flow, use the \`flow\` skill. All user content lives under foundry/.
106
+ All user content lives under foundry/.
56
107
  Scripts are located at: ${path.join(packageRoot, 'scripts')}
57
108
  </FOUNDRY_CONTEXT>`;
58
109
  }
@@ -129,8 +180,8 @@ export const FoundryPlugin = async ({ directory }) => {
129
180
  args: {
130
181
  flow: tool.schema.string().describe('Flow name'),
131
182
  cycle: tool.schema.string().describe('Cycle name'),
132
- stages: tool.schema.array(tool.schema.string()).describe('Ordered stage names'),
133
- 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'),
134
185
  goal: tool.schema.string().describe('Goal text'),
135
186
  models: tool.schema.string().optional().describe('Per-stage model overrides as JSON object, e.g. \'{"forge":"openai/gpt-4o"}\''),
136
187
  },
@@ -139,7 +190,13 @@ export const FoundryPlugin = async ({ directory }) => {
139
190
  if (existsSync(workPath)) {
140
191
  return JSON.stringify({ error: 'WORK.md already exists' });
141
192
  }
142
- 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
+ }
143
200
  if (args.models) {
144
201
  fm.models = parseModelsValue(args.models);
145
202
  }
@@ -374,8 +431,10 @@ export const FoundryPlugin = async ({ directory }) => {
374
431
  description: tool.schema.string().describe('Branch description suffix'),
375
432
  },
376
433
  async execute(args, context) {
377
- const branch = `work/${args.flowId}-${args.description}`;
378
- 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' });
379
438
  return JSON.stringify({ ok: true, branch });
380
439
  },
381
440
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@really-knows-ai/foundry",
3
- "version": "2.0.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 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 } : {}) };
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: flow
3
3
  type: composite
4
- description: Orchestrates foundry cycles as a dependency graph, driven by a flow definition.
4
+ description: Runs a defined foundry flow to produce artefacts. Use this whenever the user references a flow by id, name, or paraphrase (e.g. "use the creative flow", "run creative-flow"). Do not brainstorm — the flow's cycles already define the work. The user's request is the goal to pass in.
5
5
  composes: [cycle]
6
6
  ---
7
7
 
@@ -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`