@really-knows-ai/foundry 3.5.9 → 3.6.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/feedback-tools.js +9 -5
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +3 -1
- package/dist/CHANGELOG.md +32 -0
- package/dist/scripts/appraise-module.js +69 -7
- package/dist/scripts/lib/artefacts.js +43 -1
- package/dist/scripts/lib/feedback-store.js +34 -8
- package/dist/scripts/lib/finalize.js +10 -2
- package/dist/scripts/lib/forge-contract.js +102 -0
- package/dist/scripts/lib/history.js +2 -1
- package/dist/scripts/lib/sort-reason.js +3 -1
- package/dist/scripts/lib/sort-routing.js +172 -128
- package/dist/scripts/lib/workfile.js +28 -0
- package/dist/scripts/orchestrate-phases.js +51 -39
- package/dist/scripts/orchestrate-terminals.js +37 -2
- package/dist/scripts/orchestrate.js +62 -5
- package/dist/scripts/quench-module.js +54 -12
- package/dist/scripts/sort.js +28 -11
- package/package.json +1 -1
|
@@ -2,17 +2,10 @@
|
|
|
2
2
|
* Sort routing helpers — pure functions that decide the next stage given
|
|
3
3
|
* the current stage list, history, feedback, and iteration counters.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Replaced the handler-dispatch pattern with a state-driven decision tree
|
|
6
|
+
* that routes based on feedback item state, history, and the stage list.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
// An item is "open" (still in flight) when its head state is not
|
|
10
|
-
// 'resolved' or 'deadlocked'. The deadlocked check is retained for
|
|
11
|
-
// backward compatibility with existing feedback files.
|
|
12
|
-
const isOpenItem = (f) => f.state !== 'resolved' && f.state !== 'deadlocked';
|
|
13
|
-
|
|
14
|
-
export { isOpenItem };
|
|
15
|
-
|
|
16
9
|
export function baseStage(stage) {
|
|
17
10
|
return stage.split(':')[0];
|
|
18
11
|
}
|
|
@@ -24,7 +17,10 @@ export function findFirst(stages, base) {
|
|
|
24
17
|
return null;
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Returns the next stage in `stages` after `current`, or null if at the end.
|
|
22
|
+
*/
|
|
23
|
+
export function nextStageInChain(stages, current) {
|
|
28
24
|
const idx = stages.indexOf(current);
|
|
29
25
|
if (idx !== -1 && idx + 1 < stages.length) {
|
|
30
26
|
return stages[idx + 1];
|
|
@@ -32,150 +28,198 @@ export function nextInRoute(stages, current) {
|
|
|
32
28
|
return null;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Returns the first stage in `stages` whose base matches `base`, with
|
|
33
|
+
* a fallback for `human-appraise`. For `human-appraise`, if no exact
|
|
34
|
+
* match is found, falls back to `human-appraise:<cycle>`.
|
|
35
|
+
*/
|
|
36
|
+
export function first(base, stages, cycle) {
|
|
37
|
+
if (base === 'human-appraise') {
|
|
38
|
+
const stage = findFirst(stages, 'human-appraise');
|
|
39
|
+
return stage !== null ? stage : `human-appraise:${cycle}`;
|
|
40
|
+
}
|
|
41
|
+
return findFirst(stages, base);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Returns the first stage in `stages` whose base appears after `base`
|
|
46
|
+
* in the canonical chain order [forge, quench, appraise, human-appraise].
|
|
47
|
+
* Returns null if base is not in the canonical order or no subsequent
|
|
48
|
+
* stage is configured.
|
|
49
|
+
*/
|
|
50
|
+
export function firstAfter(stages, base) {
|
|
51
|
+
const canon = ['forge', 'quench', 'appraise', 'human-appraise'];
|
|
52
|
+
const baseIdx = canon.indexOf(base);
|
|
53
|
+
if (baseIdx === -1) return null;
|
|
54
|
+
for (let i = baseIdx + 1; i < canon.length; i++) {
|
|
55
|
+
const found = findFirst(stages, canon[i]);
|
|
56
|
+
if (found !== null) return found;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Returns true when a stage with the given base exists in `stages`.
|
|
63
|
+
*/
|
|
64
|
+
export function hasStage(stages, base) {
|
|
65
|
+
return findFirst(stages, base) !== null;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Called after all unresolved and addressed items have been exhausted.
|
|
70
|
+
* Finds the next stage after `lastStage` via `nextStageInChain`.
|
|
71
|
+
* Returns 'done' if no next stage exists.
|
|
72
|
+
* Returns 'done' if the next stage is human-appraise and
|
|
73
|
+
* opts.alwaysHumanAppraise is false.
|
|
74
|
+
* Otherwise returns the next stage.
|
|
75
|
+
*/
|
|
76
|
+
export function forwardClean(stages, lastStage, opts = {}) {
|
|
77
|
+
const next = nextStageInChain(stages, lastStage);
|
|
78
|
+
if (next === null) return 'done';
|
|
79
|
+
if (baseStage(next) === 'human-appraise' && !opts.alwaysHumanAppraise) {
|
|
80
|
+
return 'done';
|
|
81
|
+
}
|
|
82
|
+
return next;
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Helpers extracted to keep determineRoute within complexity limits
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validates maxIterations, stages list, and history emptiness.
|
|
91
|
+
* Returns a route value when the input is terminal, or null to continue.
|
|
92
|
+
*/
|
|
93
|
+
function isValidMaxIterations(maxIterations) {
|
|
94
|
+
return typeof maxIterations === 'number' && Number.isInteger(maxIterations) && maxIterations >= 1;
|
|
56
95
|
}
|
|
57
96
|
|
|
58
|
-
function
|
|
59
|
-
|
|
97
|
+
function validateRoute(maxIterations, stages, history) {
|
|
98
|
+
if (!isValidMaxIterations(maxIterations)) return 'blocked';
|
|
99
|
+
if (!stages || stages.length === 0) return 'blocked';
|
|
100
|
+
if (history.length === 0) return stages[0];
|
|
101
|
+
return null;
|
|
60
102
|
}
|
|
61
103
|
|
|
62
|
-
|
|
63
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Extracts routing state from history and feedback: the last non-sort
|
|
106
|
+
* stage entry (full alias), its base stage name, forge iteration count,
|
|
107
|
+
* and categorised feedback items.
|
|
108
|
+
*/
|
|
109
|
+
function computeRoutingState(history, feedback) {
|
|
110
|
+
const nonSort = history.filter(e => baseStage(e.stage || '') !== 'sort');
|
|
111
|
+
const lastEntry = nonSort.length > 0 ? nonSort[nonSort.length - 1].stage : null;
|
|
112
|
+
const lastStage = lastEntry !== null ? baseStage(lastEntry) : null;
|
|
113
|
+
const forgeCount = history.filter(e =>
|
|
114
|
+
baseStage(e.stage || '') === 'forge' && e.contract_passed !== false,
|
|
115
|
+
).length;
|
|
116
|
+
const unresolvedItems = feedback.filter(f => f.state === 'open' || f.state === 'rejected');
|
|
117
|
+
const addressedItems = feedback.filter(f => f.state === 'actioned' || f.state === 'wont-fix');
|
|
118
|
+
return { lastEntry, lastStage, forgeCount, unresolvedItems, addressedItems };
|
|
64
119
|
}
|
|
65
120
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Checks whether the iteration cap has been reached.
|
|
123
|
+
* When the cap is reached and not bypassed by alwaysHumanAppraise,
|
|
124
|
+
* routes to human-appraise (if deadlockHumanAppraise) or 'blocked'.
|
|
125
|
+
* Otherwise routes to forge via first('forge', stages).
|
|
126
|
+
*/
|
|
127
|
+
function checkIterationAndRoute(firstFn, stages, forgeCount, maxIterations, opts) {
|
|
128
|
+
if (forgeCount >= maxIterations && !opts.alwaysHumanAppraise) {
|
|
129
|
+
if (opts.deadlockHumanAppraise) {
|
|
130
|
+
return firstFn('human-appraise', stages, opts.cycle);
|
|
131
|
+
}
|
|
132
|
+
return 'blocked';
|
|
70
133
|
}
|
|
71
|
-
if (!
|
|
72
|
-
|
|
73
|
-
return `human-appraise:${cycle}`;
|
|
134
|
+
if (!hasStage(stages, 'forge')) return 'blocked';
|
|
135
|
+
return firstFn('forge', stages);
|
|
74
136
|
}
|
|
75
137
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Collects unique source bases from addressed items and finds the
|
|
140
|
+
* earliest stage in the canonical chain that has a matching configured
|
|
141
|
+
* stage. Returns the resolved stage alias when found, or null to
|
|
142
|
+
* fall through to forwardClean.
|
|
143
|
+
*/
|
|
144
|
+
function routeAddressedItems(addressedItems, stages, opts) {
|
|
145
|
+
const sourceBases = [...new Set(addressedItems.map(i => baseStage(i.source)))];
|
|
146
|
+
const chain = ['quench', 'appraise', 'human-appraise'];
|
|
147
|
+
for (const base of chain) {
|
|
148
|
+
if (sourceBases.includes(base) && hasStage(stages, base)) {
|
|
149
|
+
return first(base, stages, opts.cycle);
|
|
150
|
+
}
|
|
83
151
|
}
|
|
84
152
|
return null;
|
|
85
153
|
}
|
|
86
154
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const {
|
|
96
|
-
stages, current, openItems, forgeCount, maxIterations,
|
|
97
|
-
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
98
|
-
} = opts;
|
|
99
|
-
const decided = appraiseForgeOrApproval(
|
|
100
|
-
stages, openItems, forgeCount, maxIterations,
|
|
101
|
-
{ alwaysHumanAppraise, deadlockHumanAppraise, cycle },
|
|
102
|
-
);
|
|
103
|
-
if (decided !== null) return decided;
|
|
104
|
-
return nextRouteOrDone(stages, current);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function nextAfterAppraise({
|
|
108
|
-
stages, current, feedback, forgeCount, maxIterations,
|
|
109
|
-
alwaysHumanAppraise = false, deadlockHumanAppraise = false, cycle = 'default',
|
|
110
|
-
}) {
|
|
111
|
-
const openItems = feedback.filter(isOpenItem);
|
|
112
|
-
if (alwaysHumanAppraise) {
|
|
113
|
-
return routeAlwaysHumanAppraise(stages, current, openItems, cycle);
|
|
114
|
-
}
|
|
115
|
-
return decideAppraiseRoute({
|
|
116
|
-
stages, current, openItems, forgeCount, maxIterations,
|
|
117
|
-
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
118
|
-
});
|
|
155
|
+
/**
|
|
156
|
+
* Routes addressed items to their earliest source stage, falling through
|
|
157
|
+
* to forwardClean when no source matches or no addressed items exist.
|
|
158
|
+
*/
|
|
159
|
+
function routeAddressedOrForward(addressedItems, stages, lastEntry, opts) {
|
|
160
|
+
const routed = routeAddressedItems(addressedItems, stages, opts);
|
|
161
|
+
if (routed !== null) return routed;
|
|
162
|
+
return forwardClean(stages, lastEntry, opts);
|
|
119
163
|
}
|
|
120
164
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Handles the R7 case when the last non-sort history entry is forge.
|
|
167
|
+
* If unresolved items exist, delegates to checkIterationAndRoute.
|
|
168
|
+
* If clean, routes via firstAfter to the first evaluation stage,
|
|
169
|
+
* or 'done' if no stage follows forge.
|
|
170
|
+
*/
|
|
171
|
+
function handleForgeJustRan(stages, unresolvedItems, forgeCount, maxIterations, opts) {
|
|
172
|
+
if (unresolvedItems.length > 0) {
|
|
173
|
+
return checkIterationAndRoute(first, stages, forgeCount, maxIterations, opts);
|
|
174
|
+
}
|
|
175
|
+
const next = firstAfter(stages, 'forge');
|
|
176
|
+
return next !== null ? next : 'done';
|
|
127
177
|
}
|
|
128
178
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
142
|
-
});
|
|
143
|
-
return {
|
|
144
|
-
'assay': () => firstForgeOrBlocked(stages),
|
|
145
|
-
'forge': () => nextRouteOrDone(stages, lastEntry),
|
|
146
|
-
'quench': () => nextAfterQuench(
|
|
147
|
-
stages, lastEntry, feedback,
|
|
148
|
-
{ forgeCount, maxIterations, alwaysHumanAppraise, deadlockHumanAppraise, cycle },
|
|
149
|
-
),
|
|
150
|
-
'appraise': appraiseRoute,
|
|
151
|
-
'human-appraise': appraiseRoute,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function countForgeIterations(history) {
|
|
156
|
-
return history.filter(e => baseStage(e.stage || '') === 'forge').length;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function routeFromLastEntry(opts) {
|
|
160
|
-
const {
|
|
161
|
-
stages, lastEntry, feedback, forgeCount, maxIterations,
|
|
162
|
-
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
163
|
-
} = opts;
|
|
164
|
-
if (lastEntry === null) return stages[0];
|
|
165
|
-
const handlers = buildRouteHandlers({
|
|
166
|
-
stages, lastEntry, feedback, forgeCount, maxIterations,
|
|
167
|
-
alwaysHumanAppraise, deadlockHumanAppraise, cycle,
|
|
168
|
-
});
|
|
169
|
-
const handler = handlers[baseStage(lastEntry)];
|
|
170
|
-
return callHandlerOrBlocked(handler);
|
|
179
|
+
/**
|
|
180
|
+
* Handles R1 (unresolved, addressed) and R2 (forward) routing paths.
|
|
181
|
+
* Extracted to keep determineRoute within the complexity limit.
|
|
182
|
+
*/
|
|
183
|
+
function routeFeedbackState(state, stages, maxIterations, opts) {
|
|
184
|
+
if (state.unresolvedItems.length > 0) {
|
|
185
|
+
return checkIterationAndRoute(first, stages, state.forgeCount, maxIterations, opts);
|
|
186
|
+
}
|
|
187
|
+
if (state.addressedItems.length > 0) {
|
|
188
|
+
return routeAddressedOrForward(state.addressedItems, stages, state.lastEntry, opts);
|
|
189
|
+
}
|
|
190
|
+
return forwardClean(stages, state.lastEntry, opts);
|
|
171
191
|
}
|
|
172
192
|
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// determineRoute — state-driven decision tree
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Determines the next route given the current cycle state.
|
|
199
|
+
*
|
|
200
|
+
* The decision tree reads feedback item state, history, and the stage
|
|
201
|
+
* list to route to forge, an evaluation stage, human-appraise, 'done',
|
|
202
|
+
* or 'blocked'. It never calls computeArtefactVersion and does not
|
|
203
|
+
* read item.artefact_version.
|
|
204
|
+
*
|
|
205
|
+
* @param {string[]} stages - Configured stage aliases in chain order
|
|
206
|
+
* @param {object[]} history - Cycle history entries
|
|
207
|
+
* @param {object[]} feedback - Feedback items with state, source, etc.
|
|
208
|
+
* @param {number} maxIterations - Maximum forge iterations
|
|
209
|
+
* @param {object} [opts] - Options object
|
|
210
|
+
* @param {boolean} [opts.alwaysHumanAppraise=false]
|
|
211
|
+
* @param {boolean} [opts.deadlockHumanAppraise=false]
|
|
212
|
+
* @param {string} [opts.cycle='default']
|
|
213
|
+
* @returns {string} Next route: stage alias, 'done', or 'blocked'
|
|
214
|
+
*/
|
|
173
215
|
export function determineRoute(stages, history, feedback, maxIterations, opts = {}) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
216
|
+
const validation = validateRoute(maxIterations, stages, history);
|
|
217
|
+
if (validation !== null) return validation;
|
|
218
|
+
|
|
219
|
+
const state = computeRoutingState(history, feedback);
|
|
220
|
+
|
|
221
|
+
if (state.lastStage === null) return stages[0];
|
|
222
|
+
if (state.lastStage === 'forge') return handleForgeJustRan(stages, state.unresolvedItems, state.forgeCount, maxIterations, opts);
|
|
223
|
+
|
|
224
|
+
return routeFeedbackState(state, stages, maxIterations, opts);
|
|
181
225
|
}
|
|
@@ -121,3 +121,31 @@ export function createWorkfile(frontmatter, goal) {
|
|
|
121
121
|
${goal}
|
|
122
122
|
`;
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build a forge history entry options object for appendEntry.
|
|
127
|
+
*
|
|
128
|
+
* The returned object includes changedFiles (camelCase), artefact_version,
|
|
129
|
+
* and contract_passed. appendEntry/buildEntry handle sorting changedFiles,
|
|
130
|
+
* converting to changed_files in YAML, and setting timestamp/seq.
|
|
131
|
+
*
|
|
132
|
+
* @param {{ cycle: string, stage: string, iteration: number, comment: string,
|
|
133
|
+
* artefactVersion: string, contractPassed: boolean,
|
|
134
|
+
* changedFiles: string[] }} params
|
|
135
|
+
* @returns {{ cycle: string, stage: string, iteration: number,
|
|
136
|
+
* comment: string, changedFiles: string[], artefact_version: string,
|
|
137
|
+
* contract_passed: boolean }}
|
|
138
|
+
*/
|
|
139
|
+
export function buildForgeHistoryEntry(
|
|
140
|
+
{ cycle, stage, iteration, comment, artefactVersion, contractPassed, changedFiles },
|
|
141
|
+
) {
|
|
142
|
+
return {
|
|
143
|
+
cycle,
|
|
144
|
+
stage,
|
|
145
|
+
iteration,
|
|
146
|
+
comment,
|
|
147
|
+
changedFiles,
|
|
148
|
+
artefact_version: artefactVersion,
|
|
149
|
+
contract_passed: contractPassed,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -6,11 +6,12 @@ import {
|
|
|
6
6
|
getCycleDefinition,
|
|
7
7
|
getLawsForQuench,
|
|
8
8
|
} from './lib/config.js';
|
|
9
|
-
import { parseFrontmatter, writeFrontmatter } from './lib/workfile.js';
|
|
9
|
+
import { parseFrontmatter, writeFrontmatter, buildForgeHistoryEntry } from './lib/workfile.js';
|
|
10
10
|
import matter from 'gray-matter';
|
|
11
11
|
import { clearActiveStage, clearLastStage } from './lib/state.js';
|
|
12
12
|
import { appendEntry, getIteration } from './lib/history.js';
|
|
13
13
|
import { stageBaseOf } from './lib/stage-guard.js';
|
|
14
|
+
import { baseStage } from './lib/sort-routing.js';
|
|
14
15
|
import { allowedPatternsForStage } from './lib/git-policy.js';
|
|
15
16
|
import { loadExtractor } from './lib/assay/loader.js';
|
|
16
17
|
import { checkExtractorAgainstCycle } from './lib/assay/permissions.js';
|
|
@@ -66,13 +67,9 @@ function isTerminalRoute(route) {
|
|
|
66
67
|
return route === 'done' || route === 'blocked' || route === 'violation';
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
function getRouteBase(route) {
|
|
70
|
-
return routeDispatch(route);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
70
|
export async function handleSortResult(sortResult, ctx) {
|
|
74
71
|
const { route, model, token, reason } = sortResult;
|
|
75
|
-
const routeBase =
|
|
72
|
+
const routeBase = routeDispatch(route);
|
|
76
73
|
const result = await resolveRouteResult({ route, routeBase, model, token, ctx });
|
|
77
74
|
if (reason !== undefined) result.reason = reason;
|
|
78
75
|
return result;
|
|
@@ -85,10 +82,6 @@ async function resolveRouteResult({ route, routeBase, model, token, ctx }) {
|
|
|
85
82
|
return buildDispatchAction(route, model, token, ctx);
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
async function fetchCycleDefinition(foundryDir, cycleId, io) {
|
|
89
|
-
try { return await getCycleDefinition(foundryDir, cycleId, io); } catch { return null; }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
85
|
function checkOutputType(cfm, cycleId) {
|
|
93
86
|
const outputType = cfm['output-type'];
|
|
94
87
|
if (outputType) return { outputType };
|
|
@@ -103,7 +96,6 @@ async function checkArtefactType(foundryDir, outputType, io) {
|
|
|
103
96
|
catch { return { error: violation(`artefact type not found: ${outputType}`, ['WORK.md']) }; }
|
|
104
97
|
}
|
|
105
98
|
|
|
106
|
-
function isAssayAbsent(assayBlock) { return assayBlock === undefined || assayBlock === null; }
|
|
107
99
|
function isAssayInvalid(assayBlock) { return typeof assayBlock !== 'object' || Array.isArray(assayBlock); }
|
|
108
100
|
|
|
109
101
|
function checkAssayExtractors(list, cycleId) {
|
|
@@ -115,7 +107,7 @@ function checkAssayExtractors(list, cycleId) {
|
|
|
115
107
|
|
|
116
108
|
function checkAssayShape(cfm, cycleId) {
|
|
117
109
|
const assayBlock = cfm.assay;
|
|
118
|
-
if (
|
|
110
|
+
if (assayBlock === undefined || assayBlock === null) return { ok: true, extractors: null };
|
|
119
111
|
if (isAssayInvalid(assayBlock)) {
|
|
120
112
|
return { error: violation(`cycle ${cycleId}: 'assay' must be a mapping`, ['WORK.md']) };
|
|
121
113
|
}
|
|
@@ -124,13 +116,6 @@ function checkAssayShape(cfm, cycleId) {
|
|
|
124
116
|
return { ok: true, extractors: assayBlock.extractors };
|
|
125
117
|
}
|
|
126
118
|
|
|
127
|
-
function checkMemoryEnabled(io, cycleId) {
|
|
128
|
-
if (!io.exists('foundry/memory/config.md')) {
|
|
129
|
-
return violation(`cycle ${cycleId}: 'assay:' requires memory to be enabled (run the init-memory skill first)`, ['WORK.md']);
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
119
|
function checkCycleWriteDecl(cfm, cycleId) {
|
|
135
120
|
const cycleWrite = cfm.memory?.write;
|
|
136
121
|
if (!Array.isArray(cycleWrite)) {
|
|
@@ -150,10 +135,6 @@ async function checkExtractors(foundryDir, cycleId, list, cycleWriteSet, io) {
|
|
|
150
135
|
return null;
|
|
151
136
|
}
|
|
152
137
|
|
|
153
|
-
function stageTag(s, cycleId) {
|
|
154
|
-
return typeof s === 'string' && s.includes(':') ? s : `${s}:${cycleId}`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
138
|
function resolveStages(cfm, cycleId, hasValidation, assayExtractors) {
|
|
158
139
|
if (!Array.isArray(cfm.stages)) {
|
|
159
140
|
return synthesizeStages({ cycleId, hasValidation, alwaysHumanAppraise: cfm['always-human-appraise'] === true, assay: !!assayExtractors });
|
|
@@ -161,14 +142,14 @@ function resolveStages(cfm, cycleId, hasValidation, assayExtractors) {
|
|
|
161
142
|
if (cfm.stages.length === 0) {
|
|
162
143
|
return { error: violation(`cycle ${cycleId} has no stages declared in cycle definition`, ['WORK.md']) };
|
|
163
144
|
}
|
|
164
|
-
return cfm.stages.map(s =>
|
|
145
|
+
return cfm.stages.map(s => typeof s === 'string' && s.includes(':') ? s : `${s}:${cycleId}`);
|
|
165
146
|
}
|
|
166
147
|
|
|
167
|
-
function applyFmDefaults(newFm, cfm, assayExtractors) {
|
|
148
|
+
export function applyFmDefaults(newFm, cfm, assayExtractors) {
|
|
168
149
|
const maxIt = cfm['max-iterations'] ?? 3;
|
|
169
150
|
newFm['max-iterations'] = maxIt;
|
|
170
151
|
newFm['always-human-appraise'] = cfm['always-human-appraise'] === true;
|
|
171
|
-
newFm['deadlock-human-appraise'] = cfm['deadlock-human-appraise']
|
|
152
|
+
newFm['deadlock-human-appraise'] = cfm['deadlock-human-appraise'] === true;
|
|
172
153
|
if (cfm.models) newFm.models = cfm.models;
|
|
173
154
|
if (assayExtractors) newFm.assay = { extractors: assayExtractors };
|
|
174
155
|
}
|
|
@@ -184,8 +165,9 @@ function buildNewFrontmatter(workContent, stages, cfm, assayExtractors) {
|
|
|
184
165
|
}
|
|
185
166
|
|
|
186
167
|
async function checkAssayPrereqs(cfm, cycleId, io) {
|
|
187
|
-
|
|
188
|
-
|
|
168
|
+
if (!io.exists('foundry/memory/config.md')) {
|
|
169
|
+
return violation(`cycle ${cycleId}: 'assay:' requires memory to be enabled (run the init-memory skill first)`, ['WORK.md']);
|
|
170
|
+
}
|
|
189
171
|
return checkCycleWriteDecl(cfm, cycleId);
|
|
190
172
|
}
|
|
191
173
|
|
|
@@ -203,7 +185,7 @@ async function runAssayValidation(cfm, cycleId, io, foundryDir) {
|
|
|
203
185
|
|
|
204
186
|
export async function setupWorkfile(args) {
|
|
205
187
|
const { cycleId, workContent, io, git, foundryDir } = args;
|
|
206
|
-
const cycleDefDoc = await
|
|
188
|
+
const cycleDefDoc = await getCycleDefinition(foundryDir, cycleId, io).catch(() => null);
|
|
207
189
|
if (!cycleDefDoc) return violation(`cycle definition not found for id: ${cycleId}`, ['WORK.md']);
|
|
208
190
|
const cfm = cycleDefDoc.frontmatter || {};
|
|
209
191
|
return runSetupPipeline({ cfm, cycleId, workContent, io, git, foundryDir });
|
|
@@ -238,10 +220,6 @@ async function trySetupCommit(ctx) {
|
|
|
238
220
|
return { ok: true, workContent: ctx.io.readFile('WORK.md') };
|
|
239
221
|
}
|
|
240
222
|
|
|
241
|
-
function readOriginalState(io) {
|
|
242
|
-
return { workMd: io.readFile('WORK.md'), history: io.exists('WORK.history.yaml') ? io.readFile('WORK.history.yaml') : null };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
223
|
function buildFinalizeViolation(finalizeResult) {
|
|
246
224
|
if (finalizeResult.error === 'unexpected_files') {
|
|
247
225
|
return violation(`unexpected files written by subagent: ${(finalizeResult.files || []).join(', ')}`, finalizeResult.files || []);
|
|
@@ -249,9 +227,32 @@ function buildFinalizeViolation(finalizeResult) {
|
|
|
249
227
|
return violation(`stage_finalize error: ${finalizeResult.error}`, []);
|
|
250
228
|
}
|
|
251
229
|
|
|
230
|
+
function buildStageEntryBase(ctx) {
|
|
231
|
+
const summary = ctx.lastStage.summary || '(no summary)';
|
|
232
|
+
const changed = ctx.lastStage.changedFiles ?? [];
|
|
233
|
+
return { cycle: ctx.cycleId, stage: ctx.lastStage.stage,
|
|
234
|
+
iteration: ctx.iteration, comment: summary,
|
|
235
|
+
openFeedback: ctx.openFeedback, changedFiles: changed,
|
|
236
|
+
...(baseStage(ctx.lastStage.stage || '') === 'forge'
|
|
237
|
+
? buildForgeHistoryEntry({
|
|
238
|
+
cycle: ctx.cycleId, stage: ctx.lastStage.stage,
|
|
239
|
+
iteration: ctx.iteration, comment: summary,
|
|
240
|
+
artefactVersion: ctx.artefactVersion,
|
|
241
|
+
contractPassed: ctx.contractPassed,
|
|
242
|
+
changedFiles: changed,
|
|
243
|
+
})
|
|
244
|
+
: {}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
252
248
|
function writeHistoryEntries(ctx) {
|
|
253
|
-
appendEntry(ctx.historyPath, {
|
|
254
|
-
|
|
249
|
+
appendEntry(ctx.historyPath, {
|
|
250
|
+
cycle: ctx.cycleId, stage: 'sort', iteration: ctx.iteration,
|
|
251
|
+
route: ctx.lastStage.stage,
|
|
252
|
+
comment: `route ${ctx.lastStage.stage}`,
|
|
253
|
+
openFeedback: ctx.openFeedback,
|
|
254
|
+
}, ctx.io);
|
|
255
|
+
appendEntry(ctx.historyPath, buildStageEntryBase(ctx), ctx.io);
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
async function computeAllowedPatterns(lastStage, cycleId, io) {
|
|
@@ -286,12 +287,18 @@ function clearStageState(activeStage, lastStage, io) {
|
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
export async function finaliseStage(args) {
|
|
289
|
-
const { lastStage, activeStage, cycleId, io, finalize, git } = args;
|
|
290
|
-
const original =
|
|
290
|
+
const { lastStage, activeStage, cycleId, io, finalize, git, postVersion, contractPassed } = args;
|
|
291
|
+
const original = {
|
|
292
|
+
workMd: io.readFile('WORK.md'),
|
|
293
|
+
history: io.exists('WORK.history.yaml') ? io.readFile('WORK.history.yaml') : null,
|
|
294
|
+
};
|
|
291
295
|
if (typeof finalize !== 'function') {
|
|
292
296
|
return violation('orchestrate caller must inject a `finalize` function when providing lastResult; the plugin wires lib/finalize.finalizeStage; tests must pass a stub.', []);
|
|
293
297
|
}
|
|
294
|
-
const finalizeResult = await finalize({
|
|
298
|
+
const finalizeResult = await finalize({
|
|
299
|
+
cycleId, stage: lastStage.stage, baseSha: lastStage.baseSha, io,
|
|
300
|
+
artefact_version: postVersion, contractPassed,
|
|
301
|
+
});
|
|
295
302
|
if (!finalizeResult.ok) {
|
|
296
303
|
clearStageState(activeStage, null, io);
|
|
297
304
|
return buildFinalizeViolation(finalizeResult);
|
|
@@ -299,7 +306,12 @@ export async function finaliseStage(args) {
|
|
|
299
306
|
const historyPath = 'WORK.history.yaml';
|
|
300
307
|
const iteration = getIteration(historyPath, cycleId, io);
|
|
301
308
|
const openFeedback = computeOpenFeedback(io);
|
|
302
|
-
writeHistoryEntries({
|
|
309
|
+
writeHistoryEntries({
|
|
310
|
+
historyPath, cycleId,
|
|
311
|
+
lastStage: { ...lastStage, changedFiles: finalizeResult.changedFiles },
|
|
312
|
+
iteration, openFeedback, io,
|
|
313
|
+
artefactVersion: postVersion, contractPassed,
|
|
314
|
+
});
|
|
303
315
|
const commitErr = await tryStageCommit(git, lastStage, cycleId, io);
|
|
304
316
|
if (commitErr) {
|
|
305
317
|
rollbackState(io, original);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getCycleDefinition } from './lib/config.js';
|
|
2
|
-
import { getArtefactFiles } from './lib/artefacts.js';
|
|
2
|
+
import { getArtefactFiles, computeArtefactVersion } from './lib/artefacts.js';
|
|
3
3
|
import { readCycleTargets, readRecentFeedback, violation } from './orchestrate-cycle.js';
|
|
4
|
+
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
5
|
+
import { baseStage } from './lib/sort-routing.js';
|
|
4
6
|
|
|
5
7
|
async function findOutputArtefacts(cfm, io, foundryDir, baseBranch) {
|
|
6
8
|
const outputType = cfm ? cfm['output-type'] : undefined;
|
|
@@ -29,15 +31,48 @@ export async function blockedAction(cycleId, io, details, foundryDir, baseBranch
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export async function humanAppraiseAction(route, token, ctx) {
|
|
32
|
-
const { cycleId, io, baseBranch } = ctx;
|
|
34
|
+
const { cycleId, io, baseBranch, cwd } = ctx;
|
|
33
35
|
const fd = ctx.foundryDir || 'foundry';
|
|
34
36
|
const base = baseBranch || 'main';
|
|
35
37
|
const cfm = (await getCycleDefinition(fd, cycleId, io)).frontmatter;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await resolveStaleHumanAppraiseFeedback(cfm, fd, io, cycleId, cwd);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return { action: 'violation', details: `version check failed: ${err.message}`, recoverable: false, affected_files: [] };
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
const artefact = await findOutputArtefacts(cfm, io, fd, base);
|
|
37
46
|
const artefactFile = artefact ? artefact.file : null;
|
|
38
47
|
return { action: 'human_appraise', stage: route, token, context: { cycle: cycleId, artefact_file: artefactFile, recent_feedback: readRecentFeedback(io) } };
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Resolve stale human-appraise feedback. Errors propagate to the caller
|
|
52
|
+
* (humanAppraiseAction) which surfaces them as a violation.
|
|
53
|
+
*/
|
|
54
|
+
async function resolveStaleHumanAppraiseFeedback(cfm, fd, io, cycleId, cwd) {
|
|
55
|
+
const outputType = cfm['output-type'];
|
|
56
|
+
if (!outputType) return;
|
|
57
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
58
|
+
const currentVersion = await computeArtefactVersion(fd, outputType, io, cwd);
|
|
59
|
+
for (const item of store.list()) {
|
|
60
|
+
if (shouldSkipHumanAppraiseResolve(item, currentVersion)) continue;
|
|
61
|
+
store.autoResolve({
|
|
62
|
+
id: item.id,
|
|
63
|
+
reason: `superseded by forge revision ${currentVersion}`,
|
|
64
|
+
cycle: cycleId,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shouldSkipHumanAppraiseResolve(item, currentVersion) {
|
|
70
|
+
if (item.history[0].state === 'resolved') return true;
|
|
71
|
+
if (baseStage(item.source) !== 'human-appraise') return true;
|
|
72
|
+
if (item.artefact_version === currentVersion) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
41
76
|
export async function missingModelViolation(cycleId, route, io, foundryDir, baseBranch) {
|
|
42
77
|
const fd = foundryDir || 'foundry';
|
|
43
78
|
const base = baseBranch || 'main';
|