@really-knows-ai/foundry 2.3.2 → 3.0.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/README.md +180 -369
- package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
- package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
- package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
- package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
- package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
- package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
- package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
- package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
- package/dist/.opencode/plugins/foundry.js +105 -0
- package/dist/CHANGELOG.md +533 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +433 -0
- package/dist/docs/concepts.md +395 -0
- package/dist/docs/getting-started.md +344 -0
- package/dist/docs/memory-maintenance.md +176 -0
- package/dist/docs/tools.md +1411 -0
- package/dist/docs/work-spec.md +283 -0
- package/dist/scripts/lib/artefacts.js +151 -0
- package/dist/scripts/lib/assay/loader.js +151 -0
- package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
- package/dist/scripts/lib/assay/permissions.js +52 -0
- package/dist/scripts/lib/assay/run.js +219 -0
- package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
- package/dist/scripts/lib/attestation/attest.js +111 -0
- package/dist/scripts/lib/attestation/canonical-json.js +109 -0
- package/dist/scripts/lib/attestation/hash.js +17 -0
- package/dist/scripts/lib/attestation/parse.js +14 -0
- package/dist/scripts/lib/attestation/payload.js +106 -0
- package/dist/scripts/lib/attestation/render.js +16 -0
- package/dist/scripts/lib/attestation/verify.js +15 -0
- package/dist/scripts/lib/branch-guard.js +72 -0
- package/dist/scripts/lib/config-creators/appraiser.js +9 -0
- package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
- package/dist/scripts/lib/config-creators/cycle.js +11 -0
- package/dist/scripts/lib/config-creators/factory.js +49 -0
- package/dist/scripts/lib/config-creators/flow.js +11 -0
- package/dist/scripts/lib/config-validators/appraiser.js +49 -0
- package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
- package/dist/scripts/lib/config-validators/cycle.js +131 -0
- package/dist/scripts/lib/config-validators/flow.js +57 -0
- package/dist/scripts/lib/config-validators/helpers.js +96 -0
- package/dist/scripts/lib/config-validators/law.js +96 -0
- package/dist/scripts/lib/config.js +328 -0
- package/dist/scripts/lib/failed-flow.js +131 -0
- package/dist/scripts/lib/feedback-store.js +249 -0
- package/dist/scripts/lib/feedback-transitions.js +105 -0
- package/dist/scripts/lib/finalize.js +70 -0
- package/dist/scripts/lib/foundational-guards.js +13 -0
- package/dist/scripts/lib/git-bridge.js +77 -0
- package/dist/scripts/lib/git-finish/work-finish.js +233 -0
- package/dist/scripts/lib/git-policy.js +101 -0
- package/dist/scripts/lib/guards.js +125 -0
- package/dist/scripts/lib/history.js +132 -0
- package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
- package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
- package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
- package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
- package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
- package/dist/scripts/lib/memory/admin/dump.js +47 -0
- package/dist/scripts/lib/memory/admin/helpers.js +31 -0
- package/dist/scripts/lib/memory/admin/init.js +170 -0
- package/dist/scripts/lib/memory/admin/live-store.js +76 -0
- package/dist/scripts/lib/memory/admin/reembed.js +285 -0
- package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
- package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
- package/dist/scripts/lib/memory/admin/reset.js +24 -0
- package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
- package/dist/scripts/lib/memory/admin/validate.js +19 -0
- package/dist/scripts/lib/memory/config.js +149 -0
- package/dist/scripts/lib/memory/cozo.js +136 -0
- package/dist/scripts/lib/memory/drift.js +71 -0
- package/dist/scripts/lib/memory/embeddings.js +128 -0
- package/dist/scripts/lib/memory/frontmatter.js +75 -0
- package/dist/scripts/lib/memory/ndjson.js +84 -0
- package/dist/scripts/lib/memory/paths.js +25 -0
- package/dist/scripts/lib/memory/permissions.js +41 -0
- package/dist/scripts/lib/memory/prompt.js +109 -0
- package/dist/scripts/lib/memory/query.js +56 -0
- package/dist/scripts/lib/memory/reads.js +109 -0
- package/dist/scripts/lib/memory/schema.js +64 -0
- package/dist/scripts/lib/memory/search.js +73 -0
- package/dist/scripts/lib/memory/singleton.js +49 -0
- package/dist/scripts/lib/memory/store.js +162 -0
- package/dist/scripts/lib/memory/types.js +93 -0
- package/dist/scripts/lib/memory/validate.js +58 -0
- package/dist/scripts/lib/memory/writes.js +40 -0
- package/{scripts → dist/scripts}/lib/pending.js +7 -2
- package/dist/scripts/lib/secret.js +59 -0
- package/{scripts → dist/scripts}/lib/slug.js +3 -2
- package/dist/scripts/lib/snapshot/finish.js +103 -0
- package/dist/scripts/lib/snapshot/inspect.js +253 -0
- package/dist/scripts/lib/snapshot/render.js +55 -0
- package/dist/scripts/lib/sort-fs-check.js +121 -0
- package/dist/scripts/lib/sort-routing.js +101 -0
- package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
- package/{scripts → dist/scripts}/lib/state.js +4 -0
- package/dist/scripts/lib/token.js +57 -0
- package/dist/scripts/lib/tracing.js +59 -0
- package/dist/scripts/lib/ulid.js +100 -0
- package/dist/scripts/lib/validator-jsonl.js +162 -0
- package/{scripts → dist/scripts}/lib/workfile.js +38 -20
- package/dist/scripts/orchestrate-cycle.js +215 -0
- package/dist/scripts/orchestrate-phases.js +314 -0
- package/dist/scripts/orchestrate.js +163 -0
- package/dist/scripts/sort.js +278 -0
- package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +62 -40
- package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
- package/dist/skills/add-law/SKILL.md +191 -0
- package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
- package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
- package/{skills → dist/skills}/appraise/SKILL.md +62 -13
- package/dist/skills/assay/SKILL.md +72 -0
- package/dist/skills/change-embedding-model/SKILL.md +58 -0
- package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
- package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
- package/dist/skills/dry-run/SKILL.md +116 -0
- package/{skills → dist/skills}/flow/SKILL.md +15 -2
- package/dist/skills/forge/SKILL.md +121 -0
- package/dist/skills/human-appraise/SKILL.md +153 -0
- package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
- package/dist/skills/init-memory/SKILL.md +92 -0
- package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
- package/dist/skills/quench/SKILL.md +99 -0
- package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
- package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
- package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
- package/dist/skills/reset-memory/SKILL.md +54 -0
- package/dist/skills/upgrade-foundry/SKILL.md +191 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -100
- package/docs/concepts.md +0 -122
- package/docs/getting-started.md +0 -187
- package/docs/work-spec.md +0 -207
- package/scripts/lib/artefacts.js +0 -124
- package/scripts/lib/config.js +0 -175
- package/scripts/lib/feedback-transitions.js +0 -25
- package/scripts/lib/feedback.js +0 -440
- package/scripts/lib/finalize.js +0 -41
- package/scripts/lib/history.js +0 -59
- package/scripts/lib/secret.js +0 -23
- package/scripts/lib/tags.js +0 -108
- package/scripts/lib/token.js +0 -26
- package/scripts/orchestrate.js +0 -418
- package/scripts/sort.js +0 -370
- package/scripts/validate-tags.js +0 -54
- package/skills/add-law/SKILL.md +0 -111
- package/skills/forge/SKILL.md +0 -88
- package/skills/human-appraise/SKILL.md +0 -82
- package/skills/quench/SKILL.md +0 -62
- package/skills/upgrade-foundry/SKILL.md +0 -216
- /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Foundry v2.3.0 orchestrate: cycle-level helpers.
|
|
2
|
+
// Pure utilities and cycle-data accessors used by orchestration phases.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getCycleDefinition,
|
|
6
|
+
getArtefactType,
|
|
7
|
+
} from './lib/config.js';
|
|
8
|
+
import { parseArtefactsTable, setArtefactStatus } from './lib/artefacts.js';
|
|
9
|
+
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Public helpers (re-exported by orchestrate.js for tests).
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
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
|
+
export async function readCycleTargets(cycleId, io) {
|
|
24
|
+
try {
|
|
25
|
+
const cd = await getCycleDefinition('foundry', cycleId, io);
|
|
26
|
+
return cd.frontmatter?.targets ?? [];
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractOutputType(cd) {
|
|
33
|
+
return cd.frontmatter?.['output-type'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function fetchFilePatterns(output, io) {
|
|
37
|
+
const at = await getArtefactType('foundry', output, io);
|
|
38
|
+
return at.frontmatter?.['file-patterns'] ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readForgeFilePatterns(cycleId, io) {
|
|
42
|
+
let cd;
|
|
43
|
+
try {
|
|
44
|
+
cd = await getCycleDefinition('foundry', cycleId, io);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const output = extractOutputType(cd);
|
|
49
|
+
if (!output) return null;
|
|
50
|
+
return fetchFilePatterns(output, io);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Private helpers.
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function isWontFixOrRejected(it) {
|
|
58
|
+
const s = it.history[0].state;
|
|
59
|
+
return s === 'wont-fix' || s === 'rejected';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function compareTimestampsDesc(a, b) {
|
|
63
|
+
const aTs = a.history[0].timestamp;
|
|
64
|
+
const bTs = b.history[0].timestamp;
|
|
65
|
+
if (aTs !== bTs) return aTs < bTs ? 1 : -1;
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function feedbackProjection(it) {
|
|
70
|
+
return {
|
|
71
|
+
id: it.id,
|
|
72
|
+
file: it.file,
|
|
73
|
+
text: it.text,
|
|
74
|
+
state: it.history[0].state,
|
|
75
|
+
reason: it.history[0].reason,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readRecentFeedback(io, limit = 5) {
|
|
80
|
+
try {
|
|
81
|
+
if (!io.exists('WORK.feedback.yaml')) return [];
|
|
82
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
83
|
+
const items = store.list();
|
|
84
|
+
const candidates = items.filter(isWontFixOrRejected);
|
|
85
|
+
candidates.sort(compareTimestampsDesc);
|
|
86
|
+
return candidates.slice(0, limit).map(feedbackProjection);
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Spec §10. Count non-resolved items for stamping on every history entry.
|
|
94
|
+
*/
|
|
95
|
+
function isOpenFeedback(it) {
|
|
96
|
+
return it.history[0].state !== 'resolved';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function computeOpenFeedback(io) {
|
|
100
|
+
if (!io.exists('WORK.feedback.yaml')) return 0;
|
|
101
|
+
try {
|
|
102
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
103
|
+
return store.list().filter(isOpenFeedback).length;
|
|
104
|
+
} catch {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function violation(details, affectedFiles = []) {
|
|
110
|
+
return {
|
|
111
|
+
action: 'violation',
|
|
112
|
+
details,
|
|
113
|
+
recoverable: false,
|
|
114
|
+
affected_files: affectedFiles,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isUnexpectedFilesError(err) {
|
|
119
|
+
return err && err.code === 'unexpected_files';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function collectErrorFiles(err) {
|
|
123
|
+
return Array.isArray(err.files) ? err.files : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildSetupViolationMessage(files, phase) {
|
|
127
|
+
const where = phase === 'setup'
|
|
128
|
+
? 'flow setup requires a clean worktree'
|
|
129
|
+
: `stage ${phase} commit may only include allowed files`;
|
|
130
|
+
return `${where}; refusing to commit unrelated changes: ${files.join(', ')}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Call git.commit with phase-appropriate allowed patterns.
|
|
135
|
+
* Returns null on success, a violation object on policy failure.
|
|
136
|
+
*/
|
|
137
|
+
export function tryCommit(git, message, allowedPatterns, phase) {
|
|
138
|
+
if (!git || typeof git.commit !== 'function') return null;
|
|
139
|
+
try {
|
|
140
|
+
git.commit(message, { allowedPatterns });
|
|
141
|
+
return null;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (isUnexpectedFilesError(err)) {
|
|
144
|
+
const files = collectErrorFiles(err);
|
|
145
|
+
return violation(buildSetupViolationMessage(files, phase), files);
|
|
146
|
+
}
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
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
|
+
|
|
173
|
+
export function formatBlockNote(blockResult) {
|
|
174
|
+
return blockResult.ok ? '' : ` (also: failed to mark artefact blocked: ${blockResult.error})`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Stage synthesis (pure utility, used by setupWorkfile and exported publicly).
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
export function synthesizeStages({ cycleId, hasValidation, humanAppraise, assay = false }) {
|
|
182
|
+
const stages = [];
|
|
183
|
+
if (assay) stages.push(`assay:${cycleId}`);
|
|
184
|
+
stages.push(`forge:${cycleId}`);
|
|
185
|
+
if (hasValidation) stages.push(`quench:${cycleId}`);
|
|
186
|
+
stages.push(`appraise:${cycleId}`);
|
|
187
|
+
if (humanAppraise) stages.push(`human-appraise:${cycleId}`);
|
|
188
|
+
return stages;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Dispatch prompt rendering (pure utility, used by handleSortResult and exported publicly).
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns }) {
|
|
196
|
+
const lines = [
|
|
197
|
+
`You are a Foundry stage agent. Invoke the ${stage.split(':')[0]} skill and follow its instructions exactly.`,
|
|
198
|
+
``,
|
|
199
|
+
`Stage: ${stage}`,
|
|
200
|
+
`Cycle: ${cycle}`,
|
|
201
|
+
`Token: ${token}`,
|
|
202
|
+
`Working directory: ${cwd}`,
|
|
203
|
+
];
|
|
204
|
+
if (filePatterns && filePatterns.length) {
|
|
205
|
+
lines.push(`File patterns (forge only): ${JSON.stringify(filePatterns)}`);
|
|
206
|
+
}
|
|
207
|
+
lines.push(
|
|
208
|
+
``,
|
|
209
|
+
`Your FIRST tool call MUST be foundry_stage_begin({stage, cycle, token}) using the values above.`,
|
|
210
|
+
`Your LAST tool call MUST be foundry_stage_end({summary}).`,
|
|
211
|
+
``,
|
|
212
|
+
`When done, report back a brief summary. Do NOT call foundry_history_append, foundry_git_commit, or foundry_artefacts_add — the orchestrator handles all of those.`
|
|
213
|
+
);
|
|
214
|
+
return lines.join('\n');
|
|
215
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Foundry v2.3.0 orchestrate: phase logic.
|
|
2
|
+
// Private phase functions used by runOrchestrate.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getCycleDefinition,
|
|
6
|
+
getArtefactType,
|
|
7
|
+
getLawsForQuench,
|
|
8
|
+
} from './lib/config.js';
|
|
9
|
+
import { parseFrontmatter, writeFrontmatter } from './lib/workfile.js';
|
|
10
|
+
import { clearActiveStage, clearLastStage } from './lib/state.js';
|
|
11
|
+
import { appendEntry, getIteration } from './lib/history.js';
|
|
12
|
+
import { stageBaseOf } from './lib/stage-guard.js';
|
|
13
|
+
import { allowedPatternsForStage } from './lib/git-policy.js';
|
|
14
|
+
import { loadExtractor } from './lib/assay/loader.js';
|
|
15
|
+
import { checkExtractorAgainstCycle } from './lib/assay/permissions.js';
|
|
16
|
+
import {
|
|
17
|
+
findCycleOutputArtefact,
|
|
18
|
+
readCycleTargets,
|
|
19
|
+
readForgeFilePatterns,
|
|
20
|
+
readRecentFeedback,
|
|
21
|
+
computeOpenFeedback,
|
|
22
|
+
violation,
|
|
23
|
+
tryCommit,
|
|
24
|
+
markArtefactBlocked,
|
|
25
|
+
formatBlockNote,
|
|
26
|
+
synthesizeStages,
|
|
27
|
+
renderDispatchPrompt,
|
|
28
|
+
} from './orchestrate-cycle.js';
|
|
29
|
+
|
|
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) };
|
|
33
|
+
}
|
|
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' };
|
|
38
|
+
}
|
|
39
|
+
|
|
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) } };
|
|
43
|
+
}
|
|
44
|
+
|
|
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));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeDispatchPayload(route, cycleId, token, cwd, filePatterns) {
|
|
51
|
+
return { stage: route, cycle: cycleId, token, cwd, filePatterns };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function buildDispatchAction(route, model, token, ctx) {
|
|
55
|
+
if (!model) return missingModelViolation(ctx.cycleId, route, ctx.io);
|
|
56
|
+
const base = route.split(':')[0];
|
|
57
|
+
const filePatterns = base === 'forge' ? await readForgeFilePatterns(ctx.cycleId, ctx.io) : null;
|
|
58
|
+
return { action: 'dispatch', stage: route, subagent_type: model, prompt: renderDispatchPrompt(makeDispatchPayload(route, ctx.cycleId, token, ctx.cwd, filePatterns)) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function routeDispatch(route) {
|
|
62
|
+
return typeof route === 'string' ? route.split(':')[0] : '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
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');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function handleSortResult(sortResult, ctx) {
|
|
72
|
+
const { route, model, token } = sortResult;
|
|
73
|
+
if (route === 'done' || route === 'blocked' || route === 'violation') {
|
|
74
|
+
return handleTerminalRoute(route, sortResult, ctx);
|
|
75
|
+
}
|
|
76
|
+
if (routeDispatch(route) === 'human-appraise') {
|
|
77
|
+
return humanAppraiseAction(route, token, ctx.cycleId, ctx.io);
|
|
78
|
+
}
|
|
79
|
+
return buildDispatchAction(route, model, token, ctx);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function fetchCycleDefinition(foundryDir, cycleId, io) {
|
|
83
|
+
try { return await getCycleDefinition(foundryDir, cycleId, io); } catch { return null; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkOutputType(cfm, cycleId) {
|
|
87
|
+
const outputType = cfm['output-type'];
|
|
88
|
+
if (outputType) return { outputType };
|
|
89
|
+
if (cfm.output !== undefined) {
|
|
90
|
+
return { error: violation(`cycle ${cycleId} uses old schema key 'output:'. Rename it to 'output-type:' (run the upgrade-foundry skill).`, ['WORK.md']) };
|
|
91
|
+
}
|
|
92
|
+
return { error: violation(`cycle ${cycleId} missing output-type field`, ['WORK.md']) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function checkArtefactType(foundryDir, outputType, io) {
|
|
96
|
+
try { await getArtefactType(foundryDir, outputType, io); return { ok: true }; }
|
|
97
|
+
catch { return { error: violation(`artefact type not found: ${outputType}`, ['WORK.md']) }; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isAssayAbsent(assayBlock) { return assayBlock === undefined || assayBlock === null; }
|
|
101
|
+
function isAssayInvalid(assayBlock) { return typeof assayBlock !== 'object' || Array.isArray(assayBlock); }
|
|
102
|
+
|
|
103
|
+
function checkAssayExtractors(list, cycleId) {
|
|
104
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
105
|
+
return violation(`cycle ${cycleId}: 'assay.extractors' must be a non-empty array`, ['WORK.md']);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function checkAssayShape(cfm, cycleId) {
|
|
111
|
+
const assayBlock = cfm.assay;
|
|
112
|
+
if (isAssayAbsent(assayBlock)) return { ok: true, extractors: null };
|
|
113
|
+
if (isAssayInvalid(assayBlock)) {
|
|
114
|
+
return { error: violation(`cycle ${cycleId}: 'assay' must be a mapping`, ['WORK.md']) };
|
|
115
|
+
}
|
|
116
|
+
const extErr = checkAssayExtractors(assayBlock.extractors, cycleId);
|
|
117
|
+
if (extErr) return { error: extErr };
|
|
118
|
+
return { ok: true, extractors: assayBlock.extractors };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkMemoryEnabled(io, cycleId) {
|
|
122
|
+
if (!io.exists('foundry/memory/config.md')) {
|
|
123
|
+
return violation(`cycle ${cycleId}: 'assay:' requires memory to be enabled (run the init-memory skill first)`, ['WORK.md']);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function checkCycleWriteDecl(cfm, cycleId) {
|
|
129
|
+
const cycleWrite = cfm.memory?.write;
|
|
130
|
+
if (!Array.isArray(cycleWrite)) {
|
|
131
|
+
return violation(`cycle ${cycleId}: 'assay:' requires the cycle to declare memory.write`, ['WORK.md']);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function checkExtractors(foundryDir, cycleId, list, cycleWriteSet, io) {
|
|
137
|
+
for (const name of list) {
|
|
138
|
+
let ext;
|
|
139
|
+
try { ext = await loadExtractor(foundryDir, name, io); }
|
|
140
|
+
catch (err) { return violation(`cycle ${cycleId}: ${err.message}`, ['WORK.md']); }
|
|
141
|
+
const checkResult = checkExtractorAgainstCycle(ext, { writeTypes: cycleWriteSet });
|
|
142
|
+
if (!checkResult.ok) { return violation(`cycle ${cycleId}: ${checkResult.error}`, ['WORK.md']); }
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stageTag(s, cycleId) {
|
|
148
|
+
return typeof s === 'string' && s.includes(':') ? s : `${s}:${cycleId}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveStages(cfm, cycleId, hasValidation, assayExtractors) {
|
|
152
|
+
if (!Array.isArray(cfm.stages)) {
|
|
153
|
+
return synthesizeStages({ cycleId, hasValidation, humanAppraise: cfm['human-appraise'] === true, assay: !!assayExtractors });
|
|
154
|
+
}
|
|
155
|
+
if (cfm.stages.length === 0) {
|
|
156
|
+
return { error: violation(`cycle ${cycleId} has no stages declared in cycle definition`, ['WORK.md']) };
|
|
157
|
+
}
|
|
158
|
+
return cfm.stages.map(s => stageTag(s, cycleId));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function applyFmDefaults(newFm, cfm, assayExtractors) {
|
|
162
|
+
newFm['max-iterations'] = cfm['max-iterations'] ?? 3;
|
|
163
|
+
newFm['human-appraise'] = cfm['human-appraise'] === true;
|
|
164
|
+
newFm['deadlock-appraise'] = cfm['deadlock-appraise'] !== false;
|
|
165
|
+
newFm['deadlock-iterations'] = cfm['deadlock-iterations'] ?? 5;
|
|
166
|
+
if (cfm.models) newFm.models = cfm.models;
|
|
167
|
+
if (assayExtractors) newFm.assay = { extractors: assayExtractors };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildNewFrontmatter(workContent, stages, cfm, assayExtractors) {
|
|
171
|
+
const fm = parseFrontmatter(workContent);
|
|
172
|
+
const newFm = { ...fm };
|
|
173
|
+
newFm.stages = stages;
|
|
174
|
+
applyFmDefaults(newFm, cfm, assayExtractors);
|
|
175
|
+
const body = workContent.replace(/^---\n[\s\S]+?\n---\n?/, '');
|
|
176
|
+
const fmBlock = writeFrontmatter(newFm);
|
|
177
|
+
return body ? `${fmBlock}\n${body}` : fmBlock;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function checkAssayPrereqs(cfm, cycleId, io) {
|
|
181
|
+
const memErr = checkMemoryEnabled(io, cycleId);
|
|
182
|
+
if (memErr) return memErr;
|
|
183
|
+
return checkCycleWriteDecl(cfm, cycleId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runAssayValidation(cfm, cycleId, io, foundryDir) {
|
|
187
|
+
const assayResult = checkAssayShape(cfm, cycleId);
|
|
188
|
+
if (assayResult.error) return assayResult;
|
|
189
|
+
if (!assayResult.extractors) return { ok: true, extractors: null };
|
|
190
|
+
const prereqErr = await checkAssayPrereqs(cfm, cycleId, io);
|
|
191
|
+
if (prereqErr) return { error: prereqErr };
|
|
192
|
+
const cycleWriteSet = new Set(cfm.memory.write);
|
|
193
|
+
const extErr = await checkExtractors(foundryDir, cycleId, assayResult.extractors, cycleWriteSet, io);
|
|
194
|
+
if (extErr) return { error: extErr };
|
|
195
|
+
return { ok: true, extractors: assayResult.extractors };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function setupWorkfile(args) {
|
|
199
|
+
const { cycleId, workContent, io, git, foundryDir } = args;
|
|
200
|
+
const cycleDefDoc = await fetchCycleDefinition(foundryDir, cycleId, io);
|
|
201
|
+
if (!cycleDefDoc) return violation(`cycle definition not found for id: ${cycleId}`, ['WORK.md']);
|
|
202
|
+
const cfm = cycleDefDoc.frontmatter || {};
|
|
203
|
+
return runSetupPipeline({ cfm, cycleId, workContent, io, git, foundryDir });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runSetupPipeline(ctx) {
|
|
207
|
+
const typeResult = checkOutputType(ctx.cfm, ctx.cycleId);
|
|
208
|
+
if (typeResult.error) return typeResult.error;
|
|
209
|
+
const artefactResult = await checkArtefactType(ctx.foundryDir, typeResult.outputType, ctx.io);
|
|
210
|
+
if (artefactResult.error) return artefactResult.error;
|
|
211
|
+
const lawsWithValidators = await getLawsForQuench(ctx.foundryDir, ctx.io, { typeId: typeResult.outputType });
|
|
212
|
+
const assayResult = await runAssayValidation(ctx.cfm, ctx.cycleId, ctx.io, ctx.foundryDir);
|
|
213
|
+
if (assayResult.error) return assayResult.error;
|
|
214
|
+
return completeSetup({ ...ctx, lawsWithValidators, assayResult });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function completeSetup(ctx) {
|
|
218
|
+
const hasValidation = ctx.lawsWithValidators && ctx.lawsWithValidators.length > 0;
|
|
219
|
+
const stagesResult = resolveStages(ctx.cfm, ctx.cycleId, hasValidation, ctx.assayResult.extractors);
|
|
220
|
+
if (stagesResult.error) return stagesResult.error;
|
|
221
|
+
const newWork = buildNewFrontmatter(ctx.workContent, stagesResult, ctx.cfm, ctx.assayResult.extractors);
|
|
222
|
+
ctx.io.writeFile('WORK.md', newWork);
|
|
223
|
+
return trySetupCommit(ctx);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function trySetupCommit(ctx) {
|
|
227
|
+
if (!ctx.git || typeof ctx.git.commit !== 'function') {
|
|
228
|
+
return { ok: true, workContent: ctx.io.readFile('WORK.md') };
|
|
229
|
+
}
|
|
230
|
+
const v = tryCommit(ctx.git, `[${ctx.cycleId}] setup: configure stages and limits`, [], 'setup');
|
|
231
|
+
if (v) return v;
|
|
232
|
+
return { ok: true, workContent: ctx.io.readFile('WORK.md') };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readOriginalState(io) {
|
|
236
|
+
return { workMd: io.readFile('WORK.md'), history: io.exists('WORK.history.yaml') ? io.readFile('WORK.history.yaml') : null };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildFinalizeViolation(finalizeResult, blockResult) {
|
|
240
|
+
const blockNote = formatBlockNote(blockResult);
|
|
241
|
+
if (finalizeResult.error === 'unexpected_files') {
|
|
242
|
+
return violation(`unexpected files written by subagent: ${(finalizeResult.files || []).join(', ')}${blockNote}`, finalizeResult.files || []);
|
|
243
|
+
}
|
|
244
|
+
return violation(`stage_finalize error: ${finalizeResult.error}${blockNote}`, []);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function writeHistoryEntries(ctx) {
|
|
248
|
+
appendEntry(ctx.historyPath, { cycle: ctx.cycleId, stage: 'sort', iteration: ctx.iteration, route: ctx.lastStage.stage, comment: `route ${ctx.lastStage.stage}`, openFeedback: ctx.openFeedback }, ctx.io);
|
|
249
|
+
appendEntry(ctx.historyPath, { cycle: ctx.cycleId, stage: ctx.lastStage.stage, iteration: ctx.iteration, comment: ctx.lastStage.summary || '(no summary)', openFeedback: ctx.openFeedback, changedFiles: ctx.lastStage.changedFiles ?? [] }, ctx.io);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function computeAllowedPatterns(lastStage, cycleId, io) {
|
|
253
|
+
const stageBase = stageBaseOf(lastStage.stage);
|
|
254
|
+
const forgeFilePatterns = stageBase === 'forge' ? (await readForgeFilePatterns(cycleId, io)) ?? [] : [];
|
|
255
|
+
return allowedPatternsForStage({ stageBase, forgeFilePatterns });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildCommitMessage(cycleId, lastStage) {
|
|
259
|
+
return `[${cycleId}] ${lastStage.stage}: ${lastStage.summary || '(no summary)'}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function rollbackState(io, original) {
|
|
263
|
+
io.writeFile('WORK.md', original.workMd);
|
|
264
|
+
if (original.history !== null) { io.writeFile('WORK.history.yaml', original.history); }
|
|
265
|
+
else if (io.exists('WORK.history.yaml')) { io.unlink('WORK.history.yaml'); }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function tryStageCommit(git, lastStage, cycleId, io) {
|
|
269
|
+
if (!git || typeof git.commit !== 'function') return null;
|
|
270
|
+
const allowedPatterns = await computeAllowedPatterns(lastStage, cycleId, io);
|
|
271
|
+
return tryCommit(git, buildCommitMessage(cycleId, lastStage), allowedPatterns, lastStage.stage);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function clearStageState(activeStage, lastStage, io) {
|
|
275
|
+
if (activeStage) clearActiveStage(io);
|
|
276
|
+
if (lastStage) clearLastStage(io);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function finaliseStage(args) {
|
|
280
|
+
const { lastStage, activeStage, cycleId, io, finalize, git } = args;
|
|
281
|
+
const original = readOriginalState(io);
|
|
282
|
+
if (typeof finalize !== 'function') {
|
|
283
|
+
return violation('orchestrate caller must inject a `finalize` function when providing lastResult; the plugin wires lib/finalize.finalizeStage; tests must pass a stub.', []);
|
|
284
|
+
}
|
|
285
|
+
const finalizeResult = await finalize({ cycleId, stage: lastStage.stage, baseSha: lastStage.baseSha, io });
|
|
286
|
+
if (!finalizeResult.ok) {
|
|
287
|
+
const blockResult = markArtefactBlocked(cycleId, io);
|
|
288
|
+
clearStageState(activeStage, null, io);
|
|
289
|
+
return buildFinalizeViolation(finalizeResult, blockResult);
|
|
290
|
+
}
|
|
291
|
+
const historyPath = 'WORK.history.yaml';
|
|
292
|
+
const iteration = getIteration(historyPath, cycleId, io);
|
|
293
|
+
const openFeedback = computeOpenFeedback(io);
|
|
294
|
+
writeHistoryEntries({ historyPath, cycleId, lastStage, iteration, openFeedback, io });
|
|
295
|
+
const commitErr = await tryStageCommit(git, lastStage, cycleId, io);
|
|
296
|
+
if (commitErr) {
|
|
297
|
+
rollbackState(io, original);
|
|
298
|
+
clearStageState(activeStage, null, io);
|
|
299
|
+
return commitErr;
|
|
300
|
+
}
|
|
301
|
+
clearStageState(activeStage, lastStage, io);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function handleViolation(args) {
|
|
306
|
+
const { lastResult, activeStage, lastStage, cycleId, io } = args;
|
|
307
|
+
const failedStage = activeStage || lastStage;
|
|
308
|
+
if (!failedStage) { return violation('lastResult.ok=false but no stage recorded — orphaned state'); }
|
|
309
|
+
const blockResult = markArtefactBlocked(cycleId, io);
|
|
310
|
+
clearStageState(activeStage, lastStage, io);
|
|
311
|
+
const art = findCycleOutputArtefact(cycleId, io);
|
|
312
|
+
const blockNote = formatBlockNote(blockResult);
|
|
313
|
+
return violation(`subagent dispatch failed: ${lastResult.error || 'unknown error'}${blockNote}`, [art?.file].filter(Boolean));
|
|
314
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Foundry v2.3.0 orchestrate: deterministic cycle orchestration.
|
|
2
|
+
// Composes internal functions (sort, finalize, history, commit, configure)
|
|
3
|
+
// into a single entry point the LLM drives via a 3-line loop.
|
|
4
|
+
|
|
5
|
+
import { runSort } from './sort.js';
|
|
6
|
+
import { parseFrontmatter } from './lib/workfile.js';
|
|
7
|
+
import { readActiveStage, readLastStage } from './lib/state.js';
|
|
8
|
+
import { ulid as defaultUlid } from './lib/ulid.js';
|
|
9
|
+
import {
|
|
10
|
+
findCycleOutputArtefact,
|
|
11
|
+
readCycleTargets,
|
|
12
|
+
readForgeFilePatterns,
|
|
13
|
+
renderDispatchPrompt,
|
|
14
|
+
synthesizeStages,
|
|
15
|
+
violation,
|
|
16
|
+
computeOpenFeedback,
|
|
17
|
+
} from './orchestrate-cycle.js';
|
|
18
|
+
import {
|
|
19
|
+
handleSortResult,
|
|
20
|
+
setupWorkfile,
|
|
21
|
+
finaliseStage,
|
|
22
|
+
handleViolation,
|
|
23
|
+
} from './orchestrate-phases.js';
|
|
24
|
+
|
|
25
|
+
export { renderDispatchPrompt, synthesizeStages, computeOpenFeedback };
|
|
26
|
+
export { findCycleOutputArtefact, readCycleTargets, readForgeFilePatterns };
|
|
27
|
+
export { handleSortResult as __handleSortResultForTest };
|
|
28
|
+
|
|
29
|
+
export function needsSetup(workMdContent) {
|
|
30
|
+
const match = workMdContent.match(/^---\n([\s\S]*?)\n---/);
|
|
31
|
+
if (!match) return true;
|
|
32
|
+
const fm = match[1];
|
|
33
|
+
return !/^stages:/m.test(fm);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Main entry point
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function guardNoWorkMd(io) {
|
|
41
|
+
if (!io.exists('WORK.md')) {
|
|
42
|
+
return violation('no WORK.md; flow skill must create it first');
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function guardMissingCycleId(io) {
|
|
48
|
+
const workContent = io.readFile('WORK.md');
|
|
49
|
+
const fm = parseFrontmatter(workContent);
|
|
50
|
+
if (!fm.cycle) {
|
|
51
|
+
return violation('WORK.md frontmatter missing cycle field', ['WORK.md']);
|
|
52
|
+
}
|
|
53
|
+
return { cycleId: fm.cycle, workContent };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function guardSetupInconsistent(lastResult) {
|
|
57
|
+
if (lastResult) {
|
|
58
|
+
return violation(
|
|
59
|
+
'inconsistent state: lastResult provided but WORK.md still needs setup',
|
|
60
|
+
['WORK.md'],
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function guardOrphanedStage(activeStage, lastResult) {
|
|
67
|
+
if (activeStage && !lastResult) {
|
|
68
|
+
return violation(
|
|
69
|
+
`prior stage ${activeStage.stage} orphaned — no lastResult provided but active stage exists. ` +
|
|
70
|
+
`Likely cause: previous orchestrate call returned dispatch but caller did not follow up.`,
|
|
71
|
+
[],
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function guardMissingLastStage(lastStage) {
|
|
78
|
+
if (!lastStage) {
|
|
79
|
+
return violation('lastResult provided but no last stage recorded — orphaned state');
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildSortArgs(args, now) {
|
|
85
|
+
return {
|
|
86
|
+
cycleDef: args.cycleDef ?? null,
|
|
87
|
+
mint: args.mint,
|
|
88
|
+
now: typeof now === 'function' ? now() : now,
|
|
89
|
+
ulid: args.ulid ?? defaultUlid,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildSortContext(cycleId, args, io) {
|
|
94
|
+
return { cycleId, cwd: args.cwd ?? process.cwd(), io };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildSetupArgs(cycleResult, args, io) {
|
|
98
|
+
return {
|
|
99
|
+
cycleId: cycleResult.cycleId,
|
|
100
|
+
workContent: cycleResult.workContent,
|
|
101
|
+
io,
|
|
102
|
+
git: args.git,
|
|
103
|
+
foundryDir: 'foundry',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isViolation(result) {
|
|
108
|
+
return result && result.action === 'violation';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runOrchestrate(args, io) {
|
|
112
|
+
const preCheck = runPreChecks(io);
|
|
113
|
+
if (preCheck.error) return preCheck.error;
|
|
114
|
+
return runOrchestrateFlow(preCheck, args, io);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function runOrchestrateFlow(preCheck, args, io) {
|
|
118
|
+
const setupResult = await runSetupIfNeeded(preCheck, args, io);
|
|
119
|
+
if (isViolation(setupResult)) return setupResult;
|
|
120
|
+
|
|
121
|
+
const activeStage = readActiveStage(io);
|
|
122
|
+
const lastStage = readLastStage(io);
|
|
123
|
+
|
|
124
|
+
const orphanErr = guardOrphanedStage(activeStage, args.lastResult);
|
|
125
|
+
if (orphanErr) return orphanErr;
|
|
126
|
+
|
|
127
|
+
const postDispatchResult = await runPostDispatch(args, activeStage, lastStage, preCheck.cycleId, io);
|
|
128
|
+
if (isViolation(postDispatchResult)) return postDispatchResult;
|
|
129
|
+
|
|
130
|
+
const sortResult = runSort(buildSortArgs(args, args.now ?? Date.now), io);
|
|
131
|
+
return handleSortResult(sortResult, buildSortContext(preCheck.cycleId, args, io));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function runPreChecks(io) {
|
|
135
|
+
const noWork = guardNoWorkMd(io);
|
|
136
|
+
if (noWork) return { error: noWork };
|
|
137
|
+
const cycleResult = guardMissingCycleId(io);
|
|
138
|
+
if (cycleResult.error) return { error: cycleResult };
|
|
139
|
+
return cycleResult;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function runSetupIfNeeded(preCheck, args, io) {
|
|
143
|
+
if (!needsSetup(preCheck.workContent)) return null;
|
|
144
|
+
const err = guardSetupInconsistent(args.lastResult);
|
|
145
|
+
if (err) return err;
|
|
146
|
+
return setupWorkfile(buildSetupArgs(preCheck, args, io));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function runPostDispatch(args, activeStage, lastStage, cycleId, io) {
|
|
150
|
+
if (!args.lastResult) return null;
|
|
151
|
+
if (args.lastResult.ok === false) {
|
|
152
|
+
return handleViolation({ lastResult: args.lastResult, activeStage, lastStage, cycleId, io });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const stageErr = guardMissingLastStage(lastStage);
|
|
156
|
+
if (stageErr) return stageErr;
|
|
157
|
+
|
|
158
|
+
return finaliseStage({
|
|
159
|
+
lastStage, activeStage, cycleId, io,
|
|
160
|
+
finalize: args.finalize ?? null,
|
|
161
|
+
git: args.git,
|
|
162
|
+
});
|
|
163
|
+
}
|