@really-knows-ai/foundry 3.3.8 → 3.4.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/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +2 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +8 -245
- package/dist/CHANGELOG.md +57 -0
- package/dist/scripts/appraise-module.js +452 -0
- package/dist/scripts/lib/artefacts.js +12 -0
- package/dist/scripts/lib/validation.js +230 -0
- package/dist/scripts/orchestrate-cycle.js +65 -0
- package/dist/scripts/orchestrate-phases.js +9 -2
- package/dist/scripts/orchestrate.js +167 -43
- package/dist/scripts/quench-module.js +153 -0
- package/dist/scripts/sort.js +24 -7
- package/dist/skills/orchestrate/SKILL.md +29 -1
- package/package.json +5 -5
|
@@ -110,6 +110,7 @@ export function createOrchestrateTool({ tool, pending }) {
|
|
|
110
110
|
error: tool.schema.string().optional(),
|
|
111
111
|
}).optional(),
|
|
112
112
|
cycleDef: tool.schema.string().optional().describe('Test-mode cycle definition override (path to cycle file)'),
|
|
113
|
+
defaultModel: tool.schema.string().optional().describe('Fallback model for stages with no explicit model in the cycle definition (e.g. "opencode-go/deepseek-v4-flash")'),
|
|
113
114
|
},
|
|
114
115
|
|
|
115
116
|
async execute(args, context) {
|
|
@@ -145,6 +146,7 @@ export function createOrchestrateTool({ tool, pending }) {
|
|
|
145
146
|
cwd, cycleDef: args.cycleDef, git, mint, finalize,
|
|
146
147
|
now: () => Date.now(),
|
|
147
148
|
lastResult: args.lastResult ?? null,
|
|
149
|
+
defaultModel: args.defaultModel,
|
|
148
150
|
}, io);
|
|
149
151
|
|
|
150
152
|
await injectDispatchPromptExtras(result, cwd);
|
|
@@ -1,109 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readdir } from 'fs/promises';
|
|
3
|
-
import { join, relative, sep } from 'path';
|
|
4
|
-
import { minimatch } from 'minimatch';
|
|
5
|
-
import { getLawsForQuench, getArtefactType } from '../../../scripts/lib/config.js';
|
|
6
|
-
import { parseValidatorJsonl } from '../../../scripts/lib/validator-jsonl.js';
|
|
1
|
+
import { join } from 'path';
|
|
7
2
|
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
8
3
|
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
4
|
+
import { performValidation } from '../../../scripts/lib/validation.js';
|
|
9
5
|
|
|
10
6
|
const gateNotFailed = notFailedGuard(makeIO);
|
|
11
7
|
|
|
12
|
-
/**
|
|
13
|
-
* Shell-quote a string for POSIX `/bin/sh` so it is treated as a single literal
|
|
14
|
-
* argument. Wraps the value in single quotes and escapes any embedded single
|
|
15
|
-
* quotes via the `'\''` idiom. Safe for arbitrary file paths including ones
|
|
16
|
-
* containing spaces, semicolons, `$()`, backticks, quotes, and newlines.
|
|
17
|
-
*/
|
|
18
|
-
function shellQuote(value) {
|
|
19
|
-
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Execute a validator command and parse its output.
|
|
24
|
-
*/
|
|
25
|
-
async function executeValidator(expanded, worktree, patterns) {
|
|
26
|
-
try {
|
|
27
|
-
const output = execSync(expanded, {
|
|
28
|
-
cwd: worktree,
|
|
29
|
-
encoding: 'utf8',
|
|
30
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
-
});
|
|
32
|
-
const { Readable } = await import('stream');
|
|
33
|
-
const stream = Readable.from([output]);
|
|
34
|
-
return await parseValidatorJsonl(stream, patterns);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
// Validator command failed - prefer stdout for JSONL (tools like rg exit 1 with results on stdout)
|
|
37
|
-
const output = (err.stdout || err.stderr || err.message || '').trim();
|
|
38
|
-
const { Readable } = await import('stream');
|
|
39
|
-
const stream = Readable.from([output]);
|
|
40
|
-
return await parseValidatorJsonl(stream, patterns);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Run all validators for laws and collect results.
|
|
46
|
-
*
|
|
47
|
-
* Aggregates per-validator parse results into a single structured payload:
|
|
48
|
-
* - `items`: each successfully parsed feedback item, annotated with the
|
|
49
|
-
* `lawId` and `validatorId` it came from. The quench skill consumes this
|
|
50
|
-
* to call `foundry_feedback_add` with tag `law:<lawId>:<validatorId>`.
|
|
51
|
-
* - `errors`: validator-level errors split by type. `parse` for malformed
|
|
52
|
-
* JSON or missing required fields, `pattern-mismatch` for files that
|
|
53
|
-
* didn't match the artefact type's `file-patterns`.
|
|
54
|
-
*/
|
|
55
|
-
async function runValidators(laws, patterns, substitutions, worktree) {
|
|
56
|
-
const results = {
|
|
57
|
-
validatorsRun: 0,
|
|
58
|
-
items: [],
|
|
59
|
-
errors: [],
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
for (const law of laws) {
|
|
63
|
-
if (!law.validators || law.validators.length === 0) continue;
|
|
64
|
-
await runLawValidators(law, patterns, substitutions, worktree, results);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return results;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Run validators for a single law.
|
|
72
|
-
*/
|
|
73
|
-
async function runLawValidators(law, patterns, substitutions, worktree, results) {
|
|
74
|
-
for (const validator of law.validators) {
|
|
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
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
results.validatorsRun++;
|
|
81
|
-
const expanded = expandValidatorCommand(validator.command, substitutions);
|
|
82
|
-
const parseResult = await executeValidator(expanded, worktree, patterns);
|
|
83
|
-
collectValidatorResult(parseResult, law.id, validator.id, results);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Fold a single validator's parse result into the aggregate results.
|
|
89
|
-
*
|
|
90
|
-
* Items always flow through annotated with their `lawId` and `validatorId`,
|
|
91
|
-
* so the caller can construct `law:<lawId>:<validatorId>` feedback tags.
|
|
92
|
-
* Errors are surfaced with their type so the caller can distinguish parse
|
|
93
|
-
* failures from file-pattern mismatches.
|
|
94
|
-
*/
|
|
95
|
-
function collectValidatorResult(parseResult, lawId, validatorId, results) {
|
|
96
|
-
for (const item of parseResult.items) {
|
|
97
|
-
results.items.push({ lawId, validatorId, ...item });
|
|
98
|
-
}
|
|
99
|
-
for (const message of parseResult.parseErrors) {
|
|
100
|
-
results.errors.push({ lawId, validatorId, type: 'parse', message });
|
|
101
|
-
}
|
|
102
|
-
for (const message of parseResult.patternErrors) {
|
|
103
|
-
results.errors.push({ lawId, validatorId, type: 'pattern-mismatch', message });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
8
|
export function createValidateTools({ tool }) {
|
|
108
9
|
return {
|
|
109
10
|
foundry_validate_run: tool({
|
|
@@ -120,152 +21,14 @@ export function createValidateTools({ tool }) {
|
|
|
120
21
|
|
|
121
22
|
async function executeValidateRun(args, context) {
|
|
122
23
|
try {
|
|
123
|
-
|
|
24
|
+
const io = makeIO(context.worktree);
|
|
25
|
+
const foundryDir = join(context.worktree, 'foundry');
|
|
26
|
+
const result = await performValidation({ typeId: args.typeId, io, foundryDir });
|
|
27
|
+
return JSON.stringify(result);
|
|
124
28
|
} catch (err) {
|
|
125
29
|
return JSON.stringify({ ok: false, error: `foundry_validate_run: ${err.message}` });
|
|
126
30
|
}
|
|
127
31
|
}
|
|
128
32
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
*/
|
|
132
|
-
async function getValidationPatterns(foundryDir, typeId, io) {
|
|
133
|
-
const artType = await getArtefactType(foundryDir, typeId, io);
|
|
134
|
-
return artType.frontmatter['file-patterns'] || [];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function performValidation(args, context) {
|
|
138
|
-
const io = makeIO(context.worktree);
|
|
139
|
-
const foundryDir = join(context.worktree, 'foundry');
|
|
140
|
-
|
|
141
|
-
let patterns;
|
|
142
|
-
try {
|
|
143
|
-
patterns = await getValidationPatterns(foundryDir, args.typeId, io);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
return JSON.stringify({ ok: false, error: err.message });
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const validationErr = validatePatterns(patterns, args.typeId);
|
|
149
|
-
if (validationErr) return JSON.stringify(validationErr);
|
|
150
|
-
|
|
151
|
-
const laws = await getLawsForQuench(foundryDir, io, { typeId: args.typeId });
|
|
152
|
-
if (!laws?.length) {
|
|
153
|
-
return JSON.stringify({ ok: true, validatorsRun: 0, items: [], errors: [] });
|
|
154
|
-
}
|
|
155
|
-
return runValidatorsAndReport(laws, patterns, context.worktree);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Run validators and report results.
|
|
160
|
-
*/
|
|
161
|
-
async function runValidatorsAndReport(laws, patterns, worktree) {
|
|
162
|
-
const expandedFiles = await expandPatterns(patterns, 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);
|
|
168
|
-
|
|
169
|
-
return JSON.stringify({
|
|
170
|
-
ok: results.errors.length === 0,
|
|
171
|
-
validatorsRun: results.validatorsRun,
|
|
172
|
-
items: results.items,
|
|
173
|
-
errors: results.errors,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Validate file patterns.
|
|
179
|
-
*/
|
|
180
|
-
function validatePatterns(patterns, typeId) {
|
|
181
|
-
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
182
|
-
return { ok: false, error: `Artefact type ${typeId} has no file-patterns` };
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const SKIP_DIRS = new Set(['node_modules', '.git']);
|
|
188
|
-
|
|
189
|
-
function toPosix(p) {
|
|
190
|
-
return sep === '/' ? p : p.split(sep).join('/');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function readdirSafe(dir) {
|
|
194
|
-
try {
|
|
195
|
-
return await readdir(dir, { withFileTypes: true });
|
|
196
|
-
} catch {
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Recursively walk `dir` and yield POSIX-style paths relative to `root`.
|
|
203
|
-
* Skips `node_modules` and `.git` for speed; the artefacts we validate live
|
|
204
|
-
* elsewhere.
|
|
205
|
-
*/
|
|
206
|
-
async function* walkFiles(root, dir) {
|
|
207
|
-
for (const entry of await readdirSafe(dir)) {
|
|
208
|
-
const full = join(dir, entry.name);
|
|
209
|
-
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
210
|
-
yield* walkFiles(root, full);
|
|
211
|
-
} else if (entry.isFile()) {
|
|
212
|
-
yield toPosix(relative(root, full));
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function fileMatchesAnyPattern(rel, patterns) {
|
|
218
|
-
for (const pattern of patterns) {
|
|
219
|
-
if (minimatch(rel, pattern)) return true;
|
|
220
|
-
}
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Expand glob patterns to actual files in the worktree.
|
|
226
|
-
*
|
|
227
|
-
* Implemented over `readdir` + `minimatch` so we work on Node 20, which lacks
|
|
228
|
-
* `fs/promises.glob` (added in Node 22).
|
|
229
|
-
*/
|
|
230
|
-
async function expandPatterns(patterns, worktree) {
|
|
231
|
-
const files = new Set();
|
|
232
|
-
for await (const rel of walkFiles(worktree, worktree)) {
|
|
233
|
-
if (fileMatchesAnyPattern(rel, patterns)) {
|
|
234
|
-
files.add(rel);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return Array.from(files).sort();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
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'").
|
|
247
|
-
*
|
|
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.
|
|
252
|
-
*
|
|
253
|
-
* @param {string} command
|
|
254
|
-
* @param {{ pattern: string, files: string }} substitutions
|
|
255
|
-
* @returns {string}
|
|
256
|
-
*/
|
|
257
|
-
export function expandValidatorCommand(command, { pattern, files }) {
|
|
258
|
-
let cmd = command
|
|
259
|
-
.replace(/"\{pattern\}"/g, '{pattern}')
|
|
260
|
-
.replace(/'\{pattern\}'/g, '{pattern}')
|
|
261
|
-
.replace(/"\{files\}"/g, '{files}')
|
|
262
|
-
.replace(/'\{files\}'/g, '{files}');
|
|
263
|
-
|
|
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;
|
|
271
|
-
}
|
|
33
|
+
// Re-export for existing tests (validator-command-expansion.test.js)
|
|
34
|
+
export { expandValidatorCommand } from '../../../scripts/lib/validation.js';
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.4.0] - 2026-05-20
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Quench runs as an internal orchestrator module.** Quench no longer
|
|
8
|
+
dispatches an LLM subagent — it runs validators directly within the
|
|
9
|
+
orchestrator, posting feedback and resolving prior items without any
|
|
10
|
+
model involvement. A single `foundry_orchestrate` call handles the
|
|
11
|
+
full quench stage inline.
|
|
12
|
+
- **Appraise dispatch moves into the orchestrator.** Instead of
|
|
13
|
+
dispatching a single appraise subagent that can't use `task`, the
|
|
14
|
+
orchestrator gathers context internally, returns a `dispatch_multi`
|
|
15
|
+
action with pre-built prompts, and the LLM dispatches individual
|
|
16
|
+
appraiser subagents in parallel. After dispatch, the orchestrator
|
|
17
|
+
consolidates results internally — unioning, de-duplicating, and
|
|
18
|
+
posting feedback — all within the `foundry_orchestrate` loop.
|
|
19
|
+
- **New `dispatch_multi` action and `lastResults` input.** The
|
|
20
|
+
`foundry_orchestrate` tool now returns `action: "dispatch_multi"`
|
|
21
|
+
with `tasks: [{subagent_type, prompt}, ...]` and accepts
|
|
22
|
+
`lastResults: [{ok, output?, error?}, ...]` for consolidating
|
|
23
|
+
multi-dispatch results. Mutual exclusivity with `lastResult` is
|
|
24
|
+
enforced.
|
|
25
|
+
- **Stage model fallback.** When a cycle's `models:` map omits a stage,
|
|
26
|
+
the orchestrator falls back to the caller's `defaultModel`, then
|
|
27
|
+
`models.default`, then any available model in the map.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `src/scripts/quench-module.js` — `runQuench(ctx)` runs validators
|
|
32
|
+
deterministically with no LLM.
|
|
33
|
+
- `src/scripts/appraise-module.js` — `gatherAppraiseContext(ctx)` and
|
|
34
|
+
`consolidateAppraise(ctx, lastResults)` for multi-appraiser dispatch.
|
|
35
|
+
- `src/scripts/lib/validation.js` — extracted `performValidation` and
|
|
36
|
+
related functions from `validate-tools.js`.
|
|
37
|
+
- `getArtefactsForCycle()` on `src/scripts/lib/artefacts.js`.
|
|
38
|
+
- `DISPATCH_MULTI_ACTION`, `validateDispatchMulti`, and
|
|
39
|
+
`buildDispatchMultiResponse` on `orchestrate-cycle.js`.
|
|
40
|
+
- `guardLastResults()` and `dispatchByRoute()` in `orchestrate.js`.
|
|
41
|
+
- Defensive guards in `handleSortResult` for quench and appraise routes.
|
|
42
|
+
- Integration tests: `tests/orchestrate-quench.integration.test.js`,
|
|
43
|
+
`tests/orchestrate-appraise.integration.test.js`,
|
|
44
|
+
`tests/orchestrate-contract.test.js`.
|
|
45
|
+
- Unit tests: `tests/quench-module.test.js`, `tests/appraise-module.test.js`.
|
|
46
|
+
|
|
47
|
+
## [3.3.9] - 2026-05-19
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **Stage model fallback.** When a cycle's `models:` map omits a stage (e.g.,
|
|
52
|
+
`quench`), the orchestrator now falls back to the caller's `defaultModel`,
|
|
53
|
+
then `models.default`, then any available model in the map, instead of
|
|
54
|
+
failing with a hard violation. `foundry_orchestrate` accepts an optional
|
|
55
|
+
`defaultModel` arg for this purpose.
|
|
56
|
+
- **No-models-map fallback.** When the cycle has no `models:` block at all but
|
|
57
|
+
`defaultModel` is provided, the orchestrator uses it — previously returned
|
|
58
|
+
a violation.
|
|
59
|
+
|
|
3
60
|
## [3.3.8] - 2026-05-19
|
|
4
61
|
|
|
5
62
|
### Fixed
|