@really-knows-ai/foundry 3.3.9 → 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.
@@ -1,109 +1,10 @@
1
- import { execSync } from 'child_process';
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
- return await performValidation(args, context);
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
- * Perform actual validation work.
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,49 @@
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
+
3
47
  ## [3.3.9] - 2026-05-19
4
48
 
5
49
  ### Fixed