@farmslot/agent-runtime 0.20.1 → 0.21.0
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/CHANGELOG.md +5 -0
- package/bin/farmslot-agent.mjs +1 -1
- package/package.json +4 -3
- package/scripts/acceptance-cli.cjs +12 -40
- package/scripts/acceptance-ledger.cjs +20 -82
- package/scripts/check-task-artifact-contract.mjs +2 -11
- package/scripts/mark-checklist-step.cjs +9 -1
- package/scripts/worker-terminal-contract.cjs +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
- Active-development baseline; add user-facing changes here before release or package publication.
|
|
6
6
|
|
|
7
|
+
## 0.21.0 - 2026-10-14
|
|
8
|
+
|
|
9
|
+
- **BREAKING:** Make acceptance commands read-only, transfer only schema-2 engine claims, and refuse retained schema-1 verdicts for current completion.
|
|
10
|
+
- Resolve completion's evidence-manifest requirement through the selected engine attempt, matching artifact validation without a root copy or symlink.
|
|
11
|
+
|
|
7
12
|
## 0.20.1 - 2026-10-14
|
|
8
13
|
|
|
9
14
|
- Use the coordinated recipe release for content-based dependency readiness and engine-owned evidence manifests.
|
package/bin/farmslot-agent.mjs
CHANGED
|
@@ -16,7 +16,7 @@ function usage(exitCode = 0) {
|
|
|
16
16
|
' mark <task-md> <signal-json> <args...>',
|
|
17
17
|
' mark <task-dir> sub <start <id> --step N --from src | <id> <n> | <id> complete | <id> blocked | <id> status>',
|
|
18
18
|
' artifact-check <task-dir> [args...]',
|
|
19
|
-
' ac <
|
|
19
|
+
' ac <list | render> [--task-dir path]',
|
|
20
20
|
' task init <task-dir> --flow f --platform p --template id --title t [--run-mode m] [options]',
|
|
21
21
|
' recipe-quality build [--input input.json] [--output artifacts/recipe-quality.json]',
|
|
22
22
|
' (flags override top-level input fields; training fields are merged)',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farmslot/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"typecheck": "yarn workspace @farmslot/protocol build && tsc --noEmit --project tsconfig.json",
|
|
41
|
-
"test": "node --test test/claim-review-gate.test.mjs test/deterministic-checks.test.cjs && node test/mark-checklist-step.test.cjs && node test/mark-subtask.test.cjs && node test/acceptance-ledger.test.cjs && node test/checklist-target-sync.test.mjs && node test/acceptance-ledger-sync.test.mjs && node test/package-exports.test.cjs && node test/check-task-artifact-contract.test.cjs && node test/recipe-quality-builder.test.cjs && node test/task-init.test.mjs && node --import tsx ../../scripts/e2e-subtask-mark.mts && node
|
|
41
|
+
"test": "node --test test/claim-review-gate.test.mjs test/deterministic-checks.test.cjs && node test/mark-checklist-step.test.cjs && node test/mark-subtask.test.cjs && node --test test/acceptance-ledger.test.cjs test/acceptance-ledger-cutover.test.cjs && node test/checklist-target-sync.test.mjs && node test/acceptance-ledger-sync.test.mjs && node test/package-exports.test.cjs && node test/check-task-artifact-contract.test.cjs && node test/recipe-quality-builder.test.cjs && node test/task-init.test.mjs && node --import tsx ../../scripts/e2e-subtask-mark.mts && node ../../scripts/quality/run-tsx-tests.mjs --cwd . --tsconfig tsconfig.json src/execution-template && node ../../scripts/quality/run-tsx-tests.mjs --cwd . --tsconfig tsconfig.json src/task-init && node ../../scripts/quality/run-tsx-tests.mjs --cwd . --tsconfig tsconfig.json src/native",
|
|
42
42
|
"format": "prettier --write --ignore-path ../../.prettierignore .",
|
|
43
43
|
"format:check": "prettier --check --ignore-path ../../.prettierignore .",
|
|
44
44
|
"lint": "eslint \"src/**/*.ts\"",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"quality": "prettier --check --ignore-path ../../.prettierignore . && eslint \"src/**/*.ts\" && yarn typecheck && yarn build && yarn test",
|
|
47
47
|
"build": "yarn workspace @farmslot/protocol build && rm -rf dist && tsc --project tsconfig.build.json",
|
|
48
48
|
"prepack": "cd ../.. && node scripts/quality/check-farmslot-package-readiness.mjs --require-changelog --packages @farmslot/agent-runtime && yarn workspace @farmslot/agent-runtime build",
|
|
49
|
-
"prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/agent-runtime"
|
|
49
|
+
"prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/agent-runtime",
|
|
50
|
+
"test:acceptance-live": "node --import tsx ../../scripts/e2e-acceptance-ledger.mts"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@farmslot/adapter-sdk": "0.12.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `farmslot-agent ac`:
|
|
4
|
-
* (`artifacts/acceptance-status.json`)
|
|
3
|
+
* `farmslot-agent ac`: read the engine-owned acceptance-criteria projection
|
|
4
|
+
* (`artifacts/acceptance-status.json`). Retained schema-1 data is display only.
|
|
5
5
|
*
|
|
6
6
|
* The ids come from `inputs/handoff.json`, which task init fills from the
|
|
7
7
|
* criteria it received; this CLI never invents an id or a criterion text.
|
|
@@ -10,14 +10,11 @@ const fs = require('node:fs');
|
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
|
|
12
12
|
const {
|
|
13
|
-
ACCEPTANCE_PROOF_MODES,
|
|
14
|
-
ACCEPTANCE_VERDICTS,
|
|
15
13
|
AcceptanceRefusal,
|
|
16
14
|
acceptanceStatusList,
|
|
17
15
|
handoffAcceptanceCriteria,
|
|
18
16
|
renderAcceptanceCoverage,
|
|
19
17
|
readAcceptanceLedger,
|
|
20
|
-
setAcceptanceVerdict,
|
|
21
18
|
} = require('./acceptance-ledger.cjs');
|
|
22
19
|
|
|
23
20
|
const HANDOFF_INPUT = path.join('inputs', 'handoff.json');
|
|
@@ -25,12 +22,6 @@ const HANDOFF_INPUT = path.join('inputs', 'handoff.json');
|
|
|
25
22
|
const USAGE = [
|
|
26
23
|
'usage: farmslot-agent ac <command> [--task-dir <path>]',
|
|
27
24
|
'',
|
|
28
|
-
` ac set <AC-N> <${ACCEPTANCE_VERDICTS.join('|')}> [options]`,
|
|
29
|
-
` --proof-mode <${ACCEPTANCE_PROOF_MODES.join('|')}> how the criterion is proven`,
|
|
30
|
-
' --evidence <task-dir relative path> repeatable; the file must exist',
|
|
31
|
-
' --recipe-node <id> repeatable; recipe node proving it',
|
|
32
|
-
' --actor <identity> required for an automatic-ledger override',
|
|
33
|
-
' --note "..." why, especially for untestable',
|
|
34
25
|
' ac list every criterion with its current verdict (JSON, verdict null when unrecorded)',
|
|
35
26
|
' ac render the coverage table, ending with the Overall recipe coverage line',
|
|
36
27
|
'',
|
|
@@ -52,20 +43,10 @@ function parseArgs(argv) {
|
|
|
52
43
|
const opts = {
|
|
53
44
|
taskDir: null,
|
|
54
45
|
positional: [],
|
|
55
|
-
proofMode: undefined,
|
|
56
|
-
evidence: [],
|
|
57
|
-
recipeNodes: [],
|
|
58
|
-
note: undefined,
|
|
59
|
-
actor: undefined,
|
|
60
46
|
};
|
|
61
47
|
for (let i = 0; i < argv.length; i += 1) {
|
|
62
48
|
const arg = argv[i];
|
|
63
49
|
if (arg === '--task-dir') opts.taskDir = takeValue(argv, i++, arg);
|
|
64
|
-
else if (arg === '--proof-mode') opts.proofMode = takeValue(argv, i++, arg);
|
|
65
|
-
else if (arg === '--evidence') opts.evidence.push(takeValue(argv, i++, arg));
|
|
66
|
-
else if (arg === '--recipe-node') opts.recipeNodes.push(takeValue(argv, i++, arg));
|
|
67
|
-
else if (arg === '--actor') opts.actor = takeValue(argv, i++, arg);
|
|
68
|
-
else if (arg === '--note') opts.note = takeValue(argv, i++, arg);
|
|
69
50
|
else if (arg === '-h' || arg === '--help') usage(0);
|
|
70
51
|
else if (arg.startsWith('-')) throw new AcceptanceRefusal(`unknown option ${arg}`, 2);
|
|
71
52
|
else opts.positional.push(arg);
|
|
@@ -86,27 +67,14 @@ function resolveTaskDir(explicit) {
|
|
|
86
67
|
function main(argv) {
|
|
87
68
|
const [command, ...rest] = argv;
|
|
88
69
|
if (!command || command === '-h' || command === '--help') usage(command ? 0 : 2);
|
|
70
|
+
if (command === 'set') {
|
|
71
|
+
throw new AcceptanceRefusal(
|
|
72
|
+
'Verdicts are engine-owned. Run eligible recipe assertions; a verified person uses farmslot run gate --action mark-untestable for an exception.',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
89
75
|
const opts = parseArgs(rest);
|
|
90
76
|
const taskDir = resolveTaskDir(opts.taskDir);
|
|
91
77
|
|
|
92
|
-
if (command === 'set') {
|
|
93
|
-
const [id, verdict, ...extra] = opts.positional;
|
|
94
|
-
if (!id || !verdict || extra.length > 0) {
|
|
95
|
-
throw new AcceptanceRefusal('usage: ac set <AC-N> <verdict> [options]', 2);
|
|
96
|
-
}
|
|
97
|
-
const ledger = setAcceptanceVerdict(taskDir, {
|
|
98
|
-
id,
|
|
99
|
-
verdict,
|
|
100
|
-
proofMode: opts.proofMode,
|
|
101
|
-
evidence: opts.evidence,
|
|
102
|
-
recipeNodes: opts.recipeNodes,
|
|
103
|
-
note: opts.note,
|
|
104
|
-
actor: opts.actor,
|
|
105
|
-
});
|
|
106
|
-
const entry = ledger.criteria.find((criterion) => criterion.id === id);
|
|
107
|
-
console.log(`${id}: ${entry.verdict}${entry.proofMode ? ` (${entry.proofMode})` : ''}`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
78
|
if (opts.positional.length > 0) {
|
|
111
79
|
throw new AcceptanceRefusal(`ac ${command} takes no positional arguments`, 2);
|
|
112
80
|
}
|
|
@@ -118,11 +86,15 @@ function main(argv) {
|
|
|
118
86
|
const ledger = readAcceptanceLedger(taskDir);
|
|
119
87
|
if (!ledger) {
|
|
120
88
|
throw new AcceptanceRefusal(
|
|
121
|
-
'no acceptance ledger yet
|
|
89
|
+
'no acceptance ledger yet. Run the task recipe to derive engine verdicts.',
|
|
122
90
|
);
|
|
123
91
|
}
|
|
124
92
|
// The registered criteria, not just the recorded rows: a partial ledger must
|
|
125
93
|
// not report 1/1 when the task has three criteria.
|
|
94
|
+
if (ledger.schemaVersion === 1)
|
|
95
|
+
process.stdout.write(
|
|
96
|
+
'Legacy schema-1 ledger: retained display only; no measured recipe proof.\n\n',
|
|
97
|
+
);
|
|
126
98
|
process.stdout.write(renderAcceptanceCoverage(ledger, handoffAcceptanceCriteria(taskDir)));
|
|
127
99
|
return;
|
|
128
100
|
}
|
|
@@ -6,11 +6,10 @@ const { atomicWrite, readJson } = require('./mark-io.cjs');
|
|
|
6
6
|
// Acceptance-criteria ledger (ADR-060; contract in docs/reference/agent-runtime.md
|
|
7
7
|
// and docs/reference/task-directory-contract.md).
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// inputs/handoff.json; this module never invents one.
|
|
9
|
+
// The engine derives schema-2 verdicts. This module transfers the selected engine
|
|
10
|
+
// ledger into the task namespace; `farmslot-agent ac` only reads it. Retained
|
|
11
|
+
// schema-1 ledgers remain display input, never current completion proof. The ids
|
|
12
|
+
// come from task init in inputs/handoff.json; this module never invents one.
|
|
14
13
|
//
|
|
15
14
|
// Portable contract helpers are shared with protocol clients, including unbuilt slot scripts.
|
|
16
15
|
|
|
@@ -114,6 +113,11 @@ function readAcceptanceLedger(taskDir) {
|
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
function writeAcceptanceLedger(taskDir, ledger) {
|
|
116
|
+
if (ledger?.schemaVersion !== 2) {
|
|
117
|
+
throw new AcceptanceRefusal(
|
|
118
|
+
'Only schema-2 engine claims may be written; schema-1 ledgers are retained display only.',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
117
121
|
const issues = validateAcceptanceStatusLedger(ledger);
|
|
118
122
|
if (issues.length > 0) {
|
|
119
123
|
throw new AcceptanceRefusal(
|
|
@@ -138,70 +142,6 @@ function assertEvidencePath(taskDir, rawPath) {
|
|
|
138
142
|
return normalized;
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
/**
|
|
142
|
-
* Record one verdict. The criterion must be one task init registered: the ledger
|
|
143
|
-
* carries exactly the handoff ids, never an id the worker made up.
|
|
144
|
-
*/
|
|
145
|
-
function setAcceptanceVerdict(taskDir, input) {
|
|
146
|
-
const criteria = handoffAcceptanceCriteria(taskDir);
|
|
147
|
-
if (criteria.length === 0) {
|
|
148
|
-
throw new AcceptanceRefusal(
|
|
149
|
-
`${HANDOFF_INPUT} lists no acceptance criteria; there is no ledger to write`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
const criterion = criteria.find((entry) => entry.id === input.id);
|
|
153
|
-
if (!criterion) {
|
|
154
|
-
throw new AcceptanceRefusal(
|
|
155
|
-
`unknown acceptance criterion ${input.id}; ${HANDOFF_INPUT} lists ${criteria
|
|
156
|
-
.map((entry) => entry.id)
|
|
157
|
-
.join(', ')}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if (!ACCEPTANCE_VERDICTS.includes(input.verdict)) {
|
|
161
|
-
throw new AcceptanceRefusal(
|
|
162
|
-
`unknown verdict ${input.verdict}; expected one of ${ACCEPTANCE_VERDICTS.join(', ')}`,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
if (input.proofMode !== undefined && !ACCEPTANCE_PROOF_MODES.includes(input.proofMode)) {
|
|
166
|
-
throw new AcceptanceRefusal(
|
|
167
|
-
`unknown proof mode ${input.proofMode}; expected one of ${ACCEPTANCE_PROOF_MODES.join(', ')}`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
const evidence = (input.evidence ?? []).map((entry) => assertEvidencePath(taskDir, entry));
|
|
171
|
-
const stored = readAcceptanceLedger(taskDir);
|
|
172
|
-
if (stored) {
|
|
173
|
-
const issues = validateAcceptanceStatusLedger(stored);
|
|
174
|
-
if (issues.length > 0) {
|
|
175
|
-
throw new AcceptanceRefusal(
|
|
176
|
-
`${ACCEPTANCE_STATUS_ARTIFACT} does not match the ledger contract:\n- ${issues.join('\n- ')}`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (stored?.schemaVersion === 2) {
|
|
181
|
-
throw new AcceptanceRefusal(
|
|
182
|
-
'Automatic verdicts are engine-owned. Request human review; a verified person uses farmslot run gate --action mark-untestable.',
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
const byId = new Map((stored?.criteria ?? []).map((entry) => [entry.id, entry]));
|
|
186
|
-
byId.set(criterion.id, {
|
|
187
|
-
id: criterion.id,
|
|
188
|
-
text: criterion.text,
|
|
189
|
-
verdict: input.verdict,
|
|
190
|
-
...(input.proofMode ? { proofMode: input.proofMode } : {}),
|
|
191
|
-
evidence,
|
|
192
|
-
recipeNodes: [...(input.recipeNodes ?? [])],
|
|
193
|
-
...(input.note ? { note: input.note } : {}),
|
|
194
|
-
updatedAt: input.now ?? new Date().toISOString(),
|
|
195
|
-
});
|
|
196
|
-
// Handoff order, so the ledger and the rendered table always read like TASK.md.
|
|
197
|
-
const ledger = {
|
|
198
|
-
schemaVersion: 1,
|
|
199
|
-
criteria: criteria.map((entry) => byId.get(entry.id)).filter(Boolean),
|
|
200
|
-
};
|
|
201
|
-
writeAcceptanceLedger(taskDir, ledger);
|
|
202
|
-
return ledger;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
145
|
/** Transfer one engine-owned execution. Never aggregate sibling attempts. */
|
|
206
146
|
function setDerivedClaims(taskDir, claims) {
|
|
207
147
|
if (claims.schemaVersion !== 2) throw new AcceptanceRefusal('Derived claims require schema 2.');
|
|
@@ -268,24 +208,26 @@ function acceptanceContractIssues(taskDir, options = {}) {
|
|
|
268
208
|
if (!stored) {
|
|
269
209
|
return [
|
|
270
210
|
`${ACCEPTANCE_STATUS_ARTIFACT} is missing but ${HANDOFF_INPUT} lists ${criteria.length} ` +
|
|
271
|
-
'acceptance criteria
|
|
211
|
+
'acceptance criteria. Run the task recipe with an eligible assertion for each criterion.',
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
if (stored.schemaVersion !== 2) {
|
|
215
|
+
return [
|
|
216
|
+
`${ACCEPTANCE_STATUS_ARTIFACT}: legacy schema-1 verdicts are display only. Run the task recipe to obtain schema-2 engine claims.`,
|
|
272
217
|
];
|
|
273
218
|
}
|
|
274
219
|
const issues = validateAcceptanceStatusLedger(stored);
|
|
275
220
|
if (issues.length > 0) return issues;
|
|
276
221
|
const known = new Set(criteria.map((criterion) => criterion.id));
|
|
277
222
|
for (const entry of stored.criteria) {
|
|
278
|
-
if (
|
|
279
|
-
!known.has(entry.id) &&
|
|
280
|
-
(stored.schemaVersion === 1 || ACCEPTANCE_CRITERION_ID_PATTERN.test(entry.id))
|
|
281
|
-
) {
|
|
223
|
+
if (!known.has(entry.id) && ACCEPTANCE_CRITERION_ID_PATTERN.test(entry.id)) {
|
|
282
224
|
issues.push(
|
|
283
225
|
`${ACCEPTANCE_STATUS_ARTIFACT}: ${entry.id} is not an acceptance criterion of this task`,
|
|
284
226
|
);
|
|
285
227
|
}
|
|
286
228
|
}
|
|
287
229
|
const byId = new Map(stored.criteria.map((entry) => [entry.id, entry]));
|
|
288
|
-
if (stored.
|
|
230
|
+
if (stored.criteria.some((entry) => entry.overrides !== undefined))
|
|
289
231
|
issues.push(
|
|
290
232
|
'Unverified local overrides require a canonical engine projection; they grant no review authority.',
|
|
291
233
|
);
|
|
@@ -293,21 +235,18 @@ function acceptanceContractIssues(taskDir, options = {}) {
|
|
|
293
235
|
const entry = byId.get(criterion.id);
|
|
294
236
|
if (!entry) {
|
|
295
237
|
issues.push(
|
|
296
|
-
`${criterion.id} has no verdict
|
|
238
|
+
`${criterion.id} has no engine verdict. Run the task recipe with an eligible assertion for this criterion.`,
|
|
297
239
|
);
|
|
298
240
|
continue;
|
|
299
241
|
}
|
|
300
242
|
if (
|
|
301
|
-
|
|
243
|
+
options.deferClaimVerdicts !== true &&
|
|
302
244
|
(entry.verdict === 'failed' ||
|
|
303
245
|
entry.verdict === 'awaiting-review' ||
|
|
304
246
|
(!options.allowWeak && (entry.verdict === 'missing' || entry.verdict === 'weak')))
|
|
305
247
|
) {
|
|
306
248
|
issues.push(
|
|
307
|
-
|
|
308
|
-
? `${criterion.id} is ${entry.verdict}: run an eligible recipe assertion or request human evidence review.`
|
|
309
|
-
: `${criterion.id} is ${entry.verdict}: prove it, or record \`untestable\` with a note ` +
|
|
310
|
-
'(a flow may waive this with worker_terminal.acceptance.allowWeak)',
|
|
249
|
+
`${criterion.id} is ${entry.verdict}: run an eligible recipe assertion or request human evidence review.`,
|
|
311
250
|
);
|
|
312
251
|
}
|
|
313
252
|
}
|
|
@@ -328,7 +267,6 @@ module.exports = {
|
|
|
328
267
|
requireHandoffAcceptanceCriteria,
|
|
329
268
|
readAcceptanceLedger,
|
|
330
269
|
renderAcceptanceCoverage,
|
|
331
|
-
setAcceptanceVerdict,
|
|
332
270
|
setDerivedClaims,
|
|
333
271
|
summarizeAcceptanceStatus,
|
|
334
272
|
validateAcceptanceStatusLedger,
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const {
|
|
9
9
|
expandedArtifactsForCommand,
|
|
10
|
+
resolveWorkerTerminalArtifactPath,
|
|
10
11
|
deterministicChecks,
|
|
11
12
|
} = require('./worker-terminal-contract.cjs');
|
|
12
13
|
const { SUBTASK_INDEX_REL, openSubtaskUnits } = require('./subtask-unit.cjs');
|
|
@@ -164,18 +165,8 @@ function resolveTaskArtifactPath(rel) {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
function publicationArtifactPath(rel) {
|
|
167
|
-
if (rel !== 'artifacts/evidence-manifest.json') return rel;
|
|
168
|
-
const pointerPath = path.join(taskDir, 'artifacts/latest-valid-recipe-run.json');
|
|
169
|
-
if (!existsSync(pointerPath)) return rel;
|
|
170
|
-
const pointer = recipeProtocol?.sanitizeLatestValidRecipeRunPointer?.(
|
|
171
|
-
JSON.parse(readFileSync(pointerPath, 'utf8')),
|
|
172
|
-
);
|
|
173
|
-
if (!pointer) throw new Error('latest-valid-recipe-run.json: invalid pointer.');
|
|
174
|
-
const selected = `artifacts/${pointer.relativeArtifactRoot}/evidence-manifest.json`;
|
|
175
168
|
const version = terminalContract ? (terminalContract.recordedEvidenceContractVersion ?? 0) : 1;
|
|
176
|
-
|
|
177
|
-
if (version === 0 && !existsSync(path.resolve(taskDir, selected))) return rel;
|
|
178
|
-
return selected;
|
|
169
|
+
return resolveWorkerTerminalArtifactPath(taskDir, rel, version);
|
|
179
170
|
}
|
|
180
171
|
|
|
181
172
|
function isRecord(value) {
|
|
@@ -6,6 +6,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
6
6
|
const {
|
|
7
7
|
expandedArtifactsForCommand,
|
|
8
8
|
resolveWorkerTerminalContract,
|
|
9
|
+
resolveWorkerTerminalArtifactPath,
|
|
9
10
|
deterministicChecks,
|
|
10
11
|
} = require('./worker-terminal-contract.cjs');
|
|
11
12
|
const {
|
|
@@ -225,7 +226,14 @@ function assertRequiredArtifacts(taskDir, terminalCommand, contract) {
|
|
|
225
226
|
);
|
|
226
227
|
for (const storedPath of expanded.artifacts) {
|
|
227
228
|
if (storedPath === LEARNINGS_ARTIFACT && opts['skip-learnings']) continue;
|
|
228
|
-
assertArtifactFile(
|
|
229
|
+
assertArtifactFile(
|
|
230
|
+
taskDir,
|
|
231
|
+
resolveWorkerTerminalArtifactPath(
|
|
232
|
+
taskDir,
|
|
233
|
+
storedPath,
|
|
234
|
+
contract.recordedEvidenceContractVersion ?? 0,
|
|
235
|
+
),
|
|
236
|
+
);
|
|
229
237
|
}
|
|
230
238
|
}
|
|
231
239
|
|
|
@@ -174,6 +174,7 @@ const BUILTIN_FLOW_COMMANDS = {
|
|
|
174
174
|
const DEFAULT_WHEN_PRESENT = [
|
|
175
175
|
{
|
|
176
176
|
path: 'artifacts/recipe.json',
|
|
177
|
+
// The manifest requirement resolves to the selected engine attempt, not a root copy.
|
|
177
178
|
alsoRequire: ['artifacts/recipe-coverage.md', 'artifacts/evidence-manifest.json'],
|
|
178
179
|
requireRecipeQuality: true,
|
|
179
180
|
requireRecipeCoverage: true,
|
|
@@ -432,6 +433,21 @@ function expandedArtifactsForCommand(contract, command, fileExists) {
|
|
|
432
433
|
|
|
433
434
|
const DETERMINISTIC_REFERENCE = /\[DETERMINISTIC:\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\]/;
|
|
434
435
|
|
|
436
|
+
/** Resolve the logical manifest requirement through the existing retained selection. */
|
|
437
|
+
function resolveWorkerTerminalArtifactPath(taskDir, relPath, recordedEvidenceContractVersion = 1) {
|
|
438
|
+
if (relPath !== 'artifacts/evidence-manifest.json') return relPath;
|
|
439
|
+
if (!fs.existsSync(path.join(taskDir, 'artifacts/latest-valid-recipe-run.json'))) return relPath;
|
|
440
|
+
const { readRecipeAttemptSelection } = require('./retained-recipe-attempt.cjs');
|
|
441
|
+
const pointer = readRecipeAttemptSelection(taskDir);
|
|
442
|
+
if (!pointer) throw new Error('Invalid retained recipe attempt selection.');
|
|
443
|
+
const selected = `artifacts/${pointer.relativeArtifactRoot}/evidence-manifest.json`;
|
|
444
|
+
if (recordedEvidenceContractVersion !== 0 && recordedEvidenceContractVersion !== 1)
|
|
445
|
+
throw new Error('Unsupported recorded evidence contract.');
|
|
446
|
+
if (recordedEvidenceContractVersion === 0 && !fs.existsSync(path.resolve(taskDir, selected)))
|
|
447
|
+
return relPath;
|
|
448
|
+
return selected;
|
|
449
|
+
}
|
|
450
|
+
|
|
435
451
|
function deterministicDeclarations(markdown, checklist) {
|
|
436
452
|
return enumerateChecklistCheckboxes(markdown).flatMap((item) => {
|
|
437
453
|
const match = item.rawLabel.match(DETERMINISTIC_REFERENCE);
|
|
@@ -770,5 +786,6 @@ module.exports = {
|
|
|
770
786
|
templateTerminalCommands,
|
|
771
787
|
templateBodyScope,
|
|
772
788
|
expandedArtifactsForCommand,
|
|
789
|
+
resolveWorkerTerminalArtifactPath,
|
|
773
790
|
deterministicChecks,
|
|
774
791
|
};
|