@farmslot/agent-runtime 0.2.0 → 0.3.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/CHANGELOG.md +18 -0
- package/bin/farmslot-agent.mjs +6 -0
- package/dist/execution-template/create.d.ts +7 -0
- package/dist/execution-template/create.d.ts.map +1 -0
- package/dist/execution-template/create.js +63 -0
- package/dist/execution-template/create.js.map +1 -0
- package/dist/execution-template/execution-template.test.d.ts +2 -0
- package/dist/execution-template/execution-template.test.d.ts.map +1 -0
- package/dist/execution-template/execution-template.test.js +340 -0
- package/dist/execution-template/execution-template.test.js.map +1 -0
- package/dist/execution-template/frontmatter.d.ts +16 -0
- package/dist/execution-template/frontmatter.d.ts.map +1 -0
- package/dist/execution-template/frontmatter.js +110 -0
- package/dist/execution-template/frontmatter.js.map +1 -0
- package/dist/execution-template/index.d.ts +7 -0
- package/dist/execution-template/index.d.ts.map +1 -0
- package/dist/execution-template/index.js +6 -0
- package/dist/execution-template/index.js.map +1 -0
- package/dist/execution-template/infer.d.ts +25 -0
- package/dist/execution-template/infer.d.ts.map +1 -0
- package/dist/execution-template/infer.js +122 -0
- package/dist/execution-template/infer.js.map +1 -0
- package/dist/execution-template/lint.d.ts +6 -0
- package/dist/execution-template/lint.d.ts.map +1 -0
- package/dist/execution-template/lint.js +203 -0
- package/dist/execution-template/lint.js.map +1 -0
- package/dist/execution-template/resolve.d.ts +12 -0
- package/dist/execution-template/resolve.d.ts.map +1 -0
- package/dist/execution-template/resolve.js +126 -0
- package/dist/execution-template/resolve.js.map +1 -0
- package/dist/execution-template/types.d.ts +66 -0
- package/dist/execution-template/types.d.ts.map +1 -0
- package/dist/execution-template/types.js +3 -0
- package/dist/execution-template/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/scripts/check-task-artifact-contract.mjs +202 -66
- package/scripts/execution-template-cli.mjs +203 -0
- package/scripts/mark-checklist-step.cjs +26 -8
- package/scripts/worker-terminal-contract.cjs +23 -21
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Shared Markdown execution-template catalog (ADR-049). */
|
|
2
|
+
export type ExecutionTemplateSourceKind = 'custom' | 'project' | 'workspace' | 'user' | 'package' | 'fallback';
|
|
3
|
+
export type ExecutionTemplateLayout = 'flow-tree' | 'worker-flat';
|
|
4
|
+
export type ExecutionRunMode = 'autonomous' | 'interactive' | 'validation';
|
|
5
|
+
export interface ExecutionTemplateSource {
|
|
6
|
+
/** Stable source label for catalogs / shadowing (e.g. project:farmslot-farm). */
|
|
7
|
+
id: string;
|
|
8
|
+
kind: ExecutionTemplateSourceKind;
|
|
9
|
+
/** Absolute directory to scan. */
|
|
10
|
+
root: string;
|
|
11
|
+
layout: ExecutionTemplateLayout;
|
|
12
|
+
}
|
|
13
|
+
export interface ExecutionTemplateFrontmatter {
|
|
14
|
+
id?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
flow?: string;
|
|
17
|
+
version?: string | number;
|
|
18
|
+
runMode?: ExecutionRunMode;
|
|
19
|
+
platforms?: string[];
|
|
20
|
+
labels?: string[];
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface ExecutionTemplateEntry {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
flow: string;
|
|
27
|
+
version: string;
|
|
28
|
+
runMode: ExecutionRunMode | null;
|
|
29
|
+
platforms: string[];
|
|
30
|
+
labels: string[];
|
|
31
|
+
path: string;
|
|
32
|
+
relativePath: string;
|
|
33
|
+
sourceId: string;
|
|
34
|
+
sourceKind: ExecutionTemplateSourceKind;
|
|
35
|
+
/** Present when a higher-precedence source already claimed this id. */
|
|
36
|
+
shadowedBy?: string;
|
|
37
|
+
frontmatter: ExecutionTemplateFrontmatter | null;
|
|
38
|
+
heading: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface ListExecutionTemplatesOptions {
|
|
41
|
+
sources: ExecutionTemplateSource[];
|
|
42
|
+
flow?: string;
|
|
43
|
+
runMode?: ExecutionRunMode;
|
|
44
|
+
platform?: string;
|
|
45
|
+
/** Include shadowed duplicates (default true for list diagnostics). */
|
|
46
|
+
includeShadowed?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface LintIssue {
|
|
49
|
+
path: string;
|
|
50
|
+
severity: 'error' | 'warning';
|
|
51
|
+
message: string;
|
|
52
|
+
}
|
|
53
|
+
export interface LintExecutionTemplatesResult {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
issues: LintIssue[];
|
|
56
|
+
filesChecked: number;
|
|
57
|
+
}
|
|
58
|
+
export interface CreateExecutionTemplateOptions {
|
|
59
|
+
path: string;
|
|
60
|
+
flow?: string;
|
|
61
|
+
runMode?: ExecutionRunMode;
|
|
62
|
+
platforms?: string[];
|
|
63
|
+
title?: string;
|
|
64
|
+
force?: boolean;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/execution-template/types.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAE5D,MAAM,MAAM,2BAA2B,GACnC,QAAQ,GACR,SAAS,GACT,WAAW,GACX,MAAM,GACN,SAAS,GACT,UAAU,CAAC;AAEf,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG,aAAa,CAAC;AAElE,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,aAAa,GAAG,YAAY,CAAC;AAE3E,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,2BAA2B,CAAC;IAClC,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,uBAAuB,CAAC;CACjC;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,2BAA2B,CAAC;IACxC,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,4BAA4B,GAAG,IAAI,CAAC;IACjD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,uBAAuB,EAAE,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/execution-template/types.ts"],"names":[],"mappings":"AAAA,4DAA4D"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export declare const AGENT_RUNTIME_SCRIPT_EXPORTS: {
|
|
|
4
4
|
readonly workerTerminalContract: "@farmslot/agent-runtime/scripts/worker-terminal-contract.cjs";
|
|
5
5
|
readonly checkTaskArtifactContract: "@farmslot/agent-runtime/scripts/check-task-artifact-contract.mjs";
|
|
6
6
|
};
|
|
7
|
+
export * from './execution-template/index.js';
|
|
7
8
|
export * from './recipe-quality.js';
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAE/D,eAAO,MAAM,4BAA4B;;;;CAI/B,CAAC;AAEX,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAE/D,eAAO,MAAM,4BAA4B;;;;CAI/B,CAAC;AAEX,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export const AGENT_RUNTIME_SCRIPT_EXPORTS = {
|
|
|
4
4
|
workerTerminalContract: '@farmslot/agent-runtime/scripts/worker-terminal-contract.cjs',
|
|
5
5
|
checkTaskArtifactContract: '@farmslot/agent-runtime/scripts/check-task-artifact-contract.mjs',
|
|
6
6
|
};
|
|
7
|
+
export * from './execution-template/index.js';
|
|
7
8
|
export * from './recipe-quality.js';
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,qBAAqB,GAAG,yBAAyB,CAAC;AAE/D,MAAM,CAAC,MAAM,4BAA4B,GAAG;IAC1C,iBAAiB,EAAE,yDAAyD;IAC5E,sBAAsB,EAAE,8DAA8D;IACtF,yBAAyB,EAAE,kEAAkE;CACrF,CAAC;AAEX,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,qBAAqB,GAAG,yBAAyB,CAAC;AAE/D,MAAM,CAAC,MAAM,4BAA4B,GAAG;IAC1C,iBAAiB,EAAE,yDAAyD;IAC5E,sBAAsB,EAAE,8DAA8D;IACtF,yBAAyB,EAAE,kEAAkE;CACrF,CAAC;AAEX,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farmslot/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"typecheck": "yarn workspace @farmslot/protocol build && tsc --noEmit --project tsconfig.json",
|
|
23
|
-
"test": "node test/mark-checklist-step.test.cjs && node test/checklist-target-sync.test.mjs && node test/package-exports.test.cjs && node test/check-task-artifact-contract.test.cjs && node test/recipe-quality-builder.test.cjs",
|
|
23
|
+
"test": "node test/mark-checklist-step.test.cjs && node test/checklist-target-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 ../../scripts/quality/run-tsx-tests.mjs --cwd . --tsconfig tsconfig.json src/execution-template",
|
|
24
24
|
"format": "prettier --write --ignore-path ../../.prettierignore .",
|
|
25
25
|
"format:check": "prettier --check --ignore-path ../../.prettierignore .",
|
|
26
26
|
"lint": "eslint \"src/**/*.ts\"",
|
|
@@ -31,12 +31,13 @@
|
|
|
31
31
|
"prepublishOnly": "node ../../scripts/quality/check-farmslot-package-readiness.mjs --publish --packages @farmslot/agent-runtime"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@farmslot/protocol": "0.
|
|
34
|
+
"@farmslot/protocol": "0.12.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|
|
38
38
|
"eslint": "^10.3.0",
|
|
39
39
|
"prettier": "^3.8.3",
|
|
40
|
+
"tsx": "^4.19.0",
|
|
40
41
|
"typescript": "^5.6.0"
|
|
41
42
|
},
|
|
42
43
|
"repository": {
|
|
@@ -66,7 +67,8 @@
|
|
|
66
67
|
"dist/**/*.d.ts.map",
|
|
67
68
|
"dist/**/*.js.map",
|
|
68
69
|
"scripts/**/*.cjs",
|
|
69
|
-
"scripts/**/*.mjs"
|
|
70
|
+
"scripts/**/*.mjs",
|
|
71
|
+
"scripts/execution-template-cli.mjs"
|
|
70
72
|
],
|
|
71
73
|
"publishConfig": {
|
|
72
74
|
"access": "public"
|
|
@@ -1,40 +1,83 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
const { expandedArtifactsForCommand } = require('./worker-terminal-contract.cjs');
|
|
9
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const workspaceProtocolRoot = path.resolve(packageRoot, '../protocol');
|
|
11
|
+
|
|
12
|
+
function isMissingWorkspaceProtocolBuild(error, output) {
|
|
13
|
+
return (
|
|
14
|
+
error?.code === 'ERR_MODULE_NOT_FOUND' &&
|
|
15
|
+
path.basename(path.dirname(packageRoot)) === 'packages' &&
|
|
16
|
+
existsSync(path.join(workspaceProtocolRoot, 'package.json')) &&
|
|
17
|
+
!existsSync(path.join(workspaceProtocolRoot, output))
|
|
18
|
+
);
|
|
19
|
+
}
|
|
8
20
|
|
|
9
21
|
let sharedRecipeQualityValidator = null;
|
|
10
22
|
let sharedRecipeDocumentValidator = null;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
let sharedRecipeArtifactPackageValidator = null;
|
|
24
|
+
let sharedResolvedRecipeArtifactPath = null;
|
|
25
|
+
// Keep the source-checkout fallback pinned to the canonical Recipe v1 schema.
|
|
26
|
+
const RECIPE_SCHEMA_URL = 'https://farmslot.io/schemas/recipe-v1.schema.json';
|
|
15
27
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
const qualityProtocol = await import('@farmslot/protocol/contracts/recipes');
|
|
29
|
+
if (typeof qualityProtocol.isRecipeQualityArtifact !== 'function') {
|
|
30
|
+
throw new Error('installed @farmslot/protocol does not export isRecipeQualityArtifact');
|
|
31
|
+
}
|
|
32
|
+
sharedRecipeQualityValidator = qualityProtocol.isRecipeQualityArtifact;
|
|
18
33
|
} catch (error) {
|
|
34
|
+
if (!isMissingWorkspaceProtocolBuild(error, 'dist/contracts/recipes.js')) throw error;
|
|
19
35
|
// Source checkouts run this script before @farmslot/protocol has emitted dist/.
|
|
20
36
|
// Keep strict local validation instead of downgrading to the historical loose gate.
|
|
21
37
|
if (process.env.FARMSLOT_DEBUG_AGENT_RUNTIME) {
|
|
22
38
|
console.warn(`[agent-runtime] using local RecipeQualityArtifact fallback: ${error.message}`);
|
|
23
39
|
}
|
|
24
40
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
let recipeProtocol = null;
|
|
42
|
+
if (process.env.FARMSLOT_TEST_DISABLE_RECIPE_PROTOCOL !== '1') {
|
|
43
|
+
try {
|
|
44
|
+
recipeProtocol = await import('@farmslot/protocol/recipe');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (!isMissingWorkspaceProtocolBuild(error, 'dist/recipe/index.js')) throw error;
|
|
47
|
+
if (process.env.FARMSLOT_DEBUG_AGENT_RUNTIME) {
|
|
48
|
+
console.warn(`[agent-runtime] shared Recipe v1 validator unavailable: ${error.message}`);
|
|
49
|
+
}
|
|
30
50
|
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
}
|
|
52
|
+
if (recipeProtocol) {
|
|
53
|
+
for (const api of [
|
|
54
|
+
'validateRecipeDocument',
|
|
55
|
+
'validateRecipeArtifactPackage',
|
|
56
|
+
'resolvedRecipeArtifactPath',
|
|
57
|
+
]) {
|
|
58
|
+
if (typeof recipeProtocol[api] !== 'function') {
|
|
59
|
+
throw new Error(`installed @farmslot/protocol does not export ${api}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const compatibilityProbe = recipeProtocol.validateRecipeDocument({
|
|
63
|
+
$schema: RECIPE_SCHEMA_URL,
|
|
64
|
+
description: 'Validate the canonical Recipe v1 envelope.',
|
|
65
|
+
workflow: {
|
|
66
|
+
entry: 'done',
|
|
67
|
+
nodes: { done: { action: 'end', status: 'pass' } },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const incompatibleFinding = compatibilityProbe?.findings?.find(
|
|
71
|
+
(finding) => finding?.severity === 'error',
|
|
72
|
+
);
|
|
73
|
+
if (incompatibleFinding) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`installed @farmslot/protocol rejects canonical Recipe v1 (${incompatibleFinding.code})`,
|
|
76
|
+
);
|
|
37
77
|
}
|
|
78
|
+
sharedRecipeDocumentValidator = recipeProtocol.validateRecipeDocument;
|
|
79
|
+
sharedRecipeArtifactPackageValidator = recipeProtocol.validateRecipeArtifactPackage;
|
|
80
|
+
sharedResolvedRecipeArtifactPath = recipeProtocol.resolvedRecipeArtifactPath;
|
|
38
81
|
}
|
|
39
82
|
|
|
40
83
|
const taskDir = process.argv[2];
|
|
@@ -85,15 +128,34 @@ const allowedOmitKeys = new Set(['file', 'reason']);
|
|
|
85
128
|
|
|
86
129
|
function fileExists(rel) {
|
|
87
130
|
try {
|
|
88
|
-
return statSync(
|
|
89
|
-
} catch {
|
|
131
|
+
return statSync(resolveTaskArtifactPath(rel)).isFile();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (!isMissingPathError(error)) throw error;
|
|
90
134
|
return false;
|
|
91
135
|
}
|
|
92
136
|
}
|
|
93
137
|
|
|
94
138
|
function readText(rel) {
|
|
95
|
-
|
|
96
|
-
|
|
139
|
+
try {
|
|
140
|
+
return readFileSync(resolveTaskArtifactPath(rel), 'utf8');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (!isMissingPathError(error)) throw error;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isMissingPathError(error) {
|
|
148
|
+
return error && typeof error === 'object' && error.code === 'ENOENT';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveTaskArtifactPath(rel) {
|
|
152
|
+
const root = realpathSync(taskDir);
|
|
153
|
+
const candidate = realpathSync(path.resolve(taskDir, rel));
|
|
154
|
+
const relative = path.relative(root, candidate);
|
|
155
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
156
|
+
throw new Error(`${rel} resolves outside the task directory`);
|
|
157
|
+
}
|
|
158
|
+
return candidate;
|
|
97
159
|
}
|
|
98
160
|
|
|
99
161
|
function isRecord(value) {
|
|
@@ -135,7 +197,7 @@ function isRecipeQualityArtifactFallback(value) {
|
|
|
135
197
|
if (fields.project != null && typeof fields.project !== 'string') return false;
|
|
136
198
|
if (
|
|
137
199
|
fields.flow_type != null &&
|
|
138
|
-
!['fix-bug', 'review-pr', 'dev', 'pr-complete', '
|
|
200
|
+
!['fix-bug', 'review-pr', 'dev', 'pr-complete', 'update-branch'].includes(fields.flow_type)
|
|
139
201
|
)
|
|
140
202
|
return false;
|
|
141
203
|
if (fields.task_type != null && typeof fields.task_type !== 'string') return false;
|
|
@@ -246,47 +308,88 @@ function validateRecipeQualityArtifact() {
|
|
|
246
308
|
}
|
|
247
309
|
|
|
248
310
|
function validateRecipeDocumentArtifact() {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
311
|
+
const authoredRecipe = readJsonArtifact('artifacts/recipe.json', 'recipe.json');
|
|
312
|
+
if (authoredRecipe === undefined) return;
|
|
313
|
+
validateRecipeDocumentValue(authoredRecipe, 'recipe.json', { skipRecipeCallResolution: true });
|
|
314
|
+
|
|
315
|
+
const packageRoot = 'artifacts/recipe-run';
|
|
316
|
+
const recipe = readJsonArtifact(`${packageRoot}/recipe.json`, 'recipe-run/recipe.json');
|
|
317
|
+
if (recipe === undefined) {
|
|
318
|
+
issues.push('artifacts/recipe-run/recipe.json is missing');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (canonicalJson(authoredRecipe) !== canonicalJson(recipe)) {
|
|
322
|
+
issues.push('artifacts/recipe-run/recipe.json does not match artifacts/recipe.json');
|
|
323
|
+
}
|
|
324
|
+
const manifest = readJsonArtifact(
|
|
325
|
+
`${packageRoot}/artifact-manifest.json`,
|
|
326
|
+
'recipe-run/artifact-manifest.json',
|
|
327
|
+
);
|
|
328
|
+
const trace = readJsonArtifact(`${packageRoot}/trace.json`, 'recipe-run/trace.json');
|
|
329
|
+
const recipeResolution = readJsonArtifact(
|
|
330
|
+
`${packageRoot}/recipe-resolution.json`,
|
|
331
|
+
'recipe-run/recipe-resolution.json',
|
|
332
|
+
);
|
|
333
|
+
const resolvedRecipes = {};
|
|
334
|
+
if (isRecord(recipeResolution) && Array.isArray(recipeResolution.dependencies)) {
|
|
335
|
+
for (const dependency of recipeResolution.dependencies) {
|
|
336
|
+
if (!isRecord(dependency)) continue;
|
|
337
|
+
const artifact = resolvedRecipeArtifactPath(dependency.digest);
|
|
338
|
+
if (!artifact) continue;
|
|
339
|
+
const document = readJsonArtifact(`${packageRoot}/${artifact}`, `recipe-run/${artifact}`);
|
|
340
|
+
if (document !== undefined) resolvedRecipes[String(dependency.digest)] = document;
|
|
261
341
|
}
|
|
262
342
|
}
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
343
|
+
if (!sharedRecipeArtifactPackageValidator) {
|
|
344
|
+
issues.push(
|
|
345
|
+
'recipe-run package validation requires built @farmslot/protocol; build the protocol package and retry',
|
|
346
|
+
);
|
|
267
347
|
return;
|
|
268
348
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
349
|
+
const result = sharedRecipeArtifactPackageValidator({
|
|
350
|
+
recipe,
|
|
351
|
+
manifest,
|
|
352
|
+
trace,
|
|
353
|
+
recipeResolution,
|
|
354
|
+
resolvedRecipes,
|
|
355
|
+
artifactPaths: listArtifactPaths(path.join(taskDir, packageRoot)),
|
|
276
356
|
});
|
|
277
|
-
|
|
357
|
+
for (const finding of result.findings) {
|
|
358
|
+
if (finding.severity === 'error') {
|
|
359
|
+
issues.push(`${finding.code} ${finding.path}: ${finding.message}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function canonicalJson(value) {
|
|
365
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
366
|
+
if (isRecord(value)) {
|
|
367
|
+
return `{${Object.entries(value)
|
|
368
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
369
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
|
370
|
+
.join(',')}}`;
|
|
371
|
+
}
|
|
372
|
+
return JSON.stringify(value);
|
|
278
373
|
}
|
|
279
374
|
|
|
280
|
-
function
|
|
375
|
+
function resolvedRecipeArtifactPath(digest) {
|
|
376
|
+
if (sharedResolvedRecipeArtifactPath) return sharedResolvedRecipeArtifactPath(digest);
|
|
377
|
+
if (typeof digest !== 'string' || !/^sha256:[a-f0-9]{64}$/u.test(digest)) return undefined;
|
|
378
|
+
return `resolved-recipes/${digest.slice('sha256:'.length)}.recipe.json`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function readJsonArtifact(relPath, label) {
|
|
281
382
|
const text = readText(relPath);
|
|
282
|
-
if (!text) return;
|
|
283
|
-
let recipe;
|
|
383
|
+
if (!text) return undefined;
|
|
284
384
|
try {
|
|
285
|
-
|
|
385
|
+
return JSON.parse(text);
|
|
286
386
|
} catch (error) {
|
|
287
387
|
issues.push(`${label}: invalid JSON: ${error.message}`);
|
|
288
|
-
return;
|
|
388
|
+
return undefined;
|
|
289
389
|
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function validateRecipeDocumentValue(recipe, label, options) {
|
|
290
393
|
if (sharedRecipeDocumentValidator) {
|
|
291
394
|
const result = sharedRecipeDocumentValidator(recipe, options);
|
|
292
395
|
for (const finding of result.findings) {
|
|
@@ -300,28 +403,61 @@ function validateRecipeDocumentArtifactAt(relPath, label, options) {
|
|
|
300
403
|
issues.push(`${label}: expected object`);
|
|
301
404
|
return;
|
|
302
405
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
406
|
+
const allowedRootFields = new Set([
|
|
407
|
+
'$schema',
|
|
408
|
+
'title',
|
|
409
|
+
'description',
|
|
410
|
+
'paramsSchema',
|
|
411
|
+
'proofTargets',
|
|
412
|
+
'workflow',
|
|
413
|
+
]);
|
|
414
|
+
for (const field of Object.keys(recipe)) {
|
|
415
|
+
if (!allowedRootFields.has(field))
|
|
416
|
+
issues.push(`${label}: unsupported top-level field ${field}`);
|
|
417
|
+
}
|
|
418
|
+
if (recipe.$schema !== RECIPE_SCHEMA_URL) {
|
|
419
|
+
issues.push(`${label}: $schema must equal ${RECIPE_SCHEMA_URL}`);
|
|
420
|
+
}
|
|
421
|
+
if (typeof recipe.description !== 'string' || !recipe.description.trim()) {
|
|
422
|
+
issues.push(`${label}: description must be a non-empty string`);
|
|
423
|
+
}
|
|
424
|
+
if (!isRecord(recipe.workflow)) {
|
|
425
|
+
issues.push(`${label}: workflow is required`);
|
|
314
426
|
return;
|
|
315
427
|
}
|
|
316
|
-
const workflow = recipe.
|
|
428
|
+
const workflow = recipe.workflow;
|
|
317
429
|
if (typeof workflow.entry !== 'string' || !workflow.entry.trim()) {
|
|
318
|
-
issues.push(`${label}:
|
|
430
|
+
issues.push(`${label}: workflow.entry must be a non-empty string`);
|
|
319
431
|
}
|
|
320
432
|
if (!isRecord(workflow.nodes) || Object.keys(workflow.nodes).length === 0) {
|
|
321
|
-
issues.push(`${label}:
|
|
433
|
+
issues.push(`${label}: workflow.nodes must be a non-empty object`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
437
|
+
if (!isRecord(node) || typeof node.action !== 'string' || !node.action.trim()) {
|
|
438
|
+
issues.push(`${label}: workflow.nodes.${nodeId}.action must be a non-empty string`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (node.action !== 'end' && (typeof node.intent !== 'string' || !node.intent.trim())) {
|
|
442
|
+
issues.push(`${label}: workflow.nodes.${nodeId}.intent must be a non-empty string`);
|
|
443
|
+
}
|
|
322
444
|
}
|
|
323
445
|
}
|
|
324
446
|
|
|
447
|
+
function listArtifactPaths(root) {
|
|
448
|
+
if (!existsSync(root)) return [];
|
|
449
|
+
const paths = [];
|
|
450
|
+
const visit = (dir, prefix) => {
|
|
451
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
452
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
453
|
+
if (entry.isDirectory()) visit(path.join(dir, entry.name), relative);
|
|
454
|
+
else if (entry.isFile()) paths.push(relative);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
visit(root, '');
|
|
458
|
+
return paths.sort();
|
|
459
|
+
}
|
|
460
|
+
|
|
325
461
|
function parseManifest() {
|
|
326
462
|
const text = readText('artifacts/evidence-manifest.json');
|
|
327
463
|
if (!text) return null;
|