@really-knows-ai/foundry 2.3.2 → 3.0.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.
Files changed (170) hide show
  1. package/README.md +180 -369
  2. package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
  3. package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
  4. package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
  5. package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
  6. package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
  7. package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
  8. package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
  9. package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
  10. package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
  11. package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
  12. package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
  13. package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
  14. package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
  15. package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
  16. package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
  17. package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
  18. package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
  19. package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
  20. package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
  21. package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
  22. package/dist/.opencode/plugins/foundry.js +105 -0
  23. package/dist/CHANGELOG.md +533 -0
  24. package/dist/LICENSE +21 -0
  25. package/dist/README.md +278 -0
  26. package/dist/docs/README.md +59 -0
  27. package/dist/docs/architecture.md +433 -0
  28. package/dist/docs/concepts.md +395 -0
  29. package/dist/docs/getting-started.md +344 -0
  30. package/dist/docs/memory-maintenance.md +176 -0
  31. package/dist/docs/tools.md +1411 -0
  32. package/dist/docs/work-spec.md +283 -0
  33. package/dist/scripts/lib/artefacts.js +151 -0
  34. package/dist/scripts/lib/assay/loader.js +151 -0
  35. package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
  36. package/dist/scripts/lib/assay/permissions.js +52 -0
  37. package/dist/scripts/lib/assay/run.js +219 -0
  38. package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
  39. package/dist/scripts/lib/attestation/attest.js +111 -0
  40. package/dist/scripts/lib/attestation/canonical-json.js +109 -0
  41. package/dist/scripts/lib/attestation/hash.js +17 -0
  42. package/dist/scripts/lib/attestation/parse.js +14 -0
  43. package/dist/scripts/lib/attestation/payload.js +106 -0
  44. package/dist/scripts/lib/attestation/render.js +16 -0
  45. package/dist/scripts/lib/attestation/verify.js +15 -0
  46. package/dist/scripts/lib/branch-guard.js +72 -0
  47. package/dist/scripts/lib/config-creators/appraiser.js +9 -0
  48. package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
  49. package/dist/scripts/lib/config-creators/cycle.js +11 -0
  50. package/dist/scripts/lib/config-creators/factory.js +49 -0
  51. package/dist/scripts/lib/config-creators/flow.js +11 -0
  52. package/dist/scripts/lib/config-validators/appraiser.js +49 -0
  53. package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
  54. package/dist/scripts/lib/config-validators/cycle.js +131 -0
  55. package/dist/scripts/lib/config-validators/flow.js +57 -0
  56. package/dist/scripts/lib/config-validators/helpers.js +96 -0
  57. package/dist/scripts/lib/config-validators/law.js +96 -0
  58. package/dist/scripts/lib/config.js +328 -0
  59. package/dist/scripts/lib/failed-flow.js +131 -0
  60. package/dist/scripts/lib/feedback-store.js +249 -0
  61. package/dist/scripts/lib/feedback-transitions.js +105 -0
  62. package/dist/scripts/lib/finalize.js +70 -0
  63. package/dist/scripts/lib/foundational-guards.js +13 -0
  64. package/dist/scripts/lib/git-bridge.js +77 -0
  65. package/dist/scripts/lib/git-finish/work-finish.js +233 -0
  66. package/dist/scripts/lib/git-policy.js +101 -0
  67. package/dist/scripts/lib/guards.js +125 -0
  68. package/dist/scripts/lib/history.js +132 -0
  69. package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
  70. package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
  71. package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
  72. package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
  73. package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
  74. package/dist/scripts/lib/memory/admin/dump.js +47 -0
  75. package/dist/scripts/lib/memory/admin/helpers.js +31 -0
  76. package/dist/scripts/lib/memory/admin/init.js +170 -0
  77. package/dist/scripts/lib/memory/admin/live-store.js +76 -0
  78. package/dist/scripts/lib/memory/admin/reembed.js +285 -0
  79. package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
  80. package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
  81. package/dist/scripts/lib/memory/admin/reset.js +24 -0
  82. package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
  83. package/dist/scripts/lib/memory/admin/validate.js +19 -0
  84. package/dist/scripts/lib/memory/config.js +149 -0
  85. package/dist/scripts/lib/memory/cozo.js +136 -0
  86. package/dist/scripts/lib/memory/drift.js +71 -0
  87. package/dist/scripts/lib/memory/embeddings.js +128 -0
  88. package/dist/scripts/lib/memory/frontmatter.js +75 -0
  89. package/dist/scripts/lib/memory/ndjson.js +84 -0
  90. package/dist/scripts/lib/memory/paths.js +25 -0
  91. package/dist/scripts/lib/memory/permissions.js +41 -0
  92. package/dist/scripts/lib/memory/prompt.js +109 -0
  93. package/dist/scripts/lib/memory/query.js +56 -0
  94. package/dist/scripts/lib/memory/reads.js +109 -0
  95. package/dist/scripts/lib/memory/schema.js +64 -0
  96. package/dist/scripts/lib/memory/search.js +73 -0
  97. package/dist/scripts/lib/memory/singleton.js +49 -0
  98. package/dist/scripts/lib/memory/store.js +162 -0
  99. package/dist/scripts/lib/memory/types.js +93 -0
  100. package/dist/scripts/lib/memory/validate.js +58 -0
  101. package/dist/scripts/lib/memory/writes.js +40 -0
  102. package/{scripts → dist/scripts}/lib/pending.js +7 -2
  103. package/dist/scripts/lib/secret.js +59 -0
  104. package/{scripts → dist/scripts}/lib/slug.js +3 -2
  105. package/dist/scripts/lib/snapshot/finish.js +103 -0
  106. package/dist/scripts/lib/snapshot/inspect.js +253 -0
  107. package/dist/scripts/lib/snapshot/render.js +55 -0
  108. package/dist/scripts/lib/sort-fs-check.js +121 -0
  109. package/dist/scripts/lib/sort-routing.js +101 -0
  110. package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
  111. package/{scripts → dist/scripts}/lib/state.js +4 -0
  112. package/dist/scripts/lib/token.js +57 -0
  113. package/dist/scripts/lib/tracing.js +59 -0
  114. package/dist/scripts/lib/ulid.js +100 -0
  115. package/dist/scripts/lib/validator-jsonl.js +162 -0
  116. package/{scripts → dist/scripts}/lib/workfile.js +38 -20
  117. package/dist/scripts/orchestrate-cycle.js +215 -0
  118. package/dist/scripts/orchestrate-phases.js +314 -0
  119. package/dist/scripts/orchestrate.js +163 -0
  120. package/dist/scripts/sort.js +278 -0
  121. package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
  122. package/{skills → dist/skills}/add-artefact-type/SKILL.md +62 -40
  123. package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
  124. package/dist/skills/add-extractor/SKILL.md +133 -0
  125. package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
  126. package/dist/skills/add-law/SKILL.md +191 -0
  127. package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
  128. package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
  129. package/{skills → dist/skills}/appraise/SKILL.md +62 -13
  130. package/dist/skills/assay/SKILL.md +72 -0
  131. package/dist/skills/change-embedding-model/SKILL.md +58 -0
  132. package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
  133. package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
  134. package/dist/skills/dry-run/SKILL.md +116 -0
  135. package/{skills → dist/skills}/flow/SKILL.md +15 -2
  136. package/dist/skills/forge/SKILL.md +121 -0
  137. package/dist/skills/human-appraise/SKILL.md +153 -0
  138. package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
  139. package/dist/skills/init-memory/SKILL.md +92 -0
  140. package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
  141. package/dist/skills/quench/SKILL.md +99 -0
  142. package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
  143. package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
  144. package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
  145. package/dist/skills/reset-memory/SKILL.md +54 -0
  146. package/dist/skills/upgrade-foundry/SKILL.md +191 -0
  147. package/package.json +34 -17
  148. package/.opencode/plugins/foundry.js +0 -761
  149. package/CHANGELOG.md +0 -100
  150. package/docs/concepts.md +0 -122
  151. package/docs/getting-started.md +0 -187
  152. package/docs/work-spec.md +0 -207
  153. package/scripts/lib/artefacts.js +0 -124
  154. package/scripts/lib/config.js +0 -175
  155. package/scripts/lib/feedback-transitions.js +0 -25
  156. package/scripts/lib/feedback.js +0 -440
  157. package/scripts/lib/finalize.js +0 -41
  158. package/scripts/lib/history.js +0 -59
  159. package/scripts/lib/secret.js +0 -23
  160. package/scripts/lib/tags.js +0 -108
  161. package/scripts/lib/token.js +0 -26
  162. package/scripts/orchestrate.js +0 -418
  163. package/scripts/sort.js +0 -370
  164. package/scripts/validate-tags.js +0 -54
  165. package/skills/add-law/SKILL.md +0 -111
  166. package/skills/forge/SKILL.md +0 -88
  167. package/skills/human-appraise/SKILL.md +0 -82
  168. package/skills/quench/SKILL.md +0 -62
  169. package/skills/upgrade-foundry/SKILL.md +0 -216
  170. /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
@@ -1,761 +0,0 @@
1
- /**
2
- * Foundry plugin for OpenCode.ai
3
- *
4
- * All skills are always registered. Individual skills check for foundry/ dir.
5
- * - If foundry/ exists: pipeline context injected into first message
6
- * - If foundry/ does not exist: minimal prompt guiding user to init-foundry
7
- * Multi-model agents are managed as .opencode/agents/foundry-*.md files via the refresh-agents skill.
8
- */
9
-
10
- import path from 'path';
11
- import fs from 'fs';
12
- import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync, mkdirSync } from 'fs';
13
- import { fileURLToPath } from 'url';
14
- import { tool } from '@opencode-ai/plugin';
15
- import { loadHistory } from '../../scripts/lib/history.js';
16
- import { parseFrontmatter, createWorkfile, enrichStages, parseModelsValue } from '../../scripts/lib/workfile.js';
17
- import { parseArtefactsTable, addArtefactRow, setArtefactStatus } from '../../scripts/lib/artefacts.js';
18
- import { addFeedbackItem, actionFeedbackItem, wontfixFeedbackItem, resolveFeedbackItem, listFeedback } from '../../scripts/lib/feedback.js';
19
- import { getCycleDefinition, getArtefactType, getLaws, getValidation, getAppraisers, getFlow, selectAppraisers } from '../../scripts/lib/config.js';
20
- import { slugify } from '../../scripts/lib/slug.js';
21
- import { execSync, execFileSync } from 'child_process';
22
- import { createHash, randomUUID } from 'node:crypto';
23
- import { readOrCreateSecret } from '../../scripts/lib/secret.js';
24
- import { createPendingStore } from '../../scripts/lib/pending.js';
25
- import { signToken, verifyToken } from '../../scripts/lib/token.js';
26
- import {
27
- ensureFoundryDir, readActiveStage, writeActiveStage, clearActiveStage,
28
- readLastStage, writeLastStage,
29
- } from '../../scripts/lib/state.js';
30
- import { requireNoActiveStage, requireActiveStage, stageBaseOf } from '../../scripts/lib/stage-guard.js';
31
- import { finalizeStage } from '../../scripts/lib/finalize.js';
32
-
33
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
- const packageRoot = path.resolve(__dirname, '../..');
35
- const allSkillsDir = path.join(packageRoot, 'skills');
36
-
37
- function listFlows(foundryDir) {
38
- const flowsDir = path.join(foundryDir, 'flows');
39
- if (!fs.existsSync(flowsDir)) return [];
40
- const flows = [];
41
- for (const entry of readdirSync(flowsDir)) {
42
- if (!entry.endsWith('.md') || entry === '.gitkeep') continue;
43
- try {
44
- const text = readFileSync(path.join(flowsDir, entry), 'utf-8');
45
- const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
46
- if (!fmMatch) continue;
47
- const fm = fmMatch[1];
48
- const idMatch = fm.match(/^id:\s*(.+)$/m);
49
- const nameMatch = fm.match(/^name:\s*(.+)$/m);
50
- const startingMatch = fm.match(/^starting-cycles:\s*\n((?:\s*-\s*.+\n?)+)/m);
51
- const id = idMatch ? idMatch[1].trim() : entry.replace(/\.md$/, '');
52
- const name = nameMatch ? nameMatch[1].trim() : id;
53
- const startingCycles = startingMatch
54
- ? startingMatch[1].split('\n').map(l => l.replace(/^\s*-\s*/, '').trim()).filter(Boolean)
55
- : [];
56
- flows.push({ id, name, startingCycles });
57
- } catch { /* skip bad files */ }
58
- }
59
- return flows;
60
- }
61
-
62
- function getBootstrapContent(directory) {
63
- const foundryDir = path.join(directory, 'foundry');
64
- const foundryExists = fs.existsSync(foundryDir) && fs.statSync(foundryDir).isDirectory();
65
-
66
- if (!foundryExists) {
67
- return `<FOUNDRY_CONTEXT>
68
- Foundry is installed but not initialized in this project. There is no foundry/ directory.
69
-
70
- To set up Foundry, use the \`init-foundry\` skill. This will create the foundry/ directory structure
71
- and guide you through defining artefact types, laws, appraisers, cycles, and flows.
72
- </FOUNDRY_CONTEXT>`;
73
- }
74
-
75
- const flows = listFlows(foundryDir);
76
- const flowList = flows.length > 0
77
- ? flows.map(f => {
78
- const sc = f.startingCycles.length > 0 ? ` — starting cycles: ${f.startingCycles.join(', ')}` : '';
79
- return `- \`${f.id}\` — ${f.name}${sc}`;
80
- }).join('\n')
81
- : '- (no flows defined yet — use the `add-flow` skill to create one)';
82
-
83
- return `<FOUNDRY_CONTEXT>
84
- Foundry is active in this project. The foundry/ directory contains the project's artefact definitions,
85
- laws, appraisers, cycles, and flows.
86
-
87
- Foundry is a skill-driven framework for governed artefact generation and evaluation.
88
- The pipeline: forge (produce) → quench (deterministic checks) → appraise (subjective evaluation) → iterate.
89
-
90
- ## Defined flows
91
-
92
- ${flowList}
93
-
94
- **CRITICAL ROUTING RULE:** When the user references any flow above — by id (e.g. "creative-flow"),
95
- by name (e.g. "Creative Flow"), or by clear paraphrase (e.g. "the creative flow", "use the creative pipeline") —
96
- invoke the \`flow\` skill DIRECTLY with that flow's id. Do NOT invoke brainstorming, do NOT explore the
97
- codebase, do NOT ask clarifying questions about what to build. The flow's cycles already define the
98
- work. The user's request text (e.g. "make a haiku about X") is the goal to pass to the flow.
99
-
100
- Brainstorming applies to NEW features being added to foundry itself (new cycles, new artefact types,
101
- new skills). It does NOT apply to running an existing, defined flow.
102
-
103
- ## Available skills
104
-
105
- - **Pipeline:** forge, quench, appraise, orchestrate, flow, human-appraise
106
- - **Authoring:** add-artefact-type, add-law, add-appraiser, add-cycle, add-flow, init-foundry
107
- - **Maintenance:** upgrade-foundry, refresh-agents, list-agents
108
-
109
- ## Multi-model routing
110
-
111
- Foundry uses \`foundry-*\` sub-agents defined as markdown files in \`.opencode/agents/\`.
112
- Run the \`refresh-agents\` skill to regenerate them after adding or removing providers.
113
- Cycle definitions can specify per-stage models via the \`models\` frontmatter map. Appraisers can override with their own \`model\` field.
114
-
115
- All user content lives under foundry/.
116
- Scripts are located at: ${path.join(packageRoot, 'scripts')}
117
- </FOUNDRY_CONTEXT>`;
118
- }
119
-
120
- function makeIO(directory) {
121
- const resolve = (p) => path.isAbsolute(p) ? p : path.join(directory, p);
122
- return {
123
- exists: (p) => existsSync(resolve(p)),
124
- readFile: (p) => readFileSync(resolve(p), 'utf-8'),
125
- writeFile: (p, content) => writeFileSync(resolve(p), content, 'utf-8'),
126
- readDir: (p) => readdirSync(resolve(p)),
127
- mkdir: (p) => mkdirSync(resolve(p), { recursive: true }),
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'] }),
135
- };
136
- }
137
-
138
- export const FoundryPlugin = async ({ directory }) => {
139
- // Bootstrap per-worktree HMAC secret (created on first boot, persisted to .foundry/secret).
140
- // Note: `directory` is the worktree root at plugin-boot time. Per-invocation `context.worktree`
141
- // may differ in multi-worktree setups — we still use `context.worktree` inside tool `execute`
142
- // bodies to locate `.foundry/` on disk, and use the plugin-boot `secret` only for
143
- // signing/verifying. A worktree change mid-session would mismatch; deferred out of v2.2.0 scope.
144
- const secret = readOrCreateSecret(directory);
145
- const pending = createPendingStore();
146
-
147
- const plugin = {
148
- config: async (config) => {
149
- config.skills = config.skills || {};
150
- config.skills.paths = config.skills.paths || [];
151
-
152
- // Always register all skills — individual skills check for foundry/ dir
153
- if (!config.skills.paths.includes(allSkillsDir)) {
154
- config.skills.paths.push(allSkillsDir);
155
- }
156
- },
157
-
158
- 'experimental.chat.messages.transform': async (_input, output) => {
159
- const bootstrap = getBootstrapContent(directory);
160
- if (!bootstrap || !output.messages.length) return;
161
-
162
- const firstUser = output.messages.find(m => m.info.role === 'user');
163
- if (!firstUser || !firstUser.parts.length) return;
164
-
165
- if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('FOUNDRY_CONTEXT'))) return;
166
-
167
- const ref = firstUser.parts[0];
168
- firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap });
169
- },
170
-
171
- tool: {
172
- // ── History tools ──
173
- foundry_history_list: tool({
174
- description: 'List history entries for a cycle',
175
- args: {
176
- cycle: tool.schema.string().describe('Cycle name'),
177
- },
178
- async execute(args, context) {
179
- const io = makeIO(context.worktree);
180
- const historyPath = path.join(context.worktree, 'WORK.history.yaml');
181
- const entries = loadHistory(historyPath, args.cycle, io);
182
- return JSON.stringify(entries);
183
- },
184
- }),
185
-
186
- // ── Stage lifecycle tools ──
187
- foundry_stage_begin: tool({
188
- description: 'Open a subagent work stage; consumes a dispatch token from foundry_sort.',
189
- args: {
190
- stage: tool.schema.string().describe('Stage alias, e.g. "forge:create-haiku"'),
191
- cycle: tool.schema.string().describe('Cycle name'),
192
- token: tool.schema.string().describe('Token received from foundry_sort via the dispatch prompt'),
193
- },
194
- async execute(args, context) {
195
- const io = makeIO(context.worktree);
196
- // Precondition: no active stage.
197
- const current = readActiveStage(io);
198
- if (current) {
199
- return JSON.stringify({ error: `foundry_stage_begin requires no active stage; current: ${current.stage}` });
200
- }
201
- // Verify token signature + expiry.
202
- const v = verifyToken(args.token, secret);
203
- if (!v.ok) return JSON.stringify({ error: `foundry_stage_begin: token ${v.reason}` });
204
- // Payload must match args.
205
- if (v.payload.route !== args.stage || v.payload.cycle !== args.cycle) {
206
- return JSON.stringify({ error: `foundry_stage_begin: token payload mismatch (route=${v.payload.route}, cycle=${v.payload.cycle})` });
207
- }
208
- // Single-use nonce check.
209
- const meta = pending.consume(v.payload.nonce);
210
- if (!meta) return JSON.stringify({ error: `foundry_stage_begin: nonce not pending or already consumed` });
211
-
212
- // Resolve base SHA from git.
213
- let baseSha;
214
- try {
215
- baseSha = execSync('git rev-parse HEAD', { cwd: context.worktree }).toString().trim();
216
- } catch {
217
- return JSON.stringify({ error: `foundry_stage_begin: git rev-parse HEAD failed (no commits?)` });
218
- }
219
-
220
- const tokenHash = createHash('sha256').update(args.token).digest('hex');
221
- const active = {
222
- cycle: args.cycle,
223
- stage: args.stage,
224
- tokenHash,
225
- baseSha,
226
- startedAt: new Date().toISOString(),
227
- };
228
- writeActiveStage(io, active);
229
- return JSON.stringify({ ok: true, active });
230
- },
231
- }),
232
-
233
- foundry_stage_end: tool({
234
- description: 'Close the active subagent work stage; preserves baseSha for finalize.',
235
- args: {
236
- summary: tool.schema.string().describe('Short summary of the work done'),
237
- },
238
- async execute(args, context) {
239
- const io = makeIO(context.worktree);
240
- const active = readActiveStage(io);
241
- if (!active) return JSON.stringify({ error: 'foundry_stage_end requires active stage; current: none' });
242
- writeLastStage(io, { cycle: active.cycle, stage: active.stage, baseSha: active.baseSha, summary: args.summary });
243
- clearActiveStage(io);
244
- return JSON.stringify({ ok: true, summary: args.summary });
245
- },
246
- }),
247
-
248
- // ── Workfile tools ──
249
- foundry_workfile_create: tool({
250
- description: 'Create WORK.md with frontmatter and goal',
251
- args: {
252
- flow: tool.schema.string().describe('Flow name'),
253
- cycle: tool.schema.string().describe('Cycle name'),
254
- stages: tool.schema.array(tool.schema.string()).optional().describe('Ordered stage names'),
255
- maxIterations: tool.schema.number().optional().describe('Maximum iterations'),
256
- goal: tool.schema.string().describe('Goal text'),
257
- models: tool.schema.string().optional().describe('Per-stage model overrides as JSON object, e.g. \'{"forge":"openai/gpt-4o"}\''),
258
- },
259
- async execute(args, context) {
260
- const io = makeIO(context.worktree);
261
- const guard = requireNoActiveStage(io);
262
- if (!guard.ok) return JSON.stringify({ error: `foundry_workfile_create ${guard.error}` });
263
- const workPath = path.join(context.worktree, 'WORK.md');
264
- if (existsSync(workPath)) {
265
- return JSON.stringify({ error: 'foundry_workfile_create requires no WORK.md; current: exists' });
266
- }
267
- const fm = { flow: args.flow, cycle: args.cycle };
268
- if (args.stages) {
269
- fm.stages = enrichStages(args.stages, args.cycle);
270
- }
271
- if (args.maxIterations !== undefined) {
272
- fm['max-iterations'] = args.maxIterations;
273
- }
274
- if (args.models) {
275
- fm.models = parseModelsValue(args.models);
276
- }
277
- const content = createWorkfile(fm, args.goal);
278
- writeFileSync(workPath, content, 'utf-8');
279
- return JSON.stringify({ ok: true });
280
- },
281
- }),
282
-
283
- foundry_workfile_get: tool({
284
- description: 'Read WORK.md and return frontmatter + goal',
285
- args: {},
286
- async execute(_args, context) {
287
- const workPath = path.join(context.worktree, 'WORK.md');
288
- if (!existsSync(workPath)) {
289
- return JSON.stringify({ error: 'WORK.md not found' });
290
- }
291
- const text = readFileSync(workPath, 'utf-8');
292
- const fm = parseFrontmatter(text);
293
- const goalMatch = text.match(/# Goal\n\n([\s\S]*?)(?=\n\||\n##|$)/);
294
- const goal = goalMatch ? goalMatch[1].trim() : '';
295
- return JSON.stringify({ ...fm, goal });
296
- },
297
- }),
298
-
299
- foundry_workfile_delete: tool({
300
- description: 'Delete WORK.md and WORK.history.yaml (requires confirm:true)',
301
- args: {
302
- confirm: tool.schema.boolean().describe('Must be true to confirm deletion'),
303
- },
304
- async execute(args, context) {
305
- const io = makeIO(context.worktree);
306
- const guard = requireNoActiveStage(io);
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}' });
310
- }
311
- const workPath = path.join(context.worktree, 'WORK.md');
312
- const historyPath = path.join(context.worktree, 'WORK.history.yaml');
313
- if (existsSync(workPath)) {
314
- unlinkSync(workPath);
315
- }
316
- if (existsSync(historyPath)) {
317
- unlinkSync(historyPath);
318
- }
319
- return JSON.stringify({ ok: true });
320
- },
321
- }),
322
-
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.',
326
- args: {
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)'),
332
- },
333
- async execute(args, context) {
334
- const { runOrchestrate } = await import('../../scripts/orchestrate.js');
335
- const io = makeIO(context.worktree);
336
- const cwd = context.worktree;
337
-
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
- };
345
-
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();
352
- },
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
- };
358
-
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: [] });
403
- }
404
- },
405
- }),
406
-
407
- // ── Artefacts tools ──
408
- // NOTE: `foundry_artefacts_add` was removed in v2.2.0. Artefacts are now
409
- // registered automatically by `foundry_stage_finalize` as drafts, then
410
- // promoted to done|blocked via `foundry_artefacts_set_status`.
411
- foundry_artefacts_set_status: tool({
412
- description: 'Update the status of an artefact in WORK.md (done|blocked only)',
413
- args: {
414
- file: tool.schema.string().describe('Artefact file path'),
415
- status: tool.schema.string().describe('New status (done|blocked)'),
416
- },
417
- async execute(args, context) {
418
- const io = makeIO(context.worktree);
419
- const guard = requireNoActiveStage(io);
420
- if (!guard.ok) return JSON.stringify({ error: `foundry_artefacts_set_status ${guard.error}` });
421
- const workPath = path.join(context.worktree, 'WORK.md');
422
- const text = readFileSync(workPath, 'utf-8');
423
- try {
424
- const updated = setArtefactStatus(text, args.file, args.status);
425
- writeFileSync(workPath, updated, 'utf-8');
426
- return JSON.stringify({ ok: true });
427
- } catch (e) {
428
- return JSON.stringify({ error: e.message });
429
- }
430
- },
431
- }),
432
-
433
- foundry_artefacts_list: tool({
434
- description: 'List artefacts from the WORK.md table. Optionally filter by cycle — callers should always pass the current cycle to avoid picking up stale rows from prior sessions.',
435
- args: {
436
- cycle: tool.schema.string().optional().describe('Only return rows whose Cycle column matches this value'),
437
- },
438
- async execute(args, context) {
439
- const workPath = path.join(context.worktree, 'WORK.md');
440
- if (!existsSync(workPath)) {
441
- return JSON.stringify({ error: 'WORK.md not found' });
442
- }
443
- const text = readFileSync(workPath, 'utf-8');
444
- const rows = parseArtefactsTable(text);
445
- const filtered = args.cycle ? rows.filter(r => r.cycle === args.cycle) : rows;
446
- return JSON.stringify(filtered);
447
- },
448
- }),
449
-
450
- // ── Feedback tools ──
451
- foundry_feedback_add: tool({
452
- description: 'Add a feedback item to WORK.md under a file heading',
453
- args: {
454
- file: tool.schema.string().describe('Artefact file path'),
455
- text: tool.schema.string().describe('Feedback text'),
456
- tag: tool.schema.string().describe('Tag for the feedback item'),
457
- },
458
- async execute(args, context) {
459
- const io = makeIO(context.worktree);
460
- const guard = requireActiveStage(io);
461
- if (!guard.ok) return JSON.stringify({ error: `foundry_feedback_add requires active stage; ${guard.error}` });
462
- const stageBase = stageBaseOf(guard.active.stage);
463
- // Per-stage tag allow-list.
464
- if (stageBase === 'forge') {
465
- return JSON.stringify({ error: 'foundry_feedback_add: forge stages do not add feedback' });
466
- }
467
- if (stageBase === 'quench' && args.tag !== 'validation') {
468
- return JSON.stringify({ error: `foundry_feedback_add: quench may only add tag "validation"; got "${args.tag}"` });
469
- }
470
- if (stageBase === 'appraise' && !args.tag.startsWith('law:')) {
471
- return JSON.stringify({ error: `foundry_feedback_add: appraise tag must start with "law:"; got "${args.tag}"` });
472
- }
473
- if (stageBase === 'human-appraise' && args.tag !== 'human') {
474
- return JSON.stringify({ error: `foundry_feedback_add: human-appraise may only add tag "human"; got "${args.tag}"` });
475
- }
476
- const workPath = path.join(context.worktree, 'WORK.md');
477
- const content = readFileSync(workPath, 'utf-8');
478
- const r = addFeedbackItem(content, args.file, args.text, args.tag);
479
- if (!r.deduped) writeFileSync(workPath, r.text, 'utf-8');
480
- return JSON.stringify({ ok: true, deduped: r.deduped });
481
- },
482
- }),
483
-
484
- foundry_feedback_action: tool({
485
- description: 'Mark a feedback item as actioned [x]',
486
- args: {
487
- file: tool.schema.string().describe('Artefact file path'),
488
- index: tool.schema.number().describe('Zero-based index of the feedback item'),
489
- },
490
- async execute(args, context) {
491
- const io = makeIO(context.worktree);
492
- const guard = requireActiveStage(io);
493
- if (!guard.ok) return JSON.stringify({ error: `foundry_feedback_action requires active stage; ${guard.error}` });
494
- const stageBase = stageBaseOf(guard.active.stage);
495
- if (stageBase !== 'forge') {
496
- return JSON.stringify({ error: `foundry_feedback_action requires active forge stage; current: ${guard.active.stage}` });
497
- }
498
- const workPath = path.join(context.worktree, 'WORK.md');
499
- const content = readFileSync(workPath, 'utf-8');
500
- const r = actionFeedbackItem(content, args.file, args.index, stageBase);
501
- if (!r.ok) return JSON.stringify({ error: r.error });
502
- writeFileSync(workPath, r.text, 'utf-8');
503
- return JSON.stringify({ ok: true });
504
- },
505
- }),
506
-
507
- foundry_feedback_wontfix: tool({
508
- description: 'Mark a feedback item as wont-fix [~] with reason',
509
- args: {
510
- file: tool.schema.string().describe('Artefact file path'),
511
- index: tool.schema.number().describe('Zero-based index of the feedback item'),
512
- reason: tool.schema.string().describe('Reason for wont-fix'),
513
- },
514
- async execute(args, context) {
515
- const io = makeIO(context.worktree);
516
- const guard = requireActiveStage(io);
517
- if (!guard.ok) return JSON.stringify({ error: `foundry_feedback_wontfix requires active stage; ${guard.error}` });
518
- const stageBase = stageBaseOf(guard.active.stage);
519
- if (stageBase !== 'forge') {
520
- return JSON.stringify({ error: `foundry_feedback_wontfix requires active forge stage; current: ${guard.active.stage}` });
521
- }
522
- const workPath = path.join(context.worktree, 'WORK.md');
523
- const content = readFileSync(workPath, 'utf-8');
524
- const r = wontfixFeedbackItem(content, args.file, args.index, args.reason, stageBase);
525
- if (!r.ok) return JSON.stringify({ error: r.error });
526
- writeFileSync(workPath, r.text, 'utf-8');
527
- return JSON.stringify({ ok: true });
528
- },
529
- }),
530
-
531
- foundry_feedback_resolve: tool({
532
- description: 'Resolve a feedback item (approved or rejected)',
533
- args: {
534
- file: tool.schema.string().describe('Artefact file path'),
535
- index: tool.schema.number().describe('Zero-based index of the feedback item'),
536
- resolution: tool.schema.enum(['approved', 'rejected']).describe('Resolution type'),
537
- reason: tool.schema.string().optional().describe('Reason (required if rejected)'),
538
- },
539
- async execute(args, context) {
540
- const io = makeIO(context.worktree);
541
- const guard = requireActiveStage(io);
542
- if (!guard.ok) return JSON.stringify({ error: `foundry_feedback_resolve requires active stage; ${guard.error}` });
543
- const stageBase = stageBaseOf(guard.active.stage);
544
- if (!['quench', 'appraise', 'human-appraise'].includes(stageBase)) {
545
- return JSON.stringify({ error: `foundry_feedback_resolve requires active quench|appraise|human-appraise stage; current: ${guard.active.stage}` });
546
- }
547
- const workPath = path.join(context.worktree, 'WORK.md');
548
- const content = readFileSync(workPath, 'utf-8');
549
- const r = resolveFeedbackItem(content, args.file, args.index, args.resolution, args.reason, stageBase);
550
- if (!r.ok) return JSON.stringify({ error: r.error });
551
- writeFileSync(workPath, r.text, 'utf-8');
552
- return JSON.stringify({ ok: true });
553
- },
554
- }),
555
-
556
- foundry_feedback_list: tool({
557
- description: 'List feedback items, optionally filtered by file',
558
- args: {
559
- file: tool.schema.string().optional().describe('Filter by artefact file path'),
560
- },
561
- async execute(args, context) {
562
- const workPath = path.join(context.worktree, 'WORK.md');
563
- if (!existsSync(workPath)) {
564
- return JSON.stringify({ error: 'WORK.md not found' });
565
- }
566
- const text = readFileSync(workPath, 'utf-8');
567
- const fm = parseFrontmatter(text);
568
- const artefacts = parseArtefactsTable(text);
569
- const cycle = fm.cycle || '';
570
- return JSON.stringify(listFeedback(text, cycle, artefacts, args.file));
571
- },
572
- }),
573
-
574
- // ── Git tools ──
575
- foundry_git_branch: tool({
576
- description: 'Create and checkout a work branch for a flow',
577
- args: {
578
- flowId: tool.schema.string().describe('Flow ID'),
579
- description: tool.schema.string().describe('Branch description suffix'),
580
- },
581
- async execute(args, context) {
582
- const io = makeIO(context.worktree);
583
- const guard = requireNoActiveStage(io);
584
- if (!guard.ok) return JSON.stringify({ error: `foundry_git_branch ${guard.error}` });
585
- const flowSlug = slugify(args.flowId);
586
- const descSlug = slugify(args.description);
587
- const branch = `work/${flowSlug}-${descSlug}`;
588
- execFileSync('git', ['checkout', '-b', branch], { cwd: context.worktree, encoding: 'utf8', stdio: 'pipe' });
589
- return JSON.stringify({ ok: true, branch });
590
- },
591
- }),
592
-
593
- foundry_git_finish: tool({
594
- description: 'Clean up work files, squash merge to base branch, and delete the work branch',
595
- args: {
596
- message: tool.schema.string().describe('Squash merge commit message'),
597
- baseBranch: tool.schema.string().optional().describe('Target branch (default: main)'),
598
- },
599
- async execute(args, context) {
600
- const io = makeIO(context.worktree);
601
- const guard = requireNoActiveStage(io);
602
- if (!guard.ok) return JSON.stringify({ error: `foundry_git_finish ${guard.error}` });
603
- const base = args.baseBranch || 'main';
604
- const cwd = context.worktree;
605
- const opts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
606
-
607
- // Get current branch name
608
- const workBranch = execFileSync('git', ['branch', '--show-current'], opts).trim();
609
- if (workBranch === base) {
610
- return JSON.stringify({ error: `Already on ${base} — nothing to merge` });
611
- }
612
-
613
- // Delete work files
614
- const workPath = path.join(cwd, 'WORK.md');
615
- const historyPath = path.join(cwd, 'WORK.history.yaml');
616
- if (existsSync(workPath)) unlinkSync(workPath);
617
- if (existsSync(historyPath)) unlinkSync(historyPath);
618
-
619
- // Commit cleanup if there are changes
620
- try {
621
- execFileSync('git', ['add', '-A'], opts);
622
- const status = execFileSync('git', ['status', '--porcelain'], opts).trim();
623
- if (status) {
624
- const cleanupMsg = `[${workBranch.replace('work/', '')}] cleanup: remove work files`;
625
- execFileSync('git', ['commit', '-m', cleanupMsg], opts);
626
- }
627
- } catch { /* no changes to commit */ }
628
-
629
- // Switch to base and squash merge
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();
634
-
635
- // Force-delete work branch (required after squash)
636
- execFileSync('git', ['branch', '-D', workBranch], opts);
637
-
638
- return JSON.stringify({ ok: true, hash, branch: base });
639
- },
640
- }),
641
-
642
- // ── Config tools ──
643
- foundry_config_cycle: tool({
644
- description: 'Get a cycle definition from foundry config',
645
- args: {
646
- cycleId: tool.schema.string().describe('Cycle ID'),
647
- },
648
- async execute(args, context) {
649
- const io = makeIO(context.worktree);
650
- const result = await getCycleDefinition('foundry', args.cycleId, io);
651
- return JSON.stringify(result);
652
- },
653
- }),
654
-
655
- foundry_config_artefact_type: tool({
656
- description: 'Get an artefact type definition',
657
- args: {
658
- typeId: tool.schema.string().describe('Artefact type ID'),
659
- },
660
- async execute(args, context) {
661
- const io = makeIO(context.worktree);
662
- const result = await getArtefactType('foundry', args.typeId, io);
663
- return JSON.stringify(result);
664
- },
665
- }),
666
-
667
- foundry_config_laws: tool({
668
- description: 'Get laws, optionally filtered by artefact type',
669
- args: {
670
- typeId: tool.schema.string().optional().describe('Artefact type ID'),
671
- },
672
- async execute(args, context) {
673
- const io = makeIO(context.worktree);
674
- const result = args.typeId
675
- ? await getLaws('foundry', args.typeId, io)
676
- : await getLaws('foundry', io);
677
- return JSON.stringify(result);
678
- },
679
- }),
680
-
681
- foundry_config_validation: tool({
682
- description: 'Get validation commands for an artefact type',
683
- args: {
684
- typeId: tool.schema.string().describe('Artefact type ID'),
685
- },
686
- async execute(args, context) {
687
- const io = makeIO(context.worktree);
688
- const result = await getValidation('foundry', args.typeId, io);
689
- return JSON.stringify(result);
690
- },
691
- }),
692
-
693
- foundry_config_appraisers: tool({
694
- description: 'List all appraisers',
695
- args: {},
696
- async execute(_args, context) {
697
- const io = makeIO(context.worktree);
698
- const result = await getAppraisers('foundry', io);
699
- return JSON.stringify(result);
700
- },
701
- }),
702
-
703
- foundry_config_flow: tool({
704
- description: 'Get a flow definition',
705
- args: {
706
- flowId: tool.schema.string().describe('Flow ID'),
707
- },
708
- async execute(args, context) {
709
- const io = makeIO(context.worktree);
710
- const result = await getFlow('foundry', args.flowId, io);
711
- return JSON.stringify(result);
712
- },
713
- }),
714
-
715
- // ── Validate tool ──
716
- foundry_validate_run: tool({
717
- description: 'Run validation commands for an artefact type against a file',
718
- args: {
719
- typeId: tool.schema.string().describe('Artefact type ID'),
720
- file: tool.schema.string().describe('File path to validate'),
721
- },
722
- async execute(args, context) {
723
- const io = makeIO(context.worktree);
724
- const commands = await getValidation('foundry', args.typeId, io);
725
- if (!commands || commands.length === 0) return JSON.stringify({ error: 'No validation defined for type: ' + args.typeId });
726
- const results = [];
727
- for (const entry of commands) {
728
- const expanded = entry.command.replace(/\{file\}/g, args.file);
729
- try {
730
- const output = execSync(expanded, { cwd: context.worktree, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
731
- results.push({ id: entry.id, command: expanded, passed: true, output: output.trim() });
732
- } catch (err) {
733
- results.push({ id: entry.id, command: expanded, passed: false, output: (err.stderr || err.stdout || err.message || '').trim(), failureMeans: entry.failureMeans });
734
- }
735
- }
736
- return JSON.stringify(results);
737
- },
738
- }),
739
-
740
- // ── Appraiser selection tool ──
741
- foundry_appraisers_select: tool({
742
- description: 'Select appraisers for an artefact type',
743
- args: {
744
- typeId: tool.schema.string().describe('Artefact type ID'),
745
- count: tool.schema.number().optional().describe('Number of appraisers to select'),
746
- },
747
- async execute(args, context) {
748
- const io = makeIO(context.worktree);
749
- const result = args.count
750
- ? await selectAppraisers('foundry', args.typeId, args.count, io)
751
- : await selectAppraisers('foundry', args.typeId, io);
752
- return JSON.stringify(result);
753
- },
754
- }),
755
- },
756
- };
757
-
758
- Object.defineProperty(plugin, Symbol.for('foundry.test.pending'), { value: pending });
759
- Object.defineProperty(plugin, Symbol.for('foundry.test.secret'), { value: secret });
760
- return plugin;
761
- };