@really-knows-ai/foundry 3.0.1 → 3.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.
Files changed (36) hide show
  1. package/README.md +8 -7
  2. package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +53 -69
  3. package/dist/.opencode/plugins/foundry-tools/helpers.js +10 -19
  4. package/dist/.opencode/plugins/foundry-tools/refresh-agents-tool.js +88 -0
  5. package/dist/.opencode/plugins/foundry-tools/validate-tools.js +37 -29
  6. package/dist/.opencode/plugins/foundry.js +2 -0
  7. package/dist/CHANGELOG.md +182 -0
  8. package/dist/README.md +8 -7
  9. package/dist/agents/foundry.md +37 -0
  10. package/dist/docs/architecture.md +6 -3
  11. package/dist/docs/concepts.md +1 -1
  12. package/dist/docs/getting-started.md +57 -135
  13. package/dist/docs/tools.md +21 -1
  14. package/dist/scripts/sort.js +1 -1
  15. package/dist/skills/add-appraiser/SKILL.md +19 -34
  16. package/dist/skills/add-artefact-type/SKILL.md +47 -43
  17. package/dist/skills/add-cycle/SKILL.md +28 -37
  18. package/dist/skills/add-extractor/SKILL.md +21 -33
  19. package/dist/skills/add-flow/SKILL.md +43 -88
  20. package/dist/skills/add-law/SKILL.md +132 -26
  21. package/dist/skills/add-memory-edge-type/SKILL.md +11 -17
  22. package/dist/skills/add-memory-entity-type/SKILL.md +9 -16
  23. package/dist/skills/change-embedding-model/SKILL.md +6 -8
  24. package/dist/skills/drop-memory-edge-type/SKILL.md +6 -8
  25. package/dist/skills/drop-memory-entity-type/SKILL.md +6 -8
  26. package/dist/skills/dry-run/SKILL.md +11 -28
  27. package/dist/skills/flow/SKILL.md +1 -1
  28. package/dist/skills/init-foundry/SKILL.md +47 -27
  29. package/dist/skills/init-memory/SKILL.md +11 -22
  30. package/dist/skills/list-agents/SKILL.md +1 -1
  31. package/dist/skills/refresh-agents/SKILL.md +4 -26
  32. package/dist/skills/rename-memory-edge-type/SKILL.md +6 -8
  33. package/dist/skills/rename-memory-entity-type/SKILL.md +6 -8
  34. package/dist/skills/reset-memory/SKILL.md +10 -16
  35. package/dist/skills/upgrade-foundry/SKILL.md +1 -1
  36. package/package.json +2 -1
package/README.md CHANGED
@@ -142,16 +142,17 @@ Open OpenCode in your project repo and say:
142
142
  > run init-foundry
143
143
  ```
144
144
 
145
- Foundry scaffolds a `foundry/` directory, generates one `foundry-<model>` agent file
146
- per model available in your session, commits the structure, and then asks you to
147
- restart. All the foundational configuration directories are created; you will
148
- populate them next.
145
+ Foundry scaffolds a `foundry/` directory, generates one `foundry-<model>` stage agent
146
+ file per model available in your session, installs the user-facing `Foundry` guide
147
+ agent, commits the structure, and asks you to restart.
149
148
 
150
- Restart OpenCode so the new `foundry-<model>` agents register multi-model dispatch cannot route to agents it cannot discover.
149
+ Restart OpenCode so the new agents register. After the restart, switch to the
150
+ **Foundry** agent. The Foundry agent is the normal interface for authoring and
151
+ running Foundry workflows.
151
152
 
152
- ### Phase 3 — Build a flow without writing one
153
+ ### Phase 3 — Ask the Foundry agent for a flow
153
154
 
154
- Ask Foundry to set up a flow:
155
+ With the **Foundry** agent active, ask it to set up a flow:
155
156
 
156
157
  ```
157
158
  > set up a flow that writes haikus
@@ -38,73 +38,47 @@ function findLawEnd(lines, startIdx) {
38
38
  return lines.length;
39
39
  }
40
40
 
41
- // Extract full markdown for a single law from file content
42
41
  function extractLawMarkdown(content, lawId) {
43
42
  const lines = content.split('\n');
44
43
  const startIdx = findLawStart(lines, lawId);
45
-
46
44
  if (startIdx < 0) return null;
47
-
48
45
  const endIdx = findLawEnd(lines, startIdx);
49
46
  const lawLines = lines.slice(startIdx, endIdx);
50
-
51
- while (lawLines.length > 0 && lawLines[lawLines.length - 1] === '') {
52
- lawLines.pop();
53
- }
54
-
47
+ while (lawLines.length > 0 && lawLines[lawLines.length - 1] === '') lawLines.pop();
55
48
  return lawLines.join('\n') + '\n';
56
49
  }
57
50
 
58
51
  async function searchGlobalLaws(io, foundryDir, lawId) {
59
52
  const globalLawsDir = join(foundryDir, 'laws');
60
- if (!(await io.exists(globalLawsDir))) {
61
- return null;
62
- }
63
-
53
+ if (!(await io.exists(globalLawsDir))) return null;
64
54
  const files = await io.readDir(globalLawsDir);
65
55
  for (const file of files) {
66
56
  if (!file.endsWith('.md')) continue;
67
57
  const path = join(globalLawsDir, file);
68
58
  const content = await io.readFile(path);
69
- if (contentContainsLaw(content, lawId)) {
70
- return { path, fullMarkdown: content, source: 'global' };
71
- }
59
+ if (contentContainsLaw(content, lawId)) return { path, fullMarkdown: content, source: 'global' };
72
60
  }
73
-
74
61
  return null;
75
62
  }
76
63
 
77
64
  async function searchTypeSpecificLaws(io, foundryDir, lawId) {
78
65
  const artefactsDir = join(foundryDir, 'artefacts');
79
- if (!(await io.exists(artefactsDir))) {
80
- return null;
81
- }
82
-
66
+ if (!(await io.exists(artefactsDir))) return null;
83
67
  const types = await io.readDir(artefactsDir);
84
68
  for (const typeId of types) {
85
69
  const typeLawsPath = join(artefactsDir, typeId, 'laws.md');
86
70
  if (!(await io.exists(typeLawsPath))) continue;
87
-
88
71
  const content = await io.readFile(typeLawsPath);
89
- if (contentContainsLaw(content, lawId)) {
90
- return { path: typeLawsPath, fullMarkdown: content, source: `type:${typeId}` };
91
- }
72
+ if (contentContainsLaw(content, lawId)) return { path: typeLawsPath, fullMarkdown: content, source: `type:${typeId}` };
92
73
  }
93
-
94
74
  return null;
95
75
  }
96
76
 
97
77
  async function findLawByID(io, foundryDir, lawId) {
98
- let result = await searchGlobalLaws(io, foundryDir, lawId);
99
- if (result) {
100
- return { found: true, ...result };
101
- }
102
-
103
- result = await searchTypeSpecificLaws(io, foundryDir, lawId);
104
- if (result) {
105
- return { found: true, ...result };
106
- }
107
-
78
+ const global = await searchGlobalLaws(io, foundryDir, lawId);
79
+ if (global) return { found: true, ...global };
80
+ const typeSpec = await searchTypeSpecificLaws(io, foundryDir, lawId);
81
+ if (typeSpec) return { found: true, ...typeSpec };
108
82
  return { found: false };
109
83
  }
110
84
 
@@ -204,82 +178,92 @@ function computeTargetPath(target) {
204
178
 
205
179
  // --- add law executor --------------------------------------------------------
206
180
 
181
+ function extractLawId(body) {
182
+ const match = body.match(/^## ([^\s]+)/m);
183
+ return match ? match[1] : null;
184
+ }
185
+
186
+ async function checkExistingLaw(io, path, lawId) {
187
+ if (!(await io.exists(path))) return { existedBefore: false, priorContent: null };
188
+ const priorContent = await io.readFile(path);
189
+ if (contentContainsLaw(priorContent, lawId)) {
190
+ return { error: `law id "${lawId}" already exists in ${path}; use foundry_config_edit_law to update it` };
191
+ }
192
+ return { existedBefore: true, priorContent };
193
+ }
194
+
207
195
  async function validateAddLawPrerequisites(io, args) {
208
196
  const targetError = validateAddLawTarget(args.target);
209
- if (targetError) {
210
- return { error: targetError };
211
- }
197
+ if (targetError) return { error: targetError };
212
198
 
213
199
  const path = computeTargetPath(args.target);
214
200
  const validation = await validateLaw({ body: args.body, io });
215
- if (!validation.ok) {
216
- return validation;
217
- }
201
+ if (!validation.ok) return validation;
218
202
 
219
- if (await io.exists(path)) {
220
- return {
221
- ok: false,
222
- errors: [`${path} already exists; use foundry_config_edit_law to update an existing law in place`],
223
- };
224
- }
203
+ const lawId = extractLawId(args.body);
204
+ if (!lawId) return { error: 'could not determine law id from body (expected "## <law-id>" heading)' };
205
+
206
+ const existing = await checkExistingLaw(io, path, lawId);
207
+ if (existing.error) return { error: existing.error };
208
+ return { ok: true, path, lawId, ...existing };
209
+ }
210
+
211
+ function formatAddLawError(err) {
212
+ return err instanceof UnexpectedFilesError
213
+ ? JSON.stringify({ error: err.message, affected_files: err.files })
214
+ : errorJson(err);
215
+ }
225
216
 
226
- return { ok: true, path };
217
+ function buildNextContent(existedBefore, priorContent, body) {
218
+ return existedBefore ? priorContent.trimEnd() + '\n\n' + body.trimStart() : body;
219
+ }
220
+
221
+ async function rollbackAddLaw(io, path, existedBefore, priorContent) {
222
+ if (existedBefore) await io.writeFile(path, priorContent);
223
+ else await io.rm(path);
227
224
  }
228
225
 
229
226
  async function executeAddLaw(args, context) {
230
227
  const io = makeAsyncIO(context.worktree);
231
228
  const execFile = makeExecFile(context.worktree);
229
+ let path, existedBefore, priorContent;
232
230
 
233
231
  try {
234
232
  const prereq = await validateAddLawPrerequisites(io, args);
235
- if (prereq.error) {
236
- return JSON.stringify({ ok: false, errors: [prereq.error] });
237
- }
238
- if (!prereq.ok) {
239
- return JSON.stringify(prereq);
240
- }
233
+ if (prereq.error) return JSON.stringify({ ok: false, errors: [prereq.error] });
234
+ if (!prereq.ok) return JSON.stringify(prereq);
241
235
 
242
- const path = prereq.path;
236
+ ({ path, existedBefore, priorContent } = prereq);
237
+ const nextContent = buildNextContent(existedBefore, priorContent, args.body);
243
238
 
244
239
  await io.mkdirp(dirname(path));
245
- await io.writeFile(path, args.body);
240
+ await io.writeFile(path, nextContent);
246
241
 
247
242
  const sha = commitWithPolicy({
248
243
  message: `config: add law ${args.name}\n\nvia foundry_config_add_law`,
249
244
  allowedPatterns: ['foundry/**'],
250
245
  execFile,
251
246
  });
252
-
253
247
  return JSON.stringify({ ok: true, path, sha });
254
248
  } catch (err) {
255
- if (err instanceof UnexpectedFilesError) {
256
- return JSON.stringify({ error: err.message, affected_files: err.files });
257
- }
258
- return errorJson(err);
249
+ if (path) await rollbackAddLaw(io, path, existedBefore, priorContent);
250
+ return formatAddLawError(err);
259
251
  }
260
252
  }
261
253
 
262
254
  // --- helper for preserving sibling laws -------------------------------------------------------
263
255
 
264
- // Replace a law in file content while preserving other laws
265
256
  function replaceLawInContent(content, lawId, newLawMarkdown) {
266
257
  const lines = content.split('\n');
267
258
  const startIdx = findLawStart(lines, lawId);
268
259
  if (startIdx < 0) return content.trimEnd() + '\n\n' + newLawMarkdown;
269
-
270
260
  const endIdx = findLawEnd(lines, startIdx);
271
261
  const before = lines.slice(0, startIdx);
272
262
  const after = lines.slice(endIdx);
273
-
274
- // Trim trailing empty lines from before
275
263
  const beforeEnd = before.findLastIndex(l => l !== '') + 1;
276
264
  before.length = beforeEnd;
277
-
278
- // Trim leading empty lines from after
279
265
  const afterStart = after.findIndex(l => l !== '');
280
266
  if (afterStart > 0) after.splice(0, afterStart);
281
-
282
- // newLawMarkdown includes trailing newline; split and rejoin without final empty string
283
267
  const newLines = newLawMarkdown.trimEnd().split('\n');
284
268
  return before.concat(newLines, after).join('\n') + '\n';
285
269
  }
@@ -94,14 +94,13 @@ function buildFoundryNotInitializedMessage() {
94
94
  return `<FOUNDRY_CONTEXT>
95
95
  Foundry is installed but not initialised in this project. There is no foundry/ directory.
96
96
 
97
- To set up Foundry, use the \`init-foundry\` skill. This will create the foundry/ directory structure
98
- and guide you through defining artefact types, laws, appraisers, cycles, and flows.
97
+ To set up Foundry, initialise the project first. Initialisation creates the foundry/ directory structure, installs the user-facing Foundry agent, and generates model-routing stage agents. After initialisation, restart OpenCode and switch to the Foundry agent.
99
98
  </FOUNDRY_CONTEXT>`;
100
99
  }
101
100
 
102
101
  function buildFlowList(flows) {
103
102
  if (flows.length === 0) {
104
- return '- (no flows defined yet — use the `add-flow` skill to create one)';
103
+ return '- (no flows defined yet — ask the Foundry agent to set one up)';
105
104
  }
106
105
  return flows.map(f => {
107
106
  const sc = f.startingCycles.length > 0 ? ` — starting cycles: ${f.startingCycles.join(', ')}` : '';
@@ -121,27 +120,19 @@ The pipeline: assay (populate memory) → forge (produce) → quench (determinis
121
120
 
122
121
  ${flowList}
123
122
 
124
- **CRITICAL ROUTING RULE:** When the user references any flow above — by id (e.g. "creative-flow"),
125
- by name (e.g. "Creative Flow"), or by clear paraphrase (e.g. "the creative flow", "use the creative pipeline") —
126
- invoke the \`flow\` skill DIRECTLY with that flow's id. Do NOT invoke brainstorming, do NOT explore the
127
- codebase, do NOT ask clarifying questions about what to build. The flow's cycles already define the
128
- work. The user's request text (e.g. "make a haiku about X") is the goal to pass to the flow.
123
+ When the user references any flow above — by id (e.g. "creative-flow"),
124
+ by name (e.g. "Creative Flow"), or by clear paraphrase (e.g. "the creative flow",
125
+ "use the creative pipeline") ask the Foundry agent to run that flow with the user's
126
+ request as the goal. The Foundry agent handles cycle selection, work-branch creation, and
127
+ orchestration automatically.
129
128
 
130
- Brainstorming applies to NEW features being added to foundry itself (new cycles, new artefact types,
131
- new skills). It does NOT apply to running an existing, defined flow.
129
+ ## Foundry agent capabilities
132
130
 
133
- ## Available skills
134
-
135
- - **Pipeline:** assay, forge, quench, appraise, orchestrate, flow, human-appraise
136
- - **Authoring:** add-artefact-type, add-law, add-appraiser, add-cycle, add-flow, add-memory-entity-type, add-memory-edge-type, add-extractor, init-foundry
137
- - **Maintenance:** upgrade-foundry, refresh-agents, list-agents, init-memory, change-embedding-model, dry-run
138
- - **Memory Admin:** drop-memory-entity-type, drop-memory-edge-type, rename-memory-entity-type, rename-memory-edge-type, reset-memory
131
+ The Foundry agent has internal workflows for pipeline execution, authoring, maintenance, memory administration, and dry-run trials. Present these capabilities as Foundry outcomes instead of naming internal skills.
139
132
 
140
133
  ## Multi-model routing
141
134
 
142
- Foundry uses \`foundry-*\` sub-agents defined as markdown files in \`.opencode/agents/\`.
143
- Run the \`refresh-agents\` skill to regenerate them after adding or removing providers.
144
- Cycle definitions can specify per-stage models via the \`models\` frontmatter map. Appraisers can override with their own \`model\` field.
135
+ Foundry uses generated \`foundry-*\` stage agents for cycle stage dispatch. The user-facing \`Foundry\` agent is installed as \`.opencode/agents/foundry.md\` and should be used for authoring and running Foundry workflows.
145
136
 
146
137
  All user content lives under foundry/.
147
138
  Scripts are located at: ${path.join(packageRoot, 'scripts')}
@@ -0,0 +1,88 @@
1
+ import path from 'path';
2
+ import { execFileSync } from 'child_process';
3
+ import { mkdirSync, readdirSync, writeFileSync, unlinkSync } from 'fs';
4
+
5
+ const AGENT_FRONTMATTER_TEMPLATE = `---
6
+ description: "Foundry stage agent using MODEL_ID"
7
+ mode: subagent
8
+ model: "MODEL_ID"
9
+ hidden: true
10
+ ---
11
+ You are a Foundry stage agent. Follow the skill instructions provided in your task prompt exactly.
12
+ `;
13
+
14
+ function makeSlug(modelId) {
15
+ return modelId.replace(/[/.]/g, '-');
16
+ }
17
+
18
+ function buildAgentContent(modelId) {
19
+ return AGENT_FRONTMATTER_TEMPLATE.replace(/MODEL_ID/g, modelId);
20
+ }
21
+
22
+ function listModels(worktree) {
23
+ const stdout = execFileSync('opencode', ['models'], {
24
+ cwd: worktree,
25
+ encoding: 'utf8',
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ });
28
+ return stdout
29
+ .split('\n')
30
+ .map(line => line.trim())
31
+ .filter(line => line.length > 0);
32
+ }
33
+
34
+ function deleteStaleAgents(agentsDir) {
35
+ let existing;
36
+ try {
37
+ existing = readdirSync(agentsDir);
38
+ } catch {
39
+ existing = [];
40
+ }
41
+ for (const entry of existing) {
42
+ if (entry.startsWith('foundry-') && entry.endsWith('.md')) {
43
+ unlinkSync(path.join(agentsDir, entry));
44
+ }
45
+ }
46
+ }
47
+
48
+ function writeAgentFiles(agentsDir, models) {
49
+ for (const modelId of models) {
50
+ const slug = makeSlug(modelId);
51
+ const filePath = path.join(agentsDir, `foundry-${slug}.md`);
52
+ writeFileSync(filePath, buildAgentContent(modelId), 'utf8');
53
+ }
54
+ }
55
+
56
+ function refreshAgents(worktree) {
57
+ const models = listModels(worktree);
58
+ if (models.length === 0) {
59
+ return { ok: false, error: 'No models returned by `opencode models`. Is the opencode CLI available?' };
60
+ }
61
+
62
+ const agentsDir = path.join(worktree, '.opencode', 'agents');
63
+ mkdirSync(agentsDir, { recursive: true });
64
+ deleteStaleAgents(agentsDir);
65
+ writeAgentFiles(agentsDir, models);
66
+
67
+ return { ok: true, count: models.length };
68
+ }
69
+
70
+ export function createRefreshAgentsTool({ tool }) {
71
+ return {
72
+ foundry_refresh_agents: tool({
73
+ description: 'Regenerate .opencode/agents/foundry-*.md stage-agent files from the currently available models.',
74
+ args: {},
75
+ async execute(_args, context) {
76
+ try {
77
+ const result = refreshAgents(context.worktree);
78
+ return JSON.stringify(result);
79
+ } catch (err) {
80
+ return JSON.stringify({
81
+ ok: false,
82
+ error: `foundry_refresh_agents: ${err.message ?? String(err)}`,
83
+ });
84
+ }
85
+ },
86
+ }),
87
+ };
88
+ }
@@ -52,7 +52,7 @@ async function executeValidator(expanded, worktree, patterns) {
52
52
  * JSON or missing required fields, `pattern-mismatch` for files that
53
53
  * didn't match the artefact type's `file-patterns`.
54
54
  */
55
- async function runValidators(laws, patterns, patternSubstitution, worktree) {
55
+ async function runValidators(laws, patterns, substitutions, worktree) {
56
56
  const results = {
57
57
  validatorsRun: 0,
58
58
  items: [],
@@ -61,7 +61,7 @@ async function runValidators(laws, patterns, patternSubstitution, worktree) {
61
61
 
62
62
  for (const law of laws) {
63
63
  if (!law.validators || law.validators.length === 0) continue;
64
- await runLawValidators(law, patterns, patternSubstitution, worktree, results);
64
+ await runLawValidators(law, patterns, substitutions, worktree, results);
65
65
  }
66
66
 
67
67
  return results;
@@ -70,15 +70,15 @@ async function runValidators(laws, patterns, patternSubstitution, worktree) {
70
70
  /**
71
71
  * Run validators for a single law.
72
72
  */
73
- async function runLawValidators(law, patterns, patternSubstitution, worktree, results) {
73
+ async function runLawValidators(law, patterns, substitutions, worktree, results) {
74
74
  for (const validator of law.validators) {
75
- // Skip validators if pattern substitution is empty (no matching files)
76
- // Self-resolving validators (npm test, tsc) omit {pattern}, so they still run
77
- if (patternSubstitution === '' && validator.command.includes('{pattern}')) {
75
+ // Skip iff command uses {files} and there are no matching files.
76
+ // {pattern}-only and verbatim commands always run.
77
+ if (substitutions.files === '' && /(?:^|\s)\{files\}(?=\s|$)/.test(validator.command)) {
78
78
  continue;
79
79
  }
80
80
  results.validatorsRun++;
81
- const expanded = expandValidatorCommand(validator.command, patternSubstitution);
81
+ const expanded = expandValidatorCommand(validator.command, substitutions);
82
82
  const parseResult = await executeValidator(expanded, worktree, patterns);
83
83
  collectValidatorResult(parseResult, law.id, validator.id, results);
84
84
  }
@@ -160,8 +160,11 @@ async function performValidation(args, context) {
160
160
  */
161
161
  async function runValidatorsAndReport(laws, patterns, worktree) {
162
162
  const expandedFiles = await expandPatterns(patterns, worktree);
163
- const patternSubstitution = expandedFiles.map(shellQuote).join(' ');
164
- const results = await runValidators(laws, patterns, patternSubstitution, worktree);
163
+ const substitutions = {
164
+ pattern: patterns.map(shellQuote).join(' '),
165
+ files: expandedFiles.map(shellQuote).join(' '),
166
+ };
167
+ const results = await runValidators(laws, patterns, substitutions, worktree);
165
168
 
166
169
  return JSON.stringify({
167
170
  ok: results.errors.length === 0,
@@ -235,29 +238,34 @@ async function expandPatterns(patterns, worktree) {
235
238
  }
236
239
 
237
240
  /**
238
- * Expand validator command by replacing {pattern} placeholder.
241
+ * Expand validator command by replacing {pattern} and {files} placeholders.
242
+ *
243
+ * - {pattern} → space-separated, shell-quoted globs from the artefact
244
+ * type's `file-patterns:` array (e.g. "'haikus/*.md' 'drafts/*.md'").
245
+ * - {files} → space-separated, shell-quoted matching file paths in the
246
+ * worktree (e.g. "'haikus/one.md' 'haikus/two.md'").
239
247
  *
240
- * Only replaces {pattern} when it appears as a standalone token bounded by
241
- * whitespace or string start/end. This allows self-resolving validators
242
- * (e.g., npm test, tsc --noEmit) to omit the placeholder without risk of
243
- * accidental substitution if they contain the literal text "{pattern}" as part
244
- * of another string.
248
+ * Both placeholders are recognised only as standalone tokens, bounded
249
+ * by whitespace or start/end of string. Surrounding single or double
250
+ * quotes around the placeholder are stripped first so authors can
251
+ * write `rg "{pattern}"` for readability.
245
252
  *
246
- * @param {string} command - The validator command
247
- * @param {string} patternSubstitution - Shell-quoted file paths, space-separated
248
- * @returns {string} The expanded command
253
+ * @param {string} command
254
+ * @param {{ pattern: string, files: string }} substitutions
255
+ * @returns {string}
249
256
  */
250
- export function expandValidatorCommand(command, patternSubstitution) {
251
- // First strip surrounding quotes around {pattern} to handle cases like
252
- // rg "{pattern}" where authors add quotes for readability
253
- const cmd = command
257
+ export function expandValidatorCommand(command, { pattern, files }) {
258
+ let cmd = command
254
259
  .replace(/"\{pattern\}"/g, '{pattern}')
255
- .replace(/'\{pattern\}'/g, '{pattern}');
260
+ .replace(/'\{pattern\}'/g, '{pattern}')
261
+ .replace(/"\{files\}"/g, '{files}')
262
+ .replace(/'\{files\}'/g, '{files}');
256
263
 
257
- // Only substitute {pattern} when it appears as a standalone token
258
- // (bounded by whitespace or start/end of string)
259
- return cmd.replace(/(?:^|\s)\{pattern\}(?=\s|$)/g, (match) => {
260
- const leadingSpace = match.startsWith('{') ? '' : ' ';
261
- return leadingSpace + patternSubstitution;
262
- });
264
+ cmd = cmd.replace(/(?:^|\s)\{pattern\}(?=\s|$)/g, (match) =>
265
+ match.startsWith('{') ? pattern : ' ' + pattern);
266
+
267
+ cmd = cmd.replace(/(?:^|\s)\{files\}(?=\s|$)/g, (match) =>
268
+ match.startsWith('{') ? files : ' ' + files);
269
+
270
+ return cmd;
263
271
  }
@@ -29,6 +29,7 @@ import { createMemoryTools } from './foundry-tools/memory-tools.js';
29
29
  import { createMemoryAdminTools } from './foundry-tools/memory-admin-tools.js';
30
30
  import { createSnapshotTools } from './foundry-tools/snapshot-tools.js';
31
31
  import { createAttestationTools } from './foundry-tools/attestation-tools.js';
32
+ import { createRefreshAgentsTool } from './foundry-tools/refresh-agents-tool.js';
32
33
 
33
34
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
35
  const packageRoot = path.resolve(__dirname, '../..');
@@ -55,6 +56,7 @@ function buildTools(createTool, pending) {
55
56
  ...createMemoryAdminTools({ tool: createTool }),
56
57
  ...createSnapshotTools({ tool: createTool }),
57
58
  ...createAttestationTools({ tool: createTool }),
59
+ ...createRefreshAgentsTool({ tool: createTool }),
58
60
  };
59
61
  }
60
62