@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.
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +32 -43
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +9 -2
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +0 -13
- package/dist/CHANGELOG.md +23 -0
- package/dist/docs/architecture.md +1 -1
- package/dist/docs/tools.md +5 -28
- package/dist/docs/work-spec.md +2 -2
- package/dist/scripts/appraise-module.js +17 -5
- package/dist/scripts/lib/artefacts.js +134 -131
- package/dist/scripts/lib/attestation/attest.js +10 -18
- package/dist/scripts/lib/attestation/payload.js +36 -10
- package/dist/scripts/lib/finalize.js +5 -5
- package/dist/scripts/lib/validation.js +20 -6
- package/dist/scripts/lib/workfile.js +0 -3
- package/dist/scripts/orchestrate-cycle.js +5 -34
- package/dist/scripts/orchestrate-phases.js +63 -40
- package/dist/scripts/orchestrate.js +4 -5
- package/dist/scripts/quench-module.js +29 -20
- package/dist/scripts/sort.js +0 -1
- package/dist/skills/appraise/SKILL.md +3 -3
- package/dist/skills/human-appraise/SKILL.md +6 -7
- package/dist/skills/orchestrate/SKILL.md +4 -5
- package/dist/skills/quench/SKILL.md +4 -4
- package/package.json +1 -1
|
@@ -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.
|
|
25
|
-
feedbackText: io.
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
25
|
+
const { readFile, exists } = io ?? defaultIo(cwd);
|
|
23
26
|
|
|
24
27
|
return {
|
|
25
28
|
workText: readFile(path.join(cwd, 'WORK.md')),
|
|
26
|
-
historyText:
|
|
27
|
-
feedbackText:
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
67
|
-
|
|
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).
|
|
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
|
-
|
|
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(' '),
|
|
@@ -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
|
-
|
|
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
|
|
31
|
-
const
|
|
32
|
-
|
|
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
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
|
41
|
-
const
|
|
42
|
-
|
|
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
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
-
|
|
67
|
-
if (route === '
|
|
68
|
-
return
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
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
|
|
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(', ')}
|
|
273
|
+
return violation(`unexpected files written by subagent: ${(finalizeResult.files || []).join(', ')}`, finalizeResult.files || []);
|
|
250
274
|
}
|
|
251
|
-
return violation(`stage_finalize error: ${finalizeResult.error}
|
|
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
|
|
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
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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 {
|
|
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
|
|
5
|
-
*
|
|
6
|
-
* and finalises the stage. No LLM
|
|
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 {
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
}
|
package/dist/scripts/sort.js
CHANGED
|
@@ -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 {
|