@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.
- package/README.md +8 -7
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +53 -69
- package/dist/.opencode/plugins/foundry-tools/helpers.js +10 -19
- package/dist/.opencode/plugins/foundry-tools/refresh-agents-tool.js +88 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +37 -29
- package/dist/.opencode/plugins/foundry.js +2 -0
- package/dist/CHANGELOG.md +182 -0
- package/dist/README.md +8 -7
- package/dist/agents/foundry.md +37 -0
- package/dist/docs/architecture.md +6 -3
- package/dist/docs/concepts.md +1 -1
- package/dist/docs/getting-started.md +57 -135
- package/dist/docs/tools.md +21 -1
- package/dist/scripts/sort.js +1 -1
- package/dist/skills/add-appraiser/SKILL.md +19 -34
- package/dist/skills/add-artefact-type/SKILL.md +47 -43
- package/dist/skills/add-cycle/SKILL.md +28 -37
- package/dist/skills/add-extractor/SKILL.md +21 -33
- package/dist/skills/add-flow/SKILL.md +43 -88
- package/dist/skills/add-law/SKILL.md +132 -26
- package/dist/skills/add-memory-edge-type/SKILL.md +11 -17
- package/dist/skills/add-memory-entity-type/SKILL.md +9 -16
- package/dist/skills/change-embedding-model/SKILL.md +6 -8
- package/dist/skills/drop-memory-edge-type/SKILL.md +6 -8
- package/dist/skills/drop-memory-entity-type/SKILL.md +6 -8
- package/dist/skills/dry-run/SKILL.md +11 -28
- package/dist/skills/flow/SKILL.md +1 -1
- package/dist/skills/init-foundry/SKILL.md +47 -27
- package/dist/skills/init-memory/SKILL.md +11 -22
- package/dist/skills/list-agents/SKILL.md +1 -1
- package/dist/skills/refresh-agents/SKILL.md +4 -26
- package/dist/skills/rename-memory-edge-type/SKILL.md +6 -8
- package/dist/skills/rename-memory-entity-type/SKILL.md +6 -8
- package/dist/skills/reset-memory/SKILL.md +10 -16
- package/dist/skills/upgrade-foundry/SKILL.md +1 -1
- 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
|
|
146
|
-
per model available in your session,
|
|
147
|
-
|
|
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
|
|
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 —
|
|
153
|
+
### Phase 3 — Ask the Foundry agent for a flow
|
|
153
154
|
|
|
154
|
-
|
|
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
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
256
|
-
|
|
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,
|
|
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 —
|
|
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
|
-
|
|
125
|
-
by name (e.g. "Creative Flow"), or by clear paraphrase (e.g. "the creative flow",
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
new skills). It does NOT apply to running an existing, defined flow.
|
|
129
|
+
## Foundry agent capabilities
|
|
132
130
|
|
|
133
|
-
|
|
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-*\`
|
|
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,
|
|
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,
|
|
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,
|
|
73
|
+
async function runLawValidators(law, patterns, substitutions, worktree, results) {
|
|
74
74
|
for (const validator of law.validators) {
|
|
75
|
-
// Skip
|
|
76
|
-
//
|
|
77
|
-
if (
|
|
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,
|
|
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
|
|
164
|
-
|
|
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}
|
|
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
|
-
*
|
|
241
|
-
* whitespace or
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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
|
|
247
|
-
* @param {
|
|
248
|
-
* @returns {string}
|
|
253
|
+
* @param {string} command
|
|
254
|
+
* @param {{ pattern: string, files: string }} substitutions
|
|
255
|
+
* @returns {string}
|
|
249
256
|
*/
|
|
250
|
-
export function expandValidatorCommand(command,
|
|
251
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|