@really-knows-ai/foundry 3.4.0 → 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.
@@ -7,7 +7,6 @@
7
7
  import path from 'node:path';
8
8
  import { load as loadYaml } from 'js-yaml';
9
9
  import { parseFrontmatter } from '../workfile.js';
10
- import { parseArtefactsTable } from '../artefacts.js';
11
10
  import { parseAllHistoryEntries } from '../history.js';
12
11
  import { sha256Buffer } from './hash.js';
13
12
  import { buildAttestationPayload } from './payload.js';
@@ -21,19 +20,11 @@ function readWorkFiles(cwd, io) {
21
20
 
22
21
  return {
23
22
  workText: io.readFile(workPath),
24
- historyText: io.fileExists(historyPath) ? io.readFile(historyPath) : '',
25
- feedbackText: io.fileExists(feedbackPath) ? io.readFile(feedbackPath) : '',
23
+ historyText: io.exists(historyPath) ? io.readFile(historyPath) : '',
24
+ feedbackText: io.exists(feedbackPath) ? io.readFile(feedbackPath) : '',
26
25
  };
27
26
  }
28
27
 
29
- function checkBlockedArtefacts(artefacts) {
30
- const blocked = artefacts.filter(a => a.status === 'blocked');
31
- if (blocked.length > 0) {
32
- return `foundry_attest: cycle has blocked artefact(s): ${blocked.map(a => a.file).join(', ')}`;
33
- }
34
- return null;
35
- }
36
-
37
28
  function checkMissingStages(frontmatter, historyText) {
38
29
  const entries = parseAllHistoryEntries(historyText);
39
30
  const completed = new Set(entries.map(e => e.stage));
@@ -54,10 +45,7 @@ function checkUnresolvedFeedback(feedbackText) {
54
45
  return null;
55
46
  }
56
47
 
57
- function findCycleError(frontmatter, artefacts, historyText, feedbackText) {
58
- const blockedError = checkBlockedArtefacts(artefacts);
59
- if (blockedError) return blockedError;
60
-
48
+ function findCycleError(frontmatter, historyText, feedbackText) {
61
49
  const missingError = checkMissingStages(frontmatter, historyText);
62
50
  if (missingError) return missingError;
63
51
 
@@ -73,7 +61,9 @@ function computeDiffSha(execGit, baseBranch) {
73
61
 
74
62
  export async function buildAttestation({
75
63
  cwd,
64
+ foundryDir,
76
65
  baseBranch,
66
+ branchBaseSha,
77
67
  goalText,
78
68
  archiveBranch,
79
69
  archiveTipSha,
@@ -82,20 +72,22 @@ export async function buildAttestation({
82
72
  }) {
83
73
  const { workText, historyText, feedbackText } = readWorkFiles(cwd, io);
84
74
  const frontmatter = parseFrontmatter(workText);
85
- const artefacts = parseArtefactsTable(workText);
86
75
 
87
- const cycleError = findCycleError(frontmatter, artefacts, historyText, feedbackText);
76
+ const cycleError = findCycleError(frontmatter, historyText, feedbackText);
88
77
  if (cycleError) {
89
78
  return { ok: false, error: cycleError };
90
79
  }
91
80
 
92
81
  const diffSha = computeDiffSha(execGit, baseBranch);
93
82
 
94
- const payload = buildAttestationPayload({
83
+ const payload = await buildAttestationPayload({
95
84
  cwd,
85
+ foundryDir: foundryDir ?? 'foundry',
96
86
  goalText,
97
87
  archiveBranch,
98
88
  archiveTipSha,
89
+ baseBranch,
90
+ branchBaseSha,
99
91
  io,
100
92
  });
101
93
 
@@ -5,26 +5,29 @@
5
5
  */
6
6
 
7
7
  import { readFileSync, existsSync } from 'node:fs';
8
+ import { execFileSync } from 'node:child_process';
8
9
  import path from 'node:path';
9
10
  import { parseFrontmatter } from '../workfile.js';
10
- import { parseArtefactsTable } from '../artefacts.js';
11
+ import { getArtefactFiles } from '../artefacts.js';
12
+ import { getCycleDefinition } from '../config.js';
11
13
  import { parseAllHistoryEntries } from '../history.js';
12
14
  import { sha256Text, sortPaths } from './hash.js';
13
15
 
14
- function defaultIo() {
16
+ function defaultIo(cwd) {
15
17
  return {
16
18
  readFile: (filePath) => readFileSync(filePath, 'utf8'),
17
- fileExists: (filePath) => existsSync(filePath),
19
+ exists: (filePath) => existsSync(filePath),
20
+ exec: (args) => execFileSync(args[0], args.slice(1), { cwd, encoding: 'utf8' }),
18
21
  };
19
22
  }
20
23
 
21
24
  function readWorkFiles(cwd, io) {
22
- const { readFile, fileExists } = io ?? defaultIo();
25
+ const { readFile, exists } = io ?? defaultIo(cwd);
23
26
 
24
27
  return {
25
28
  workText: readFile(path.join(cwd, 'WORK.md')),
26
- historyText: fileExists(path.join(cwd, 'WORK.history.yaml')) ? readFile(path.join(cwd, 'WORK.history.yaml')) : '',
27
- feedbackText: fileExists(path.join(cwd, 'WORK.feedback.yaml')) ? readFile(path.join(cwd, 'WORK.feedback.yaml')) : '',
29
+ historyText: exists(path.join(cwd, 'WORK.history.yaml')) ? readFile(path.join(cwd, 'WORK.history.yaml')) : '',
30
+ feedbackText: exists(path.join(cwd, 'WORK.feedback.yaml')) ? readFile(path.join(cwd, 'WORK.feedback.yaml')) : '',
28
31
  };
29
32
  }
30
33
 
@@ -85,11 +88,34 @@ function buildGovernance(frontmatter, workText, historyText, feedbackText) {
85
88
  };
86
89
  }
87
90
 
88
- export function buildAttestationPayload({ cwd, goalText, archiveBranch, archiveTipSha, io }) {
89
- const { workText, historyText, feedbackText } = readWorkFiles(cwd, io);
91
+ async function discoverCycleOutputs(resolvedFd, cycleId, resolvedIo, options) {
92
+ try {
93
+ const cfm = (await getCycleDefinition(resolvedFd, cycleId, resolvedIo)).frontmatter;
94
+ const outputType = cfm && cfm['output-type'];
95
+ if (outputType) return getArtefactFiles(resolvedFd, outputType, resolvedIo, options);
96
+ } catch {
97
+ // If cycle definition is missing, outputs remain empty
98
+ }
99
+ return [];
100
+ }
101
+
102
+ export async function buildAttestationPayload(
103
+ { cwd, foundryDir: fd, goalText, archiveBranch, archiveTipSha, baseBranch, branchBaseSha, io },
104
+ ) {
105
+ const resolvedIo = io || defaultIo(cwd);
106
+ const resolvedFd = fd || 'foundry';
107
+ const { workText, historyText, feedbackText } = readWorkFiles(cwd, resolvedIo);
90
108
 
91
109
  const frontmatter = parseFrontmatter(workText);
92
- const artefacts = parseArtefactsTable(workText);
110
+
111
+ // Discover artefact outputs from branch changes
112
+ let outputs = [];
113
+ const cycleId = frontmatter.cycle;
114
+ if (cycleId) {
115
+ outputs = await discoverCycleOutputs(resolvedFd, cycleId, resolvedIo, { baseBranch, branchBaseSha });
116
+ }
117
+
118
+ const outputEntries = outputs.map(({ file, state }) => ({ path: file, state }));
93
119
 
94
120
  const sortedEntries = parseAndSortHistoryEntries(historyText);
95
121
  const stages = buildStagesFromEntries(sortedEntries);
@@ -97,7 +123,7 @@ export function buildAttestationPayload({ cwd, goalText, archiveBranch, archiveT
97
123
  return {
98
124
  contract: buildContract(frontmatter),
99
125
  governance: buildGovernance(frontmatter, workText, historyText, feedbackText),
100
- outputs: artefacts.map(row => ({ path: row.file, status: row.status })),
126
+ outputs: outputEntries,
101
127
  process: { stages },
102
128
  request: { goal_text: goalText },
103
129
  schema: 'foundry-attestation/v1',
@@ -49,7 +49,7 @@ function classifyFiles(files, allowedPatterns) {
49
49
  return { matched, unexpected };
50
50
  }
51
51
 
52
- export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes, registerArtefact, io }) {
52
+ export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes, io }) {
53
53
  if (!io?.exec) {
54
54
  throw new Error('finalizeStage: io.exec is required');
55
55
  }
@@ -62,9 +62,9 @@ export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes
62
62
  // For non-forge stages, matched files are tool-managed side effects
63
63
  // (e.g. assay's memory writes) that should not become artefacts.
64
64
  if (stageBase !== 'forge') return { ok: true, artefacts: [], changedFiles: sortedFiles };
65
- const artefacts = sortedFiles.map(file => {
66
- registerArtefact({ file, type: cycleDef.outputArtefactType, status: 'draft' });
67
- return { file, type: cycleDef.outputArtefactType, status: 'draft' };
68
- });
65
+ const artefacts = sortedFiles.map(file => ({
66
+ file,
67
+ type: cycleDef.outputArtefactType,
68
+ }));
69
69
  return { ok: true, artefacts, changedFiles: sortedFiles };
70
70
  }
@@ -76,12 +76,14 @@ export async function getValidationPatterns(foundryDir, typeId, io) {
76
76
  * Run validation commands for an artefact type deterministically.
77
77
  *
78
78
  * Accepts an IO interface and foundryDir directly (no plugin context
79
- * dependency). Returns a plain result object.
79
+ * dependency). When `artefacts` is provided, the `{files}` substitution
80
+ * uses non-deleted files from the artefact list instead of expanding
81
+ * patterns across the worktree.
80
82
  *
81
- * @param {{ typeId: string, io: object, foundryDir: string }} params
83
+ * @param {{ typeId: string, io: object, foundryDir: string, artefacts?: Array<{file: string, state: string}> }} params
82
84
  * @returns {Promise<{ ok: boolean, validatorsRun: number, items: Array, errors: Array }>}
83
85
  */
84
- export async function performValidation({ typeId, io, foundryDir }) {
86
+ export async function performValidation({ typeId, io, foundryDir, artefacts }) {
85
87
  let patterns;
86
88
  try {
87
89
  patterns = await getValidationPatterns(foundryDir, typeId, io);
@@ -96,15 +98,27 @@ export async function performValidation({ typeId, io, foundryDir }) {
96
98
  if (!laws?.length) {
97
99
  return { ok: true, validatorsRun: 0, items: [], errors: [] };
98
100
  }
99
- return runValidatorsAndReport(laws, patterns, foundryDir);
101
+ return runValidatorsAndReport(laws, patterns, foundryDir, artefacts);
100
102
  }
101
103
 
102
104
  /**
103
105
  * Run validators for a set of laws and build the aggregated result.
106
+ *
107
+ * When `artefacts` is provided, the `{files}` substitution uses non-deleted
108
+ * files from the artefact list. Otherwise patterns are expanded across the
109
+ * worktree (backwards compatibility for callers like `foundry_validate_run`).
104
110
  */
105
- export async function runValidatorsAndReport(laws, patterns, foundryDir) {
111
+ export async function runValidatorsAndReport(laws, patterns, foundryDir, artefacts) {
106
112
  const worktree = dirname(foundryDir);
107
- const expandedFiles = await expandPatterns(patterns, worktree);
113
+ let expandedFiles;
114
+ if (artefacts) {
115
+ expandedFiles = artefacts
116
+ .filter(({ state }) => state !== 'deleted')
117
+ .map(({ file }) => file)
118
+ .sort();
119
+ } else {
120
+ expandedFiles = await expandPatterns(patterns, worktree);
121
+ }
108
122
  const substitutions = {
109
123
  pattern: patterns.map(shellQuote).join(' '),
110
124
  files: expandedFiles.map(shellQuote).join(' '),
@@ -127,8 +127,5 @@ export function createWorkfile(frontmatter, goal) {
127
127
  # Goal
128
128
 
129
129
  ${goal}
130
-
131
- | File | Type | Cycle | Status |
132
- |------|------|-------|--------|
133
130
  `;
134
131
  }
@@ -5,21 +5,12 @@ import {
5
5
  getCycleDefinition,
6
6
  getArtefactType,
7
7
  } from './lib/config.js';
8
- import { parseArtefactsTable, setArtefactStatus } from './lib/artefacts.js';
9
8
  import { openFeedbackStore } from './lib/feedback-store.js';
10
9
 
11
10
  // ---------------------------------------------------------------------------
12
11
  // Public helpers (re-exported by orchestrate.js for tests).
13
12
  // ---------------------------------------------------------------------------
14
13
 
15
- export function findCycleOutputArtefact(cycleId, io) {
16
- if (!io.exists('WORK.md')) return null;
17
- const content = io.readFile('WORK.md');
18
- const rows = parseArtefactsTable(content);
19
- const match = rows.find(r => r.cycle === cycleId);
20
- return match ? { file: match.file, type: match.type, status: match.status } : null;
21
- }
22
-
23
14
  export async function readCycleTargets(cycleId, io) {
24
15
  try {
25
16
  const cd = await getCycleDefinition('foundry', cycleId, io);
@@ -47,7 +38,11 @@ export async function readForgeFilePatterns(cycleId, io) {
47
38
  }
48
39
  const output = extractOutputType(cd);
49
40
  if (!output) return null;
50
- return fetchFilePatterns(output, io);
41
+ try {
42
+ return await fetchFilePatterns(output, io);
43
+ } catch {
44
+ return null;
45
+ }
51
46
  }
52
47
 
53
48
  // ---------------------------------------------------------------------------
@@ -148,31 +143,7 @@ export function tryCommit(git, message, allowedPatterns, phase) {
148
143
  }
149
144
  }
150
145
 
151
- function findCycleRow(cycleId, rows) {
152
- return rows.find(r => r.cycle === cycleId);
153
- }
154
-
155
- function writeBlockedStatus(content, row, io) {
156
- try {
157
- io.writeFile('WORK.md', setArtefactStatus(content, row.file, 'blocked'));
158
- return { ok: true };
159
- } catch (e) {
160
- return { ok: false, error: e?.message || String(e) };
161
- }
162
- }
163
-
164
- export function markArtefactBlocked(cycleId, io) {
165
- if (!io.exists('WORK.md')) return { ok: true };
166
- const content = io.readFile('WORK.md');
167
- const rows = parseArtefactsTable(content);
168
- const row = findCycleRow(cycleId, rows);
169
- if (!row) return { ok: true };
170
- return writeBlockedStatus(content, row, io);
171
- }
172
146
 
173
- export function formatBlockNote(blockResult) {
174
- return blockResult.ok ? '' : ` (also: failed to mark artefact blocked: ${blockResult.error})`;
175
- }
176
147
 
177
148
  // ---------------------------------------------------------------------------
178
149
  // Stage synthesis (pure utility, used by setupWorkfile and exported publicly).
@@ -13,38 +13,62 @@ import { stageBaseOf } from './lib/stage-guard.js';
13
13
  import { allowedPatternsForStage } from './lib/git-policy.js';
14
14
  import { loadExtractor } from './lib/assay/loader.js';
15
15
  import { checkExtractorAgainstCycle } from './lib/assay/permissions.js';
16
+ import { getArtefactFiles } from './lib/artefacts.js';
16
17
  import {
17
- findCycleOutputArtefact,
18
18
  readCycleTargets,
19
19
  readForgeFilePatterns,
20
20
  readRecentFeedback,
21
21
  computeOpenFeedback,
22
22
  violation,
23
23
  tryCommit,
24
- markArtefactBlocked,
25
- formatBlockNote,
26
24
  synthesizeStages,
27
25
  renderDispatchPrompt,
28
26
  } from './orchestrate-cycle.js';
29
27
 
30
- async function doneAction(cycleId, io) {
31
- const art = findCycleOutputArtefact(cycleId, io);
32
- return { action: 'done', cycle: cycleId, artefact_file: art?.file ?? null, next_cycles: await readCycleTargets(cycleId, io) };
28
+ async function findOutputArtefacts(cfm, io, foundryDir, baseBranch) {
29
+ const outputType = cfm ? cfm['output-type'] : undefined;
30
+ if (!outputType) return null;
31
+ const artefacts = await getArtefactFiles(foundryDir, outputType, io, { baseBranch });
32
+ return artefacts.find(a => a.state !== 'deleted') || null;
33
33
  }
34
34
 
35
- function blockedAction(cycleId, io, details) {
36
- const art = findCycleOutputArtefact(cycleId, io);
37
- return { action: 'blocked', cycle: cycleId, artefact_file: art?.file ?? null, reason: details ?? 'iteration limit reached with unresolved feedback' };
35
+ async function doneAction(cycleId, io, foundryDir, baseBranch) {
36
+ const fd = foundryDir || 'foundry';
37
+ const base = baseBranch || 'main';
38
+ const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
39
+ const artefact = await findOutputArtefacts(cfm, io, fd, base);
40
+ const artefactFile = artefact ? artefact.file : null;
41
+ return { action: 'done', cycle: cycleId, artefact_file: artefactFile, next_cycles: await readCycleTargets(cycleId, io) };
38
42
  }
39
43
 
40
- function humanAppraiseAction(route, token, cycleId, io) {
41
- const art = findCycleOutputArtefact(cycleId, io);
42
- return { action: 'human_appraise', stage: route, token, context: { cycle: cycleId, artefact_file: art?.file ?? null, recent_feedback: readRecentFeedback(io) } };
44
+ async function blockedAction(cycleId, io, details, foundryDir, baseBranch) {
45
+ const fd = foundryDir || 'foundry';
46
+ const base = baseBranch || 'main';
47
+ const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
48
+ const artefact = await findOutputArtefacts(cfm, io, fd, base);
49
+ const artefactFile = artefact ? artefact.file : null;
50
+ const reason = details || 'iteration limit reached with unresolved feedback';
51
+ return { action: 'blocked', cycle: cycleId, artefact_file: artefactFile, reason };
43
52
  }
44
53
 
45
- function missingModelViolation(cycleId, route, io) {
46
- const art = findCycleOutputArtefact(cycleId, io);
47
- return violation(`cycle ${cycleId} stage ${route} has no model declared in cycle definition`, [art?.file].filter(Boolean));
54
+ async function humanAppraiseAction(route, token, ctx) {
55
+ const { cycleId, io, baseBranch } = ctx;
56
+ const fd = ctx.foundryDir || 'foundry';
57
+ const base = baseBranch || 'main';
58
+ const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
59
+ const artefact = await findOutputArtefacts(cfm, io, fd, base);
60
+ const artefactFile = artefact ? artefact.file : null;
61
+ return { action: 'human_appraise', stage: route, token, context: { cycle: cycleId, artefact_file: artefactFile, recent_feedback: readRecentFeedback(io) } };
62
+ }
63
+
64
+ async function missingModelViolation(cycleId, route, io, foundryDir, baseBranch) {
65
+ const fd = foundryDir || 'foundry';
66
+ const base = baseBranch || 'main';
67
+ const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
68
+ const outputType = cfm ? cfm['output-type'] : undefined;
69
+ const artefacts = outputType ? await getArtefactFiles(fd, outputType, io, { baseBranch: base }) : [];
70
+ const affectedFiles = artefacts.filter(a => a.state !== 'deleted').map(a => a.file);
71
+ return violation(`cycle ${cycleId} stage ${route} has no model declared in cycle definition`, affectedFiles);
48
72
  }
49
73
 
50
74
  function makeDispatchPayload(route, cycleId, token, cwd, filePatterns) {
@@ -52,7 +76,7 @@ function makeDispatchPayload(route, cycleId, token, cwd, filePatterns) {
52
76
  }
53
77
 
54
78
  async function buildDispatchAction(route, model, token, ctx) {
55
- if (!model) return missingModelViolation(ctx.cycleId, route, ctx.io);
79
+ if (!model) return missingModelViolation(ctx.cycleId, route, ctx.io, ctx.foundryDir, ctx.baseBranch ?? 'main');
56
80
  const base = route.split(':')[0];
57
81
  const filePatterns = base === 'forge' ? await readForgeFilePatterns(ctx.cycleId, ctx.io) : null;
58
82
  return { action: 'dispatch', stage: route, subagent_type: model, prompt: renderDispatchPrompt(makeDispatchPayload(route, ctx.cycleId, token, ctx.cwd, filePatterns)) };
@@ -63,26 +87,27 @@ export function routeDispatch(route) {
63
87
  }
64
88
 
65
89
  async function handleTerminalRoute(route, sortResult, ctx) {
66
- if (route === 'done') return doneAction(ctx.cycleId, ctx.io);
67
- if (route === 'blocked') return blockedAction(ctx.cycleId, ctx.io, sortResult.details);
68
- return violation(sortResult.details ?? 'sort returned violation');
90
+ const baseBranch = ctx.baseBranch || 'main';
91
+ if (route === 'done') return doneAction(ctx.cycleId, ctx.io, ctx.foundryDir, baseBranch);
92
+ if (route === 'blocked') return blockedAction(ctx.cycleId, ctx.io, sortResult.details, ctx.foundryDir, baseBranch);
93
+ const details = sortResult.details || 'sort returned violation';
94
+ return violation(details);
69
95
  }
70
96
 
71
97
  function isTerminalRoute(route) {
72
98
  return route === 'done' || route === 'blocked' || route === 'violation';
73
99
  }
74
100
 
101
+ function getRouteBase(route) {
102
+ return routeDispatch(route);
103
+ }
104
+
75
105
  export async function handleSortResult(sortResult, ctx) {
76
106
  const { route, model, token } = sortResult;
77
- if (isTerminalRoute(route)) {
78
- return handleTerminalRoute(route, sortResult, ctx);
79
- }
80
- if (routeDispatch(route) === 'quench' || routeDispatch(route) === 'appraise') {
81
- return violation(`${routeDispatch(route)} route reached handleSortResult — should have been handled upstream in orchestrate.js`);
82
- }
83
- if (routeDispatch(route) === 'human-appraise') {
84
- return humanAppraiseAction(route, token, ctx.cycleId, ctx.io);
85
- }
107
+ const routeBase = getRouteBase(route);
108
+ if (isTerminalRoute(route)) return handleTerminalRoute(route, sortResult, ctx);
109
+ if (routeBase === 'quench' || routeBase === 'appraise') return violation(routeBase + ' route reached handleSortResult');
110
+ if (routeBase === 'human-appraise') return humanAppraiseAction(route, token, ctx);
86
111
  return buildDispatchAction(route, model, token, ctx);
87
112
  }
88
113
 
@@ -243,12 +268,11 @@ function readOriginalState(io) {
243
268
  return { workMd: io.readFile('WORK.md'), history: io.exists('WORK.history.yaml') ? io.readFile('WORK.history.yaml') : null };
244
269
  }
245
270
 
246
- function buildFinalizeViolation(finalizeResult, blockResult) {
247
- const blockNote = formatBlockNote(blockResult);
271
+ function buildFinalizeViolation(finalizeResult) {
248
272
  if (finalizeResult.error === 'unexpected_files') {
249
- return violation(`unexpected files written by subagent: ${(finalizeResult.files || []).join(', ')}${blockNote}`, finalizeResult.files || []);
273
+ return violation(`unexpected files written by subagent: ${(finalizeResult.files || []).join(', ')}`, finalizeResult.files || []);
250
274
  }
251
- return violation(`stage_finalize error: ${finalizeResult.error}${blockNote}`, []);
275
+ return violation(`stage_finalize error: ${finalizeResult.error}`, []);
252
276
  }
253
277
 
254
278
  function writeHistoryEntries(ctx) {
@@ -291,9 +315,8 @@ export async function finaliseStage(args) {
291
315
  }
292
316
  const finalizeResult = await finalize({ cycleId, stage: lastStage.stage, baseSha: lastStage.baseSha, io });
293
317
  if (!finalizeResult.ok) {
294
- const blockResult = markArtefactBlocked(cycleId, io);
295
318
  clearStageState(activeStage, null, io);
296
- return buildFinalizeViolation(finalizeResult, blockResult);
319
+ return buildFinalizeViolation(finalizeResult);
297
320
  }
298
321
  const historyPath = 'WORK.history.yaml';
299
322
  const iteration = getIteration(historyPath, cycleId, io);
@@ -310,12 +333,12 @@ export async function finaliseStage(args) {
310
333
  }
311
334
 
312
335
  export function handleViolation(args) {
313
- const { lastResult, activeStage, lastStage, cycleId, io } = args;
336
+ const { lastResult, activeStage, lastStage } = args;
314
337
  const failedStage = activeStage || lastStage;
315
338
  if (!failedStage) { return violation('lastResult.ok=false but no stage recorded — orphaned state'); }
316
- const blockResult = markArtefactBlocked(cycleId, io);
317
- clearStageState(activeStage, lastStage, io);
318
- const art = findCycleOutputArtefact(cycleId, io);
319
- const blockNote = formatBlockNote(blockResult);
320
- return violation(`subagent dispatch failed: ${lastResult.error || 'unknown error'}${blockNote}`, [art?.file].filter(Boolean));
339
+ clearStageState(activeStage, lastStage, args.io);
340
+ return violation(
341
+ `subagent dispatch failed: ${lastResult.error || 'unknown error'}`,
342
+ lastResult.affected_files || [],
343
+ );
321
344
  }
@@ -8,14 +8,12 @@ import { readActiveStage, readLastStage, writeActiveStage, clearActiveStage } fr
8
8
  import { stageBaseOf } from './lib/stage-guard.js';
9
9
  import { ulid as defaultUlid } from './lib/ulid.js';
10
10
  import {
11
- findCycleOutputArtefact,
12
11
  readCycleTargets,
13
12
  readForgeFilePatterns,
14
13
  renderDispatchPrompt,
15
14
  synthesizeStages,
16
15
  violation,
17
16
  computeOpenFeedback,
18
- markArtefactBlocked,
19
17
  DISPATCH_MULTI_ACTION,
20
18
  validateDispatchMulti,
21
19
  buildDispatchMultiResponse,
@@ -36,7 +34,7 @@ export {
36
34
  DISPATCH_MULTI_ACTION, validateDispatchMulti, buildDispatchMultiResponse,
37
35
  };
38
36
  export { gatherAppraiseContext, consolidateAppraise };
39
- export { findCycleOutputArtefact, readCycleTargets, readForgeFilePatterns };
37
+ export { readCycleTargets, readForgeFilePatterns };
40
38
  export { handleSortResult as __handleSortResultForTest };
41
39
 
42
40
  export function needsSetup(workMdContent) {
@@ -121,7 +119,7 @@ function buildSortArgs(args, now) {
121
119
  }
122
120
 
123
121
  function buildSortContext(cycleId, args, io) {
124
- return { cycleId, cwd: args.cwd ?? process.cwd(), io };
122
+ return { cycleId, cwd: args.cwd ?? process.cwd(), io, foundryDir: args.foundryDir ?? 'foundry', baseBranch: args.baseBranch ?? 'main' };
125
123
  }
126
124
 
127
125
  function buildSetupArgs(cycleResult, args, io) {
@@ -162,6 +160,7 @@ function buildQuenchContext(cycleId, args, io) {
162
160
  const stageId = `quench:${cycleId}`;
163
161
  return { cycleId, stageId, io, git: args.git, finalize: buildFinalizeWrapper(cycleId, args, io),
164
162
  now: args.now, ulid: args.ulid, mint: args.mint, foundryDir: 'foundry', defaultModel: args.defaultModel,
163
+ baseBranch: args.baseBranch ?? 'main',
165
164
  feedback: buildFeedback(cycleId, stageId, io) };
166
165
  }
167
166
 
@@ -169,6 +168,7 @@ function buildAppraiseCtx(cycleId, args, io) {
169
168
  const stageId = `appraise:${cycleId}`;
170
169
  return { cycleId, io, git: args.git, finalize: buildFinalizeWrapper(cycleId, args, io),
171
170
  foundryDir: 'foundry', defaultModel: args.defaultModel,
171
+ baseBranch: args.baseBranch ?? 'main',
172
172
  activeStage: readActiveStage(io), lastStage: readLastStage(io),
173
173
  feedback: buildFeedback(cycleId, stageId, io) };
174
174
  }
@@ -211,7 +211,6 @@ async function handleAppraiseConsolidateRoute(sortResult, preCheck, args, io) {
211
211
  const ctx = buildAppraiseCtx(preCheck.cycleId, args, io);
212
212
  const result = await consolidateAppraise(ctx, args.lastResults);
213
213
  if (result.action === 'violation') {
214
- markArtefactBlocked(preCheck.cycleId, io);
215
214
  clearActiveStage(io);
216
215
  return result;
217
216
  }
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Quench module — deterministic validation run entirely within the orchestrator.
3
3
  *
4
- * The module runs validators for each draft artefact in the current cycle,
5
- * posts feedback for each validation item, resolves prior quench feedback,
6
- * and finalises the stage. No LLM involvement.
4
+ * The module discovers artefact changes via branch-based artefact discovery,
5
+ * runs validators for each artefact change, posts feedback for each validation
6
+ * item, resolves prior quench feedback, and finalises the stage. No LLM
7
+ * involvement.
7
8
  */
8
9
 
9
10
  import { readActiveStage } from './lib/state.js';
10
- import { getArtefactsForCycle, setArtefactStatus } from './lib/artefacts.js';
11
+ import { getArtefactFiles } from './lib/artefacts.js';
12
+ import { getCycleDefinition } from './lib/config.js';
11
13
  import { performValidation } from './lib/validation.js';
12
14
 
13
15
  /**
@@ -22,20 +24,37 @@ export async function runQuench(ctx) {
22
24
  return { ok: false, error: 'No active stage found' };
23
25
  }
24
26
 
25
- const artefacts = getArtefactsForCycle(ctx.cycleId, ctx.io);
27
+ const cycleDef = await getCycleDefinition(ctx.foundryDir, ctx.cycleId, ctx.io);
28
+ const outputType = cycleDef.frontmatter['output-type'];
29
+ if (!outputType) {
30
+ return { ok: false, error: `Cycle ${ctx.cycleId} has no output-type` };
31
+ }
32
+
33
+ const discovery = await discoverArtefacts(ctx, outputType);
34
+ if (!discovery.ok) return discovery;
35
+ const artefacts = discovery.artefacts;
26
36
 
27
37
  if (artefacts.length === 0) {
28
38
  return await handleNoArtefacts(ctx, activeStageRecord);
29
39
  }
30
40
 
31
- return await processArtefacts(ctx, artefacts, activeStageRecord);
41
+ return await processArtefacts(ctx, artefacts, activeStageRecord, outputType);
42
+ }
43
+
44
+ async function discoverArtefacts(ctx, outputType) {
45
+ try {
46
+ const artefacts = await getArtefactFiles(ctx.foundryDir, outputType, ctx.io, { baseBranch: ctx.baseBranch ?? 'main' });
47
+ return { ok: true, artefacts };
48
+ } catch (err) {
49
+ return { ok: false, error: `Failed to discover artefacts: ${err.message}` };
50
+ }
32
51
  }
33
52
 
34
53
  /**
35
54
  * Handle the case where no artefacts exist for this cycle.
36
55
  */
37
56
  async function handleNoArtefacts(ctx, activeStageRecord) {
38
- const summary = 'SKIP: no artefacts';
57
+ const summary = 'SKIP: no files';
39
58
  await ctx.finalize({
40
59
  lastStage: { stage: ctx.stageId, summary, baseSha: activeStageRecord.baseSha },
41
60
  activeStage: activeStageRecord,
@@ -46,16 +65,17 @@ async function handleNoArtefacts(ctx, activeStageRecord) {
46
65
  /**
47
66
  * Process each artefact: run validation, post feedback, handle errors.
48
67
  */
49
- async function processArtefacts(ctx, artefacts, activeStageRecord) {
68
+ async function processArtefacts(ctx, artefacts, activeStageRecord, outputType) {
50
69
  const perArtefact = [];
51
70
  const currentFeedback = [];
52
71
  let allOk = true;
53
72
 
54
73
  for (const artefact of artefacts) {
55
74
  const result = await performValidation({
56
- typeId: artefact.type,
75
+ typeId: outputType,
57
76
  io: ctx.io,
58
77
  foundryDir: ctx.foundryDir,
78
+ artefacts: [artefact],
59
79
  });
60
80
 
61
81
  const outcome = handleArtefactResult(ctx, artefact, result, currentFeedback);
@@ -89,12 +109,10 @@ function handleArtefactResult(ctx, artefact, result, currentFeedback) {
89
109
  }
90
110
 
91
111
  if (result.error) {
92
- markArtefactBlocked(ctx.io, artefact.file);
93
112
  return { ok: false, text: `${artefact.file}: ${result.error}` };
94
113
  }
95
114
 
96
115
  if (isAllErrors(result)) {
97
- markArtefactBlocked(ctx.io, artefact.file);
98
116
  const messages = result.errors.map(e => e.message).join('; ');
99
117
  return { ok: false, text: `${artefact.file}: ${messages}` };
100
118
  }
@@ -142,12 +160,3 @@ function resolvePriorFeedback(ctx, currentFeedback) {
142
160
  ctx.feedback.resolve(prior.id, decision);
143
161
  }
144
162
  }
145
-
146
- /**
147
- * Mark an artefact as blocked in the artefacts table.
148
- */
149
- function markArtefactBlocked(io, file) {
150
- const workText = io.readFile('WORK.md');
151
- const updated = setArtefactStatus(workText, file, 'blocked');
152
- io.writeFile('WORK.md', updated);
153
- }
@@ -275,7 +275,6 @@ export function runSort(args = {}, io = defaultIO) {
275
275
  // Exports (for testing) — keep main() private
276
276
  // ---------------------------------------------------------------------------
277
277
 
278
- export { parseArtefactsTable } from './lib/artefacts.js';
279
278
  export { loadHistory } from './lib/history.js';
280
279
  export { parseFrontmatter } from './lib/workfile.js';
281
280
  export {