@farmslot/agent-runtime 0.1.1 → 0.3.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/farmslot-agent.mjs +6 -0
  3. package/dist/execution-template/create.d.ts +7 -0
  4. package/dist/execution-template/create.d.ts.map +1 -0
  5. package/dist/execution-template/create.js +63 -0
  6. package/dist/execution-template/create.js.map +1 -0
  7. package/dist/execution-template/execution-template.test.d.ts +2 -0
  8. package/dist/execution-template/execution-template.test.d.ts.map +1 -0
  9. package/dist/execution-template/execution-template.test.js +340 -0
  10. package/dist/execution-template/execution-template.test.js.map +1 -0
  11. package/dist/execution-template/frontmatter.d.ts +16 -0
  12. package/dist/execution-template/frontmatter.d.ts.map +1 -0
  13. package/dist/execution-template/frontmatter.js +110 -0
  14. package/dist/execution-template/frontmatter.js.map +1 -0
  15. package/dist/execution-template/index.d.ts +7 -0
  16. package/dist/execution-template/index.d.ts.map +1 -0
  17. package/dist/execution-template/index.js +6 -0
  18. package/dist/execution-template/index.js.map +1 -0
  19. package/dist/execution-template/infer.d.ts +25 -0
  20. package/dist/execution-template/infer.d.ts.map +1 -0
  21. package/dist/execution-template/infer.js +122 -0
  22. package/dist/execution-template/infer.js.map +1 -0
  23. package/dist/execution-template/lint.d.ts +6 -0
  24. package/dist/execution-template/lint.d.ts.map +1 -0
  25. package/dist/execution-template/lint.js +203 -0
  26. package/dist/execution-template/lint.js.map +1 -0
  27. package/dist/execution-template/resolve.d.ts +12 -0
  28. package/dist/execution-template/resolve.d.ts.map +1 -0
  29. package/dist/execution-template/resolve.js +126 -0
  30. package/dist/execution-template/resolve.js.map +1 -0
  31. package/dist/execution-template/types.d.ts +66 -0
  32. package/dist/execution-template/types.d.ts.map +1 -0
  33. package/dist/execution-template/types.js +3 -0
  34. package/dist/execution-template/types.js.map +1 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/package.json +6 -4
  40. package/scripts/check-task-artifact-contract.mjs +250 -8
  41. package/scripts/execution-template-cli.mjs +203 -0
  42. package/scripts/mark-checklist-step.cjs +26 -8
  43. package/scripts/worker-terminal-contract.cjs +23 -21
@@ -0,0 +1,126 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { inferTemplateMetadata } from './infer.js';
4
+ const SOURCE_KIND_RANK = {
5
+ custom: 0,
6
+ project: 1,
7
+ workspace: 2,
8
+ user: 3,
9
+ package: 4,
10
+ fallback: 5,
11
+ };
12
+ function listMarkdownFiles(root, layout) {
13
+ if (!existsSync(root) || !statSync(root).isDirectory())
14
+ return [];
15
+ const out = [];
16
+ if (layout === 'worker-flat') {
17
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
18
+ if (entry.isFile() && entry.name.endsWith('.md')) {
19
+ out.push(path.join(root, entry.name));
20
+ }
21
+ }
22
+ return out.sort((a, b) => a.localeCompare(b));
23
+ }
24
+ // flow-tree: <root>/<flow>/*.md (one level under each flow dir)
25
+ for (const flowEntry of readdirSync(root, { withFileTypes: true })) {
26
+ if (!flowEntry.isDirectory())
27
+ continue;
28
+ const flowDir = path.join(root, flowEntry.name);
29
+ for (const fileEntry of readdirSync(flowDir, { withFileTypes: true })) {
30
+ if (fileEntry.isFile() && fileEntry.name.endsWith('.md')) {
31
+ out.push(path.join(flowDir, fileEntry.name));
32
+ }
33
+ }
34
+ }
35
+ return out.sort((a, b) => a.localeCompare(b));
36
+ }
37
+ function matchesFilters(entry, options) {
38
+ if (options.flow && entry.flow !== options.flow)
39
+ return false;
40
+ if (options.runMode && entry.runMode !== options.runMode)
41
+ return false;
42
+ if (options.platform) {
43
+ const platforms = entry.platforms;
44
+ if (!platforms.includes('*') && !platforms.includes(options.platform))
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ /**
50
+ * List execution templates across sources.
51
+ * Higher-precedence kinds win when ids collide; losers are marked `shadowedBy`.
52
+ */
53
+ export function listExecutionTemplates(options) {
54
+ const includeShadowed = options.includeShadowed !== false;
55
+ // Same-kind ties break by CALLER order (the order sources were provided),
56
+ // not alphabetical ids — the caller's ordering is the intent.
57
+ const sources = options.sources
58
+ .map((source, callerIndex) => ({ source, callerIndex }))
59
+ .sort((a, b) => {
60
+ const rank = SOURCE_KIND_RANK[a.source.kind] - SOURCE_KIND_RANK[b.source.kind];
61
+ if (rank !== 0)
62
+ return rank;
63
+ return a.callerIndex - b.callerIndex;
64
+ })
65
+ .map(({ source }) => source);
66
+ const winners = new Map();
67
+ const shadowed = [];
68
+ // Shadowing resolves BEFORE filters: a filtered-out winner must not let a
69
+ // lower-precedence duplicate become effective through the filter.
70
+ for (const source of sources) {
71
+ for (const absolutePath of listMarkdownFiles(source.root, source.layout)) {
72
+ const relativePath = path.relative(source.root, absolutePath);
73
+ const text = readFileSync(absolutePath, 'utf8');
74
+ const entry = inferTemplateMetadata({
75
+ absolutePath,
76
+ relativePath,
77
+ source,
78
+ text,
79
+ });
80
+ const existing = winners.get(entry.id);
81
+ if (!existing) {
82
+ winners.set(entry.id, entry);
83
+ continue;
84
+ }
85
+ shadowed.push({ ...entry, shadowedBy: existing.sourceId });
86
+ }
87
+ }
88
+ const result = [...winners.values()].filter((entry) => matchesFilters(entry, options));
89
+ if (includeShadowed) {
90
+ result.push(...shadowed.filter((entry) => matchesFilters(entry, options)));
91
+ }
92
+ return result.sort((a, b) => {
93
+ if (a.flow !== b.flow)
94
+ return a.flow.localeCompare(b.flow);
95
+ if (Boolean(a.shadowedBy) !== Boolean(b.shadowedBy))
96
+ return a.shadowedBy ? 1 : -1;
97
+ return a.id.localeCompare(b.id);
98
+ });
99
+ }
100
+ /** Convenience: build a project worker-flat source. */
101
+ export function projectWorkerTemplateSource(projectName, projectTemplatesDir) {
102
+ return {
103
+ id: `project:${projectName}`,
104
+ kind: 'project',
105
+ root: path.join(projectTemplatesDir, 'worker'),
106
+ layout: 'worker-flat',
107
+ };
108
+ }
109
+ /** Convenience: build a package flow-tree source (e.g. recipe-cook references/templates). */
110
+ export function packageFlowTreeTemplateSource(packageId, templatesRoot) {
111
+ return {
112
+ id: `package:${packageId}`,
113
+ kind: 'package',
114
+ root: templatesRoot,
115
+ layout: 'flow-tree',
116
+ };
117
+ }
118
+ export function customTemplateSource(customId, root, layout = 'flow-tree') {
119
+ return {
120
+ id: `custom:${customId}`,
121
+ kind: 'custom',
122
+ root,
123
+ layout,
124
+ };
125
+ }
126
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/execution-template/resolve.ts"],"names":[],"mappings":"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"}
@@ -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,3 @@
1
+ /** Shared Markdown execution-template catalog (ADR-049). */
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -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
@@ -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.1.1",
3
+ "version": "0.3.0",
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.7.4"
34
+ "@farmslot/protocol": "0.11.1"
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,22 +1,84 @@
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;
22
+ let sharedRecipeDocumentValidator = null;
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';
10
27
  try {
11
- ({ isRecipeQualityArtifact: sharedRecipeQualityValidator } =
12
- await import('@farmslot/protocol/contracts/recipes'));
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;
13
33
  } catch (error) {
34
+ if (!isMissingWorkspaceProtocolBuild(error, 'dist/contracts/recipes.js')) throw error;
14
35
  // Source checkouts run this script before @farmslot/protocol has emitted dist/.
15
36
  // Keep strict local validation instead of downgrading to the historical loose gate.
16
37
  if (process.env.FARMSLOT_DEBUG_AGENT_RUNTIME) {
17
38
  console.warn(`[agent-runtime] using local RecipeQualityArtifact fallback: ${error.message}`);
18
39
  }
19
40
  }
41
+ let recipeProtocol = null;
42
+ try {
43
+ recipeProtocol = await import('@farmslot/protocol/recipe');
44
+ } catch (error) {
45
+ if (!isMissingWorkspaceProtocolBuild(error, 'dist/recipe/index.js')) throw error;
46
+ // Source checkouts can run before @farmslot/protocol has emitted dist/.
47
+ // Keep strict local validation until the shared validator is available.
48
+ if (process.env.FARMSLOT_DEBUG_AGENT_RUNTIME) {
49
+ console.warn(`[agent-runtime] using local Recipe v1 fallback: ${error.message}`);
50
+ }
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
+ );
77
+ }
78
+ sharedRecipeDocumentValidator = recipeProtocol.validateRecipeDocument;
79
+ sharedRecipeArtifactPackageValidator = recipeProtocol.validateRecipeArtifactPackage;
80
+ sharedResolvedRecipeArtifactPath = recipeProtocol.resolvedRecipeArtifactPath;
81
+ }
20
82
 
21
83
  const taskDir = process.argv[2];
22
84
  const rawArgs = process.argv.slice(3);
@@ -66,15 +128,34 @@ const allowedOmitKeys = new Set(['file', 'reason']);
66
128
 
67
129
  function fileExists(rel) {
68
130
  try {
69
- return statSync(path.join(taskDir, rel)).isFile();
70
- } catch {
131
+ return statSync(resolveTaskArtifactPath(rel)).isFile();
132
+ } catch (error) {
133
+ if (!isMissingPathError(error)) throw error;
71
134
  return false;
72
135
  }
73
136
  }
74
137
 
75
138
  function readText(rel) {
76
- const p = path.join(taskDir, rel);
77
- return existsSync(p) ? readFileSync(p, 'utf8') : null;
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;
78
159
  }
79
160
 
80
161
  function isRecord(value) {
@@ -116,7 +197,7 @@ function isRecipeQualityArtifactFallback(value) {
116
197
  if (fields.project != null && typeof fields.project !== 'string') return false;
117
198
  if (
118
199
  fields.flow_type != null &&
119
- !['fix-bug', 'review-pr', 'dev', 'pr-complete', 'merge-main'].includes(fields.flow_type)
200
+ !['fix-bug', 'review-pr', 'dev', 'pr-complete', 'update-branch'].includes(fields.flow_type)
120
201
  )
121
202
  return false;
122
203
  if (fields.task_type != null && typeof fields.task_type !== 'string') return false;
@@ -226,6 +307,166 @@ function validateRecipeQualityArtifact() {
226
307
  }
227
308
  }
228
309
 
310
+ function validateRecipeDocumentArtifact() {
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 recipeResolution = readJsonArtifact(
329
+ `${packageRoot}/recipe-resolution.json`,
330
+ 'recipe-run/recipe-resolution.json',
331
+ );
332
+ const resolvedRecipes = {};
333
+ if (isRecord(recipeResolution) && Array.isArray(recipeResolution.dependencies)) {
334
+ for (const dependency of recipeResolution.dependencies) {
335
+ if (!isRecord(dependency)) continue;
336
+ const artifact = resolvedRecipeArtifactPath(dependency.digest);
337
+ if (!artifact) continue;
338
+ const document = readJsonArtifact(`${packageRoot}/${artifact}`, `recipe-run/${artifact}`);
339
+ if (document !== undefined) resolvedRecipes[String(dependency.digest)] = document;
340
+ }
341
+ }
342
+ if (sharedRecipeArtifactPackageValidator) {
343
+ const result = sharedRecipeArtifactPackageValidator({
344
+ recipe,
345
+ manifest,
346
+ recipeResolution,
347
+ resolvedRecipes,
348
+ artifactPaths: listArtifactPaths(path.join(taskDir, packageRoot)),
349
+ });
350
+ for (const finding of result.findings) {
351
+ if (finding.severity === 'error') {
352
+ issues.push(`${finding.code} ${finding.path}: ${finding.message}`);
353
+ }
354
+ }
355
+ return;
356
+ }
357
+ if (manifest === undefined) issues.push('artifact-manifest.json is missing');
358
+ if (recipeResolution === undefined) issues.push('recipe-resolution.json is missing');
359
+ const externalRecipeIds = new Set(
360
+ isRecord(recipeResolution) && Array.isArray(recipeResolution.dependencies)
361
+ ? recipeResolution.dependencies
362
+ .filter(isRecord)
363
+ .map((dependency) => dependency.ref)
364
+ .filter((ref) => typeof ref === 'string')
365
+ : [],
366
+ );
367
+ validateRecipeDocumentValue(recipe, 'recipe.json', { externalRecipeIds });
368
+ for (const [digest, document] of Object.entries(resolvedRecipes)) {
369
+ validateRecipeDocumentValue(document, `resolved recipe ${digest}`, { externalRecipeIds });
370
+ }
371
+ }
372
+
373
+ function canonicalJson(value) {
374
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
375
+ if (isRecord(value)) {
376
+ return `{${Object.entries(value)
377
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
378
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
379
+ .join(',')}}`;
380
+ }
381
+ return JSON.stringify(value);
382
+ }
383
+
384
+ function resolvedRecipeArtifactPath(digest) {
385
+ if (sharedResolvedRecipeArtifactPath) return sharedResolvedRecipeArtifactPath(digest);
386
+ if (typeof digest !== 'string' || !/^sha256:[a-f0-9]{64}$/u.test(digest)) return undefined;
387
+ return `resolved-recipes/${digest.slice('sha256:'.length)}.recipe.json`;
388
+ }
389
+
390
+ function readJsonArtifact(relPath, label) {
391
+ const text = readText(relPath);
392
+ if (!text) return undefined;
393
+ try {
394
+ return JSON.parse(text);
395
+ } catch (error) {
396
+ issues.push(`${label}: invalid JSON: ${error.message}`);
397
+ return undefined;
398
+ }
399
+ }
400
+
401
+ function validateRecipeDocumentValue(recipe, label, options) {
402
+ if (sharedRecipeDocumentValidator) {
403
+ const result = sharedRecipeDocumentValidator(recipe, options);
404
+ for (const finding of result.findings) {
405
+ if (finding.severity === 'error') {
406
+ issues.push(`${label}: ${finding.code} ${finding.path}: ${finding.message}`);
407
+ }
408
+ }
409
+ return;
410
+ }
411
+ if (!isRecord(recipe)) {
412
+ issues.push(`${label}: expected object`);
413
+ return;
414
+ }
415
+ const allowedRootFields = new Set([
416
+ '$schema',
417
+ 'title',
418
+ 'description',
419
+ 'paramsSchema',
420
+ 'proofTargets',
421
+ 'workflow',
422
+ ]);
423
+ for (const field of Object.keys(recipe)) {
424
+ if (!allowedRootFields.has(field))
425
+ issues.push(`${label}: unsupported top-level field ${field}`);
426
+ }
427
+ if (recipe.$schema !== RECIPE_SCHEMA_URL) {
428
+ issues.push(`${label}: $schema must equal ${RECIPE_SCHEMA_URL}`);
429
+ }
430
+ if (typeof recipe.description !== 'string' || !recipe.description.trim()) {
431
+ issues.push(`${label}: description must be a non-empty string`);
432
+ }
433
+ if (!isRecord(recipe.workflow)) {
434
+ issues.push(`${label}: workflow is required`);
435
+ return;
436
+ }
437
+ const workflow = recipe.workflow;
438
+ if (typeof workflow.entry !== 'string' || !workflow.entry.trim()) {
439
+ issues.push(`${label}: workflow.entry must be a non-empty string`);
440
+ }
441
+ if (!isRecord(workflow.nodes) || Object.keys(workflow.nodes).length === 0) {
442
+ issues.push(`${label}: workflow.nodes must be a non-empty object`);
443
+ return;
444
+ }
445
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
446
+ if (!isRecord(node) || typeof node.action !== 'string' || !node.action.trim()) {
447
+ issues.push(`${label}: workflow.nodes.${nodeId}.action must be a non-empty string`);
448
+ continue;
449
+ }
450
+ if (node.action !== 'end' && (typeof node.intent !== 'string' || !node.intent.trim())) {
451
+ issues.push(`${label}: workflow.nodes.${nodeId}.intent must be a non-empty string`);
452
+ }
453
+ }
454
+ }
455
+
456
+ function listArtifactPaths(root) {
457
+ if (!existsSync(root)) return [];
458
+ const paths = [];
459
+ const visit = (dir, prefix) => {
460
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
461
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
462
+ if (entry.isDirectory()) visit(path.join(dir, entry.name), relative);
463
+ else if (entry.isFile()) paths.push(relative);
464
+ }
465
+ };
466
+ visit(root, '');
467
+ return paths.sort();
468
+ }
469
+
229
470
  function parseManifest() {
230
471
  const text = readText('artifacts/evidence-manifest.json');
231
472
  if (!text) return null;
@@ -339,6 +580,7 @@ function parseManifest() {
339
580
  }
340
581
 
341
582
  const hasRecipe = fileExists('artifacts/recipe.json');
583
+ validateRecipeDocumentArtifact();
342
584
  if (
343
585
  hasRecipe &&
344
586
  flags.has('--require-recipe-quality-if-recipe') &&