@really-knows-ai/foundry 3.3.9 → 3.5.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.
@@ -1,58 +1,47 @@
1
1
  import path from 'path';
2
- import { readFileSync, writeFileSync, existsSync } from 'fs';
3
- import { requireNoActiveStage } from '../../../scripts/lib/stage-guard.js';
4
- import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
5
- import { parseArtefactsTable, setArtefactStatus } from '../../../scripts/lib/artefacts.js';
6
- import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { getArtefactFiles } from '../../../scripts/lib/artefacts.js';
4
+ import { getCycleDefinition } from '../../../scripts/lib/config.js';
5
+ import { parseFrontmatter } from '../../../scripts/lib/workfile.js';
6
+ import { makeIO } from './helpers.js';
7
7
 
8
- const gateNotFailed = notFailedGuard(makeIO);
8
+ function readWorkCycleId(worktree) {
9
+ const workPath = path.join(worktree, 'WORK.md');
10
+ if (!existsSync(workPath)) throw new Error('WORK.md not found');
11
+ const frontmatter = parseFrontmatter(readFileSync(workPath, 'utf-8'));
12
+ if (!frontmatter.cycle) throw new Error('current cycle not found in WORK.md frontmatter');
13
+ return frontmatter.cycle;
14
+ }
15
+
16
+ async function readOutputType(foundryDir, cycleId, io) {
17
+ let cd;
18
+ try {
19
+ cd = await getCycleDefinition(foundryDir, cycleId, io);
20
+ } catch { return null; }
21
+ return cd.frontmatter && cd.frontmatter['output-type'];
22
+ }
9
23
 
10
24
  function makeListTool(tool) {
11
25
  return tool({
12
- 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.',
13
- args: {
14
- cycle: tool.schema.string().optional().describe('Only return rows whose Cycle column matches this value'),
15
- },
16
- async execute(args, context) {
17
- const workPath = path.join(context.worktree, 'WORK.md');
18
- if (!existsSync(workPath)) {
19
- return JSON.stringify({ error: 'WORK.md not found' });
26
+ description: 'List artefact changes on the current work branch for the current cycle. Returns [{ file, state }] entries.',
27
+ args: {},
28
+ async execute(_args, context) {
29
+ try {
30
+ const cycleId = readWorkCycleId(context.worktree);
31
+ const io = makeIO(context.worktree);
32
+ const outputType = await readOutputType('foundry', cycleId, io);
33
+ if (!outputType) return JSON.stringify([]);
34
+ const artefacts = await getArtefactFiles('foundry', outputType, io, { baseBranch: 'main' });
35
+ return JSON.stringify(artefacts);
36
+ } catch (err) {
37
+ return JSON.stringify({ error: err.message });
20
38
  }
21
- const text = readFileSync(workPath, 'utf-8');
22
- const rows = parseArtefactsTable(text);
23
- const filtered = args.cycle ? rows.filter(r => r.cycle === args.cycle) : rows;
24
- return JSON.stringify(filtered);
25
39
  },
26
40
  });
27
41
  }
28
42
 
29
43
  export function createArtefactTools({ tool }) {
30
44
  return {
31
- // NOTE: `foundry_artefacts_add` was removed in v2.2.0. Artefacts are now
32
- // registered automatically by the orchestrator's internal finalize step as drafts,
33
- // then promoted to done|blocked via `foundry_artefacts_set_status`.
34
- foundry_artefacts_set_status: tool({
35
- description: 'Update the status of an artefact in WORK.md (done|blocked only)',
36
- args: {
37
- file: tool.schema.string().describe('Artefact file path'),
38
- status: tool.schema.string().describe('New status (done|blocked)'),
39
- },
40
- execute: guarded('foundry_artefacts_set_status', [flowBranchGuard, gateNotFailed], async (args, context) => {
41
- const io = makeIO(context.worktree);
42
- const guard = requireNoActiveStage(io);
43
- if (!guard.ok) return JSON.stringify({ error: `foundry_artefacts_set_status ${guard.error}` });
44
- const workPath = path.join(context.worktree, 'WORK.md');
45
- const text = readFileSync(workPath, 'utf-8');
46
- try {
47
- const updated = setArtefactStatus(text, args.file, args.status);
48
- writeFileSync(workPath, updated, 'utf-8');
49
- return JSON.stringify({ ok: true });
50
- } catch (e) {
51
- return JSON.stringify({ error: e.message });
52
- }
53
- }, { branchIo: branchIoFactory, io: asyncIoFactory }),
54
- }),
55
-
56
45
  foundry_artefacts_list: makeListTool(tool),
57
46
  };
58
47
  }
@@ -131,11 +131,18 @@ function commitAttestation(cwd, cycle, content, opts) {
131
131
  function buildAttestationInputs(opts) {
132
132
  return {
133
133
  cwd: opts.cwd,
134
+ foundryDir: 'foundry',
134
135
  baseBranch: opts.baseBranch,
136
+ branchBaseSha: opts.branchBaseSha,
135
137
  goalText: opts.goalText,
136
138
  archiveBranch: opts.archiveBranch,
137
139
  archiveTipSha: opts.archiveTipSha,
138
- io: { readFile: (p) => readFileSync(p, 'utf8'), fileExists: (p) => existsSync(p) },
140
+ io: {
141
+ readFile: (p) => readFileSync(p, 'utf8'),
142
+ fileExists: (p) => existsSync(p),
143
+ exists: (p) => existsSync(p),
144
+ exec: (args) => execFileSync(args[0], args.slice(1), { cwd: opts.cwd, encoding: 'utf8' }),
145
+ },
139
146
  execGit: opts.execGit,
140
147
  };
141
148
  }
@@ -151,7 +158,7 @@ function createAttestTool(tool) {
151
158
  return tool({
152
159
  description:
153
160
  'Verify the current work cycle is complete (all required stages ran, no unresolved ' +
154
- 'feedback, no blocked artefacts) and commit an ATTEST.md attestation file to the work branch. ' +
161
+ 'feedback) and commit an ATTEST.md attestation file to the work branch. ' +
155
162
  'foundry_git_finish will not merge without this commit at HEAD.',
156
163
  args: {
157
164
  baseBranch: tool.schema.string().optional()
@@ -1,11 +1,8 @@
1
- import path from 'path';
2
1
  import { execFileSync } from 'child_process';
3
2
  import { randomUUID } from 'node:crypto';
4
- import { readFileSync, writeFileSync } from 'fs';
5
3
  import { signToken } from '../../../scripts/lib/token.js';
6
4
  import { readOrCreateSecret } from '../../../scripts/lib/secret.js';
7
5
  import { getCycleDefinition, getArtefactType } from '../../../scripts/lib/config.js';
8
- import { addArtefactRow } from '../../../scripts/lib/artefacts.js';
9
6
  import { stageBaseOf } from '../../../scripts/lib/stage-guard.js';
10
7
  import { finalizeStage } from '../../../scripts/lib/finalize.js';
11
8
  import { commitWithPolicy } from '../../../scripts/lib/git-bridge.js';
@@ -40,15 +37,6 @@ function createGitBridge(cwd) {
40
37
  };
41
38
  }
42
39
 
43
- function makeRegisterArtefact(cwd, cycleId) {
44
- const workPath = path.join(cwd, 'WORK.md');
45
- return ({ file, type, status }) => {
46
- const text = readFileSync(workPath, 'utf-8');
47
- const updated = addArtefactRow(text, { file, type, cycle: cycleId, status });
48
- writeFileSync(workPath, updated, 'utf-8');
49
- };
50
- }
51
-
52
40
  async function createFinalize(cwd, io) {
53
41
  return async ({ cycleId, stage, baseSha }) => {
54
42
  let cycleDoc;
@@ -78,7 +66,6 @@ async function createFinalize(cwd, io) {
78
66
  cycleDef,
79
67
  artefactTypes,
80
68
  io,
81
- registerArtefact: makeRegisterArtefact(cwd, cycleId),
82
69
  });
83
70
  return result;
84
71
  };
@@ -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,72 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.5.1] - 2026-05-22
4
+
5
+ ### Fixed
6
+
7
+ - Run CI against the active Node release lines: 22.x, 24.x, 25.x, and 26.x.
8
+ - Use Node 22-compatible `mock.module({ namedExports })` test mocks.
9
+
10
+ ## [3.5.0] - 2026-05-22
11
+
12
+ ### Changed
13
+
14
+ - Add branch-based artefact discovery with `getArtefactFiles` and git change-state tracking.
15
+ - Remove the `WORK.md` artefact table, artefact registration side effects, and per-artefact status updates.
16
+ - Update quench, appraise, orchestration, plugin tools, and attestation to use branch artefact discovery.
17
+ - Remove `foundry_artefacts_set_status` and update `foundry_artefacts_list` to return `{file, state}` entries.
18
+ - Remove artefact table generation from new `WORK.md` files.
19
+ - Update tests and docs for branch artefact discovery.
20
+
21
+ ### Fixed
22
+
23
+ - Include all non-deleted artefact files in missing-model violation payloads.
24
+ - Thread base-branch selection through artefact discovery contexts.
25
+
26
+ ## [3.4.0] - 2026-05-20
27
+
28
+ ### Changed
29
+
30
+ - **Quench runs as an internal orchestrator module.** Quench no longer
31
+ dispatches an LLM subagent — it runs validators directly within the
32
+ orchestrator, posting feedback and resolving prior items without any
33
+ model involvement. A single `foundry_orchestrate` call handles the
34
+ full quench stage inline.
35
+ - **Appraise dispatch moves into the orchestrator.** Instead of
36
+ dispatching a single appraise subagent that can't use `task`, the
37
+ orchestrator gathers context internally, returns a `dispatch_multi`
38
+ action with pre-built prompts, and the LLM dispatches individual
39
+ appraiser subagents in parallel. After dispatch, the orchestrator
40
+ consolidates results internally — unioning, de-duplicating, and
41
+ posting feedback — all within the `foundry_orchestrate` loop.
42
+ - **New `dispatch_multi` action and `lastResults` input.** The
43
+ `foundry_orchestrate` tool now returns `action: "dispatch_multi"`
44
+ with `tasks: [{subagent_type, prompt}, ...]` and accepts
45
+ `lastResults: [{ok, output?, error?}, ...]` for consolidating
46
+ multi-dispatch results. Mutual exclusivity with `lastResult` is
47
+ enforced.
48
+ - **Stage model fallback.** When a cycle's `models:` map omits a stage,
49
+ the orchestrator falls back to the caller's `defaultModel`, then
50
+ `models.default`, then any available model in the map.
51
+
52
+ ### Added
53
+
54
+ - `src/scripts/quench-module.js` — `runQuench(ctx)` runs validators
55
+ deterministically with no LLM.
56
+ - `src/scripts/appraise-module.js` — `gatherAppraiseContext(ctx)` and
57
+ `consolidateAppraise(ctx, lastResults)` for multi-appraiser dispatch.
58
+ - `src/scripts/lib/validation.js` — extracted `performValidation` and
59
+ related functions from `validate-tools.js`.
60
+ - `getArtefactsForCycle()` on `src/scripts/lib/artefacts.js`.
61
+ - `DISPATCH_MULTI_ACTION`, `validateDispatchMulti`, and
62
+ `buildDispatchMultiResponse` on `orchestrate-cycle.js`.
63
+ - `guardLastResults()` and `dispatchByRoute()` in `orchestrate.js`.
64
+ - Defensive guards in `handleSortResult` for quench and appraise routes.
65
+ - Integration tests: `tests/orchestrate-quench.integration.test.js`,
66
+ `tests/orchestrate-appraise.integration.test.js`,
67
+ `tests/orchestrate-contract.test.js`.
68
+ - Unit tests: `tests/quench-module.test.js`, `tests/appraise-module.test.js`.
69
+
3
70
  ## [3.3.9] - 2026-05-19
4
71
 
5
72
  ### Fixed
@@ -238,7 +238,7 @@ Input artefacts (files matching an input type's `file-patterns`) are read-only.
238
238
  When an unrecoverable error occurs (e.g. assay extractor abort, invalid JSONL, or memory-sync failure), the orchestrator marks `WORK.md` frontmatter with `status: failed` and a `reason`. The flow is then locked:
239
239
 
240
240
  - **Blocked tools.** All mutation tools refuse to run and return an error referencing the failure reason:
241
- - **Lifecycle:** `foundry_stage_begin`, `foundry_orchestrate`, `foundry_workfile_create`, `foundry_artefacts_set_status`
241
+ - **Lifecycle:** `foundry_stage_begin`, `foundry_orchestrate`, `foundry_workfile_create`
242
242
  - **Stage work:** `foundry_assay_run`, `foundry_validate_run`
243
243
  - **Feedback writes:** `foundry_feedback_add`, `foundry_feedback_action`, `foundry_feedback_wontfix`, `foundry_feedback_resolve` (`foundry_feedback_list` remains callable)
244
244
  - **Appraiser selection:** `foundry_appraisers_select`
@@ -59,7 +59,6 @@ state machine, see [`docs/concepts.md`](./concepts.md) and
59
59
 
60
60
  **Artefacts**
61
61
  - [`foundry_artefacts_list`](#foundry_artefacts_list)
62
- - [`foundry_artefacts_set_status`](#foundry_artefacts_set_status)
63
62
 
64
63
  **Feedback**
65
64
  - [`foundry_feedback_add`](#foundry_feedback_add)
@@ -326,41 +325,19 @@ flow** (escape hatch).
326
325
 
327
326
  ### `foundry_artefacts_list`
328
327
 
329
- > List artefacts from the WORK.md table. Optional `cycle` filter
330
- > callers should always pass the current cycle to avoid stale rows.
328
+ > List artefact changes on the current work branch for the current cycle.
331
329
 
332
330
  **Args:**
333
- - `cycle` (string, optional).
331
+ - none.
334
332
 
335
- **Returns:** array of `{file, type, cycle, status, ...}` rows. `{error:
336
- "WORK.md not found"}` if missing.
333
+ **Returns:** array of `{file, state}` entries. `{error: "WORK.md not found"}`
334
+ if `WORK.md` is missing, or `{error: "current cycle not found in WORK.md frontmatter"}`
335
+ if `WORK.md` has no current cycle.
337
336
 
338
337
  **Stage requirements:** none.
339
338
 
340
339
  **Side effects:** none.
341
340
 
342
- ### `foundry_artefacts_set_status`
343
-
344
- > Update the status of an artefact in `WORK.md` (`done` | `blocked`
345
- > only).
346
-
347
- **Args:**
348
- - `file` (string, required): Artefact file path.
349
- - `status` (string, required): New status (`done` | `blocked`).
350
-
351
- **Returns:** `{ ok: true }`. On invalid input: `{ error: <message> }`.
352
-
353
- **Stage requirements:** requires no active stage. Refuses on failed
354
- flow.
355
-
356
- **Failure modes:**
357
- - Invalid status, unknown file, malformed table → error from
358
- `setArtefactStatus`.
359
-
360
- **Side effects:** rewrites `WORK.md`.
361
-
362
- ---
363
-
364
341
  ## Feedback
365
342
 
366
343
  All feedback tools (except `foundry_feedback_list`) require an active
@@ -168,11 +168,11 @@ A crash between the two steps leaves the live file untouched.
168
168
  | Frontmatter (`flow`, `cycle`) | `foundry_workfile_create` (flow skill) | updated in place as the flow advances between cycles |
169
169
  | Frontmatter (`stages`, `max-iterations`, `human-appraise`, `deadlock-appraise`, `deadlock-iterations`, `models`) | `foundry_orchestrate` (first call of each cycle, internally) | reset on each new cycle |
170
170
  | Goal | `foundry_workfile_create` (flow skill) | nobody |
171
- | Artefacts | the orchestrator's internal finalize step (after forge closes) | `foundry_artefacts_set_status` (orchestrator `done`/`blocked`) |
171
+ | Artefact files | forge stage writes files on disk | git tracks file changes; cycle-level state records completion or failure |
172
172
  | `WORK.feedback.yaml` | `foundry_feedback_add` (`quench` / `appraise` / `human-appraise`) | `foundry_feedback_action` / `foundry_feedback_wontfix` (forge), `foundry_feedback_resolve` (source stage / human-appraise override); sort writes only deadlocked snapshots |
173
173
  | `WORK.history.yaml` | `foundry_orchestrate` | `foundry_orchestrate` |
174
174
 
175
- Note: `foundry_artefacts_add` no longer exists as a public tool — artefact registration is automatic via the orchestrator's internal finalize step, which scans the git diff and registers files matching the output type's `file-patterns` as `draft`.
175
+ Artefact files are discovered from branch changes matching the cycle output type's `file-patterns`.
176
176
 
177
177
  ## WORK.history.yaml
178
178