@farmslot/agent-runtime 0.1.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.
@@ -0,0 +1,178 @@
1
+ import { isRecipeQualityArtifact, RECIPE_QUALITY_ARTIFACT_SOURCES, RECIPE_QUALITY_ARTIFACT_VERSION, RECIPE_QUALITY_DIMENSION_STATUSES, RECIPE_QUALITY_FALLBACK_SOURCES, RECIPE_QUALITY_PROOF_MODES, RECIPE_QUALITY_VERDICTS, } from '@farmslot/protocol';
2
+ const COMPACT_VERDICT_BY_VERDICT = {
3
+ pass: 'PASS',
4
+ warn: 'WARN',
5
+ fail: 'FAIL',
6
+ };
7
+ const CORE_ARTIFACT_KEYS = new Set([
8
+ 'version',
9
+ 'verdict',
10
+ 'compact',
11
+ 'dimensions',
12
+ 'structural_findings',
13
+ 'contextual_findings',
14
+ 'suggested_recipe_delta',
15
+ 'training_fields',
16
+ 'meta',
17
+ ]);
18
+ function assertRecipeQualityVerdict(value) {
19
+ if (!RECIPE_QUALITY_VERDICTS.includes(value)) {
20
+ throw new TypeError(`verdict must be one of: ${RECIPE_QUALITY_VERDICTS.join(', ')}.`);
21
+ }
22
+ }
23
+ function assertRecipeQualityProducer(value) {
24
+ if (!RECIPE_QUALITY_ARTIFACT_SOURCES.includes(value)) {
25
+ throw new TypeError(`producer must be one of: ${RECIPE_QUALITY_ARTIFACT_SOURCES.join(', ')}.`);
26
+ }
27
+ }
28
+ function assertStringArray(name, value) {
29
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
30
+ throw new TypeError(`${name} must be an array of strings.`);
31
+ }
32
+ }
33
+ function defaultFallbackUsed(producer, fallbackSource) {
34
+ return fallbackSource != null || producer.startsWith('fallback:');
35
+ }
36
+ function isPlainRecord(value) {
37
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
38
+ }
39
+ function assertOptionalPlainRecord(name, value) {
40
+ if (value != null && !isPlainRecord(value)) {
41
+ throw new TypeError(`${name} must be a JSON object when provided.`);
42
+ }
43
+ }
44
+ function assertOptionalBoolean(name, value) {
45
+ if (value != null && typeof value !== 'boolean') {
46
+ throw new TypeError(`${name} must be a boolean when provided.`);
47
+ }
48
+ }
49
+ function assertOptionalArray(name, value) {
50
+ if (value != null && !Array.isArray(value)) {
51
+ throw new TypeError(`${name} must be an array when provided.`);
52
+ }
53
+ }
54
+ function validateTrainingFields(value) {
55
+ assertOptionalPlainRecord('trainingFields', value);
56
+ const proofMode = value?.proof_mode;
57
+ if (proofMode != null && !RECIPE_QUALITY_PROOF_MODES.includes(proofMode)) {
58
+ throw new TypeError(`trainingFields.proof_mode must be one of: ${RECIPE_QUALITY_PROOF_MODES.join(', ')}.`);
59
+ }
60
+ }
61
+ function validateDimensions(value) {
62
+ assertOptionalPlainRecord('dimensions', value);
63
+ if (!value)
64
+ return;
65
+ for (const [name, dimension] of Object.entries(value)) {
66
+ if (!isPlainRecord(dimension)) {
67
+ throw new TypeError(`dimensions.${name} must be a JSON object.`);
68
+ }
69
+ if (!RECIPE_QUALITY_DIMENSION_STATUSES.includes(dimension.status)) {
70
+ throw new TypeError(`dimensions.${name}.status must be one of: ${RECIPE_QUALITY_DIMENSION_STATUSES.join(', ')}.`);
71
+ }
72
+ if (typeof dimension.reason !== 'string') {
73
+ throw new TypeError(`dimensions.${name}.reason must be a string.`);
74
+ }
75
+ assertStringArray(`dimensions.${name}.evidence`, dimension.evidence);
76
+ }
77
+ }
78
+ function validateFindings(name, value) {
79
+ assertOptionalArray(name, value);
80
+ if (!value)
81
+ return;
82
+ for (const [index, finding] of value.entries()) {
83
+ if (!isPlainRecord(finding)) {
84
+ throw new TypeError(`${name}.${index} must be a JSON object.`);
85
+ }
86
+ if (typeof finding.code !== 'string')
87
+ throw new TypeError(`${name}.${index}.code must be a string.`);
88
+ if (typeof finding.message !== 'string')
89
+ throw new TypeError(`${name}.${index}.message must be a string.`);
90
+ if (finding.dimension != null && typeof finding.dimension !== 'string') {
91
+ throw new TypeError(`${name}.${index}.dimension must be a string when provided.`);
92
+ }
93
+ if (finding.evidence != null)
94
+ assertStringArray(`${name}.${index}.evidence`, finding.evidence);
95
+ }
96
+ }
97
+ function validateExtraKeys(extra) {
98
+ assertOptionalPlainRecord('extra', extra);
99
+ if (!extra)
100
+ return;
101
+ for (const key of Object.keys(extra)) {
102
+ if (CORE_ARTIFACT_KEYS.has(key)) {
103
+ throw new TypeError(`extra.${key} cannot override a RecipeQualityArtifact core field.`);
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * Builds the canonical `artifacts/recipe-quality.json` payload from the compact
109
+ * fields agents are expected to know after a recipe-quality pass.
110
+ */
111
+ export function buildRecipeQualityArtifact(input) {
112
+ assertRecipeQualityVerdict(input.verdict);
113
+ assertStringArray('reasons', input.reasons);
114
+ if (input.reasons.length === 0) {
115
+ throw new TypeError('reasons must include at least one reason.');
116
+ }
117
+ assertStringArray('betterVersionGuidance', input.betterVersionGuidance ?? []);
118
+ assertStringArray('suggestedRecipeDelta', input.suggestedRecipeDelta ?? []);
119
+ assertStringArray('sourceSignals', input.sourceSignals ?? ['recipe-quality.json']);
120
+ validateDimensions(input.dimensions);
121
+ validateFindings('structuralFindings', input.structuralFindings);
122
+ validateFindings('contextualFindings', input.contextualFindings);
123
+ assertOptionalBoolean('fallbackUsed', input.fallbackUsed);
124
+ assertOptionalBoolean('legacyTask', input.legacyTask);
125
+ assertOptionalBoolean('artifactRequired', input.artifactRequired);
126
+ validateTrainingFields(input.trainingFields);
127
+ validateExtraKeys(input.extra);
128
+ const producer = input.producer ?? 'worker';
129
+ assertRecipeQualityProducer(producer);
130
+ const fallbackSource = input.fallbackSource;
131
+ if (fallbackSource != null && !RECIPE_QUALITY_FALLBACK_SOURCES.includes(fallbackSource)) {
132
+ throw new TypeError(`fallbackSource must be one of: ${RECIPE_QUALITY_FALLBACK_SOURCES.join(', ')}.`);
133
+ }
134
+ const fallbackUsed = input.fallbackUsed ?? defaultFallbackUsed(producer, fallbackSource);
135
+ if (fallbackSource != null && !producer.startsWith('fallback:')) {
136
+ throw new TypeError('fallbackSource requires a fallback producer.');
137
+ }
138
+ if (fallbackSource != null && fallbackUsed === false) {
139
+ throw new TypeError('fallbackUsed cannot be false when fallbackSource is set.');
140
+ }
141
+ if (producer.startsWith('fallback:') && fallbackUsed === false) {
142
+ throw new TypeError('fallbackUsed cannot be false for fallback producers.');
143
+ }
144
+ if (!producer.startsWith('fallback:') && fallbackUsed === true) {
145
+ throw new TypeError('fallbackUsed cannot be true for non-fallback producers.');
146
+ }
147
+ const artifact = {
148
+ ...(input.extra ?? {}),
149
+ version: RECIPE_QUALITY_ARTIFACT_VERSION,
150
+ verdict: input.verdict,
151
+ compact: {
152
+ verdict: COMPACT_VERDICT_BY_VERDICT[input.verdict],
153
+ reasons: input.reasons,
154
+ better_version_guidance: input.betterVersionGuidance ?? [],
155
+ },
156
+ dimensions: input.dimensions ?? {},
157
+ structural_findings: input.structuralFindings ?? [],
158
+ contextual_findings: input.contextualFindings ?? [],
159
+ suggested_recipe_delta: input.suggestedRecipeDelta ?? [],
160
+ training_fields: {
161
+ proof_mode: 'unknown',
162
+ ...(input.trainingFields ?? {}),
163
+ },
164
+ meta: {
165
+ producer,
166
+ fallback_used: fallbackUsed,
167
+ ...(fallbackSource ? { fallback_source: fallbackSource } : {}),
168
+ legacy_task: input.legacyTask ?? false,
169
+ artifact_required: input.artifactRequired ?? true,
170
+ source_signals: input.sourceSignals ?? ['recipe-quality.json'],
171
+ },
172
+ };
173
+ if (!isRecipeQualityArtifact(artifact)) {
174
+ throw new TypeError('Built recipe-quality artifact does not satisfy RecipeQualityArtifact.');
175
+ }
176
+ return artifact;
177
+ }
178
+ //# sourceMappingURL=recipe-quality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipe-quality.js","sourceRoot":"","sources":["../src/recipe-quality.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@farmslot/agent-runtime",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "farmslot-agent": "./bin/farmslot-agent.mjs"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./scripts/mark-checklist-step.cjs": "./scripts/mark-checklist-step.cjs",
17
+ "./scripts/worker-terminal-contract.cjs": "./scripts/worker-terminal-contract.cjs",
18
+ "./scripts/check-task-artifact-contract.mjs": "./scripts/check-task-artifact-contract.mjs"
19
+ },
20
+ "scripts": {
21
+ "typecheck": "yarn workspace @farmslot/protocol build && tsc --noEmit --project tsconfig.json",
22
+ "test": "node test/mark-checklist-step.test.cjs && node test/package-exports.test.cjs && node test/check-task-artifact-contract.test.cjs && node test/recipe-quality-builder.test.cjs",
23
+ "format": "prettier --write --ignore-path ../../.prettierignore .",
24
+ "format:check": "prettier --check --ignore-path ../../.prettierignore .",
25
+ "lint": "eslint \"src/**/*.ts\"",
26
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
27
+ "quality": "prettier --check --ignore-path ../../.prettierignore . && eslint \"src/**/*.ts\" && yarn typecheck && yarn build && yarn test",
28
+ "build": "yarn workspace @farmslot/protocol build && rm -rf dist && tsc --project tsconfig.build.json",
29
+ "prepack": "cd ../.. && node scripts/quality/check-farmslot-package-readiness.mjs --require-changelog --packages @farmslot/agent-runtime && yarn workspace @farmslot/agent-runtime build",
30
+ "prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/agent-runtime"
31
+ },
32
+ "dependencies": {
33
+ "@farmslot/protocol": "0.7.3"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "eslint": "^10.3.0",
38
+ "prettier": "^3.8.3",
39
+ "typescript": "^5.6.0"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/deeeed/farmslot.git",
44
+ "directory": "packages/agent-runtime"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/deeeed/farmslot/issues"
48
+ },
49
+ "license": "MIT",
50
+ "homepage": "https://farmslot.io/docs/reference/agent-runtime",
51
+ "keywords": [
52
+ "farmslot",
53
+ "agent-runtime",
54
+ "task-lifecycle",
55
+ "validation",
56
+ "agentic-engineering"
57
+ ],
58
+ "files": [
59
+ "README.md",
60
+ "LICENSE",
61
+ "CHANGELOG.md",
62
+ "bin/**/*.mjs",
63
+ "dist/**/*.js",
64
+ "dist/**/*.d.ts",
65
+ "dist/**/*.d.ts.map",
66
+ "dist/**/*.js.map",
67
+ "scripts/**/*.cjs",
68
+ "scripts/**/*.mjs"
69
+ ],
70
+ "publishConfig": {
71
+ "access": "public"
72
+ }
73
+ }
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const { expandedArtifactsForCommand } = require('./worker-terminal-contract.cjs');
8
+
9
+ let sharedRecipeQualityValidator = null;
10
+ try {
11
+ ({ isRecipeQualityArtifact: sharedRecipeQualityValidator } =
12
+ await import('@farmslot/protocol/contracts/recipes'));
13
+ } catch (error) {
14
+ // Source checkouts run this script before @farmslot/protocol has emitted dist/.
15
+ // Keep strict local validation instead of downgrading to the historical loose gate.
16
+ if (process.env.FARMSLOT_DEBUG_AGENT_RUNTIME) {
17
+ console.warn(`[agent-runtime] using local RecipeQualityArtifact fallback: ${error.message}`);
18
+ }
19
+ }
20
+
21
+ const taskDir = process.argv[2];
22
+ const rawArgs = process.argv.slice(3);
23
+ const flags = new Set();
24
+ let contractPath;
25
+ let terminalCommand;
26
+ for (let i = 0; i < rawArgs.length; i += 1) {
27
+ const arg = rawArgs[i];
28
+ if (arg === '--contract') {
29
+ contractPath = rawArgs[i + 1];
30
+ i += 1;
31
+ continue;
32
+ }
33
+ if (arg === '--terminal') {
34
+ terminalCommand = rawArgs[i + 1];
35
+ i += 1;
36
+ continue;
37
+ }
38
+ flags.add(arg);
39
+ }
40
+
41
+ if (!taskDir) {
42
+ console.error(
43
+ 'usage: check-task-artifact-contract.mjs <task-dir> [--contract path] [--terminal complete|no-change|blocked] [--require-recipe-quality-if-recipe] [--require-recipe-coverage-if-recipe] [--require-learnings] [--skip-learnings]',
44
+ );
45
+ process.exit(2);
46
+ }
47
+
48
+ const issues = [];
49
+ const mediaExt = /\.(png|jpe?g|gif|mp4|mov|webm)$/i;
50
+ const internalArtifactPath =
51
+ /^artifacts\/(?:harness-launch|harness-relaunch|harness-relaunch-node20|runtime-launch|runtime-relaunch|runner-blockers)\//;
52
+ const allowedManifestKeys = new Set([
53
+ 'version',
54
+ 'preferred_mode',
55
+ 'summary',
56
+ 'before_after_pairs',
57
+ 'standalone',
58
+ 'omit',
59
+ 'videos',
60
+ 'before_state_capture',
61
+ ]);
62
+ const allowedPairKeys = new Set(['label', 'covers', 'before', 'after', 'note']);
63
+ const allowedStandaloneKeys = new Set(['label', 'covers', 'file', 'note']);
64
+ const allowedVideoKeys = new Set(['before', 'after', 'preferred', 'note']);
65
+ const allowedOmitKeys = new Set(['file', 'reason']);
66
+
67
+ function fileExists(rel) {
68
+ try {
69
+ return statSync(path.join(taskDir, rel)).isFile();
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ function readText(rel) {
76
+ const p = path.join(taskDir, rel);
77
+ return existsSync(p) ? readFileSync(p, 'utf8') : null;
78
+ }
79
+
80
+ function isRecord(value) {
81
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
82
+ }
83
+
84
+ function isStringArray(value) {
85
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
86
+ }
87
+
88
+ function isRecipeQualityArtifactFallback(value) {
89
+ if (!isRecord(value)) return false;
90
+ if (value.version !== 1) return false;
91
+ if (!['pass', 'warn', 'fail'].includes(value.verdict)) return false;
92
+ if (!isRecord(value.compact)) return false;
93
+ if (!['PASS', 'WARN', 'FAIL'].includes(value.compact.verdict)) return false;
94
+ if (!isStringArray(value.compact.reasons)) return false;
95
+ if (!isStringArray(value.compact.better_version_guidance)) return false;
96
+ if (!isRecord(value.dimensions)) return false;
97
+ for (const dimension of Object.values(value.dimensions)) {
98
+ if (!isRecord(dimension)) return false;
99
+ if (!['pass', 'warn', 'fail', 'not_applicable'].includes(dimension.status)) return false;
100
+ if (typeof dimension.reason !== 'string') return false;
101
+ if (!isStringArray(dimension.evidence)) return false;
102
+ }
103
+ for (const key of ['structural_findings', 'contextual_findings']) {
104
+ if (!Array.isArray(value[key])) return false;
105
+ for (const finding of value[key]) {
106
+ if (!isRecord(finding)) return false;
107
+ if (typeof finding.code !== 'string' || typeof finding.message !== 'string') return false;
108
+ if (finding.dimension != null && typeof finding.dimension !== 'string') return false;
109
+ if (finding.evidence != null && !isStringArray(finding.evidence)) return false;
110
+ }
111
+ }
112
+ if (!isStringArray(value.suggested_recipe_delta)) return false;
113
+ if (!isRecord(value.training_fields)) return false;
114
+ const fields = value.training_fields;
115
+ if (fields.farm != null && typeof fields.farm !== 'string') return false;
116
+ if (fields.project != null && typeof fields.project !== 'string') return false;
117
+ if (
118
+ fields.flow_type != null &&
119
+ !['fix-bug', 'review-pr', 'dev', 'pr-complete', 'merge-main'].includes(fields.flow_type)
120
+ )
121
+ return false;
122
+ if (fields.task_type != null && typeof fields.task_type !== 'string') return false;
123
+ if (fields.claim_is_visual != null && typeof fields.claim_is_visual !== 'boolean') return false;
124
+ if (
125
+ fields.proof_mode != null &&
126
+ !['logs', 'state', 'trace', 'screenshot', 'mixed', 'unknown'].includes(fields.proof_mode)
127
+ )
128
+ return false;
129
+ if (fields.anti_patterns != null && !isStringArray(fields.anti_patterns)) return false;
130
+ if (fields.good_patterns != null && !isStringArray(fields.good_patterns)) return false;
131
+ if (!isRecord(value.meta)) return false;
132
+ const meta = value.meta;
133
+ if (
134
+ ![
135
+ 'worker',
136
+ 'gateway',
137
+ 'fallback:recipe-coverage',
138
+ 'fallback:recipe-json',
139
+ 'fallback:report',
140
+ 'fallback:missing',
141
+ ].includes(meta.producer)
142
+ )
143
+ return false;
144
+ if (typeof meta.fallback_used !== 'boolean') return false;
145
+ if (typeof meta.legacy_task !== 'boolean') return false;
146
+ if (typeof meta.artifact_required !== 'boolean') return false;
147
+ if (!isStringArray(meta.source_signals)) return false;
148
+ if (
149
+ meta.fallback_source != null &&
150
+ ![
151
+ 'fallback:recipe-coverage',
152
+ 'fallback:recipe-json',
153
+ 'fallback:report',
154
+ 'fallback:missing',
155
+ ].includes(meta.fallback_source)
156
+ )
157
+ return false;
158
+ return true;
159
+ }
160
+
161
+ function isRecipeQualityArtifactStrict(value) {
162
+ const validator = sharedRecipeQualityValidator ?? isRecipeQualityArtifactFallback;
163
+ return validator(value);
164
+ }
165
+
166
+ function unknownKeys(record, allowed, prefix) {
167
+ for (const key of Object.keys(record)) {
168
+ if (!allowed.has(key)) issues.push(`${prefix}.${key}: unknown key`);
169
+ }
170
+ }
171
+
172
+ function optionalString(record, key, prefix) {
173
+ if (record[key] !== undefined && typeof record[key] !== 'string') {
174
+ issues.push(`${prefix}.${key}: expected string`);
175
+ }
176
+ }
177
+
178
+ function optionalStringArray(record, key, prefix) {
179
+ if (record[key] === undefined) return;
180
+ if (!Array.isArray(record[key]) || record[key].some((entry) => typeof entry !== 'string')) {
181
+ issues.push(`${prefix}.${key}: expected string[]`);
182
+ }
183
+ }
184
+
185
+ function normalizeArtifactPath(value) {
186
+ if (typeof value !== 'string') return null;
187
+ const trimmed = value.trim();
188
+ if (!trimmed || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(trimmed)) return null;
189
+ const pathPart = trimmed.split(/[?#]/, 1)[0];
190
+ const normalized = pathPart.replace(/\\/g, '/').replace(/^\.\/+/g, '');
191
+ if (!normalized || path.posix.isAbsolute(normalized) || normalized.includes('\0')) return null;
192
+ const segments = normalized.split('/').filter(Boolean);
193
+ if (segments.some((segment) => segment === '.' || segment === '..')) return null;
194
+ const withoutArtifacts =
195
+ segments[0] === 'artifacts' ? segments.slice(1).join('/') : segments.join('/');
196
+ if (!withoutArtifacts || !mediaExt.test(withoutArtifacts)) return null;
197
+ return `artifacts/${withoutArtifacts}`;
198
+ }
199
+
200
+ function addRef(refs, value) {
201
+ const normalized = normalizeArtifactPath(value);
202
+ if (!normalized) return;
203
+ if (internalArtifactPath.test(normalized)) {
204
+ issues.push(`evidence-manifest references internal artifact: ${normalized}`);
205
+ return;
206
+ }
207
+ refs.add(normalized);
208
+ }
209
+
210
+ function validateRecipeQualityArtifact() {
211
+ const text = readText('artifacts/recipe-quality.json');
212
+ if (!text) return;
213
+ let artifact;
214
+ try {
215
+ artifact = JSON.parse(text);
216
+ } catch (error) {
217
+ issues.push(`recipe-quality.json: invalid JSON: ${error.message}`);
218
+ return;
219
+ }
220
+ if (!isRecord(artifact)) {
221
+ issues.push('recipe-quality.json: expected object');
222
+ return;
223
+ }
224
+ if (!isRecipeQualityArtifactStrict(artifact)) {
225
+ issues.push('recipe-quality.json: does not match RecipeQualityArtifact contract');
226
+ }
227
+ }
228
+
229
+ function parseManifest() {
230
+ const text = readText('artifacts/evidence-manifest.json');
231
+ if (!text) return null;
232
+ let manifest;
233
+ try {
234
+ manifest = JSON.parse(text);
235
+ } catch (error) {
236
+ issues.push(`evidence-manifest.json: invalid JSON: ${error.message}`);
237
+ return null;
238
+ }
239
+ if (!isRecord(manifest)) {
240
+ issues.push('evidence-manifest.json: expected object');
241
+ return null;
242
+ }
243
+ unknownKeys(manifest, allowedManifestKeys, 'manifest');
244
+ if (manifest.version !== undefined && typeof manifest.version !== 'number') {
245
+ issues.push('manifest.version: expected number');
246
+ }
247
+ if (
248
+ manifest.preferred_mode !== undefined &&
249
+ !['screenshots', 'video'].includes(manifest.preferred_mode)
250
+ ) {
251
+ issues.push('manifest.preferred_mode: expected screenshots or video');
252
+ }
253
+ optionalString(manifest, 'summary', 'manifest');
254
+
255
+ if (manifest.before_after_pairs !== undefined) {
256
+ if (!Array.isArray(manifest.before_after_pairs)) {
257
+ issues.push('manifest.before_after_pairs: expected array');
258
+ } else {
259
+ manifest.before_after_pairs.forEach((entry, index) => {
260
+ const prefix = `manifest.before_after_pairs[${index}]`;
261
+ if (!isRecord(entry)) return issues.push(`${prefix}: expected object`);
262
+ unknownKeys(entry, allowedPairKeys, prefix);
263
+ if (typeof entry.label !== 'string' || !entry.label.trim())
264
+ issues.push(`${prefix}.label: expected non-empty string`);
265
+ optionalStringArray(entry, 'covers', prefix);
266
+ optionalString(entry, 'before', prefix);
267
+ optionalString(entry, 'after', prefix);
268
+ optionalString(entry, 'note', prefix);
269
+ if (entry.before === undefined && entry.after === undefined)
270
+ issues.push(`${prefix}: expected before or after`);
271
+ });
272
+ }
273
+ }
274
+
275
+ if (manifest.standalone !== undefined) {
276
+ if (!Array.isArray(manifest.standalone)) {
277
+ issues.push('manifest.standalone: expected array');
278
+ } else {
279
+ manifest.standalone.forEach((entry, index) => {
280
+ const prefix = `manifest.standalone[${index}]`;
281
+ if (!isRecord(entry)) return issues.push(`${prefix}: expected object`);
282
+ unknownKeys(entry, allowedStandaloneKeys, prefix);
283
+ if (typeof entry.label !== 'string' || !entry.label.trim())
284
+ issues.push(`${prefix}.label: expected non-empty string`);
285
+ optionalStringArray(entry, 'covers', prefix);
286
+ if (typeof entry.file !== 'string' || !entry.file.trim())
287
+ issues.push(`${prefix}.file: expected non-empty string`);
288
+ optionalString(entry, 'note', prefix);
289
+ });
290
+ }
291
+ }
292
+
293
+ if (manifest.videos !== undefined) {
294
+ if (!isRecord(manifest.videos)) {
295
+ issues.push('manifest.videos: expected object');
296
+ } else {
297
+ unknownKeys(manifest.videos, allowedVideoKeys, 'manifest.videos');
298
+ optionalString(manifest.videos, 'before', 'manifest.videos');
299
+ optionalString(manifest.videos, 'after', 'manifest.videos');
300
+ optionalString(manifest.videos, 'note', 'manifest.videos');
301
+ if (
302
+ manifest.videos.preferred !== undefined &&
303
+ typeof manifest.videos.preferred !== 'boolean'
304
+ ) {
305
+ issues.push('manifest.videos.preferred: expected boolean');
306
+ }
307
+ }
308
+ }
309
+
310
+ if (manifest.omit !== undefined) {
311
+ if (!Array.isArray(manifest.omit)) {
312
+ issues.push('manifest.omit: expected array');
313
+ } else {
314
+ manifest.omit.forEach((entry, index) => {
315
+ const prefix = `manifest.omit[${index}]`;
316
+ if (typeof entry === 'string') return;
317
+ if (!isRecord(entry)) return issues.push(`${prefix}: expected string or object`);
318
+ unknownKeys(entry, allowedOmitKeys, prefix);
319
+ if (typeof entry.file !== 'string' || !entry.file.trim())
320
+ issues.push(`${prefix}.file: expected non-empty string`);
321
+ optionalString(entry, 'reason', prefix);
322
+ });
323
+ }
324
+ }
325
+
326
+ const refs = new Set();
327
+ for (const pair of manifest.before_after_pairs ?? []) {
328
+ addRef(refs, pair?.before);
329
+ addRef(refs, pair?.after);
330
+ }
331
+ for (const entry of manifest.standalone ?? []) addRef(refs, entry?.file);
332
+ addRef(refs, manifest.videos?.before);
333
+ addRef(refs, manifest.videos?.after);
334
+
335
+ for (const ref of [...refs].sort()) {
336
+ if (!fileExists(ref)) issues.push(`evidence-manifest references missing artifact: ${ref}`);
337
+ }
338
+ return { manifest, refs };
339
+ }
340
+
341
+ const hasRecipe = fileExists('artifacts/recipe.json');
342
+ if (
343
+ hasRecipe &&
344
+ flags.has('--require-recipe-quality-if-recipe') &&
345
+ !fileExists('artifacts/recipe-quality.json')
346
+ ) {
347
+ issues.push('recipe.json exists but artifacts/recipe-quality.json is missing');
348
+ }
349
+ if (
350
+ hasRecipe &&
351
+ flags.has('--require-recipe-coverage-if-recipe') &&
352
+ !fileExists('artifacts/recipe-coverage.md')
353
+ ) {
354
+ issues.push('recipe.json exists but artifacts/recipe-coverage.md is missing');
355
+ }
356
+ validateRecipeQualityArtifact();
357
+
358
+ const parsed = parseManifest();
359
+ const coverage = readText('artifacts/recipe-coverage.md');
360
+ if (coverage && /\|\s*(visual|mixed)\s*\|/i.test(coverage)) {
361
+ const refCount = parsed?.refs?.size ?? 0;
362
+ if (refCount === 0)
363
+ issues.push(
364
+ 'recipe-coverage.md declares visual/mixed proof but evidence-manifest has no media references',
365
+ );
366
+ }
367
+
368
+ if (flags.has('--require-learnings')) {
369
+ const learnings = readText('artifacts/learnings.md');
370
+ if (!learnings?.trim()) {
371
+ issues.push('artifacts/learnings.md: required non-empty learnings artifact');
372
+ }
373
+ }
374
+
375
+ if (contractPath && existsSync(contractPath) && terminalCommand) {
376
+ const contract = JSON.parse(readFileSync(contractPath, 'utf8'));
377
+ const expanded = expandedArtifactsForCommand(contract, terminalCommand, fileExists);
378
+ for (const artifactPath of expanded.artifacts) {
379
+ if (flags.has('--skip-learnings') && artifactPath === 'artifacts/learnings.md') continue;
380
+ if (artifactPath.endsWith('.md') && !readText(artifactPath)?.trim()) {
381
+ issues.push(`${artifactPath}: required non-empty artifact (worker terminal contract)`);
382
+ }
383
+ if (!fileExists(artifactPath)) {
384
+ issues.push(`${artifactPath}: required artifact missing (worker terminal contract)`);
385
+ }
386
+ }
387
+ if (expanded.requireRecipeQuality && hasRecipe && !fileExists('artifacts/recipe-quality.json')) {
388
+ issues.push('recipe.json exists but artifacts/recipe-quality.json is missing');
389
+ }
390
+ if (expanded.requireRecipeCoverage && hasRecipe && !fileExists('artifacts/recipe-coverage.md')) {
391
+ issues.push('recipe.json exists but artifacts/recipe-coverage.md is missing');
392
+ }
393
+ }
394
+
395
+ if (issues.length > 0) {
396
+ console.error('TASK_ARTIFACT_CONTRACT_FAIL');
397
+ for (const issue of issues) console.error(`- ${issue}`);
398
+ process.exit(1);
399
+ }
400
+ console.log('TASK_ARTIFACT_CONTRACT_PASS');