@farmslot/recipe-harness 0.1.0 → 0.2.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 (86) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/README.md +30 -11
  3. package/bin/farmslot-recipe.mjs +1 -1
  4. package/dist/adapters/core.d.ts +1 -1
  5. package/dist/adapters/core.d.ts.map +1 -1
  6. package/dist/adapters/core.js +1 -1
  7. package/dist/adapters/core.js.map +1 -1
  8. package/dist/adapters/ui.d.ts +1 -1
  9. package/dist/adapters/ui.d.ts.map +1 -1
  10. package/dist/{cli.d.ts → cli/index.d.ts} +1 -1
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/{cli.js → cli/index.js} +3 -3
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/cli/run-command.d.ts.map +1 -1
  15. package/dist/cli/run-command.js +2 -2
  16. package/dist/cli/run-command.js.map +1 -1
  17. package/dist/{cli-support.d.ts → cli/support.d.ts} +3 -3
  18. package/dist/cli/support.d.ts.map +1 -0
  19. package/dist/{cli-support.js → cli/support.js} +3 -3
  20. package/dist/cli/support.js.map +1 -0
  21. package/dist/cli/validate-command.js +2 -2
  22. package/dist/cli/validate-command.js.map +1 -1
  23. package/dist/core/flows.d.ts +23 -0
  24. package/dist/core/flows.d.ts.map +1 -0
  25. package/dist/core/flows.js +274 -0
  26. package/dist/core/flows.js.map +1 -0
  27. package/dist/core/graph.d.ts +12 -0
  28. package/dist/core/graph.d.ts.map +1 -0
  29. package/dist/core/graph.js +116 -0
  30. package/dist/core/graph.js.map +1 -0
  31. package/dist/core/hud.d.ts +11 -0
  32. package/dist/core/hud.d.ts.map +1 -0
  33. package/dist/core/hud.js +129 -0
  34. package/dist/core/hud.js.map +1 -0
  35. package/dist/core/json.d.ts.map +1 -0
  36. package/dist/core/json.js.map +1 -0
  37. package/dist/core/predicates.d.ts +7 -0
  38. package/dist/core/predicates.d.ts.map +1 -0
  39. package/dist/core/predicates.js +102 -0
  40. package/dist/core/predicates.js.map +1 -0
  41. package/dist/core/runner.d.ts.map +1 -0
  42. package/dist/core/runner.js +532 -0
  43. package/dist/core/runner.js.map +1 -0
  44. package/dist/core/trace.d.ts +5 -0
  45. package/dist/core/trace.d.ts.map +1 -0
  46. package/dist/core/trace.js +23 -0
  47. package/dist/core/trace.js.map +1 -0
  48. package/dist/core/types.d.ts.map +1 -0
  49. package/dist/core/types.js.map +1 -0
  50. package/dist/core/validation.d.ts +4 -0
  51. package/dist/core/validation.d.ts.map +1 -0
  52. package/dist/core/validation.js +18 -0
  53. package/dist/core/validation.js.map +1 -0
  54. package/dist/index.d.ts +2 -11
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -7
  57. package/dist/index.js.map +1 -1
  58. package/dist/{writers.d.ts → node/writers.d.ts} +1 -1
  59. package/dist/node/writers.d.ts.map +1 -0
  60. package/dist/{writers.js → node/writers.js} +1 -1
  61. package/dist/node/writers.js.map +1 -0
  62. package/dist/runtime/cdp.d.ts +1 -1
  63. package/dist/runtime/cdp.d.ts.map +1 -1
  64. package/dist/runtime/cdp.js +1 -1
  65. package/dist/runtime/cdp.js.map +1 -1
  66. package/dist/runtime/react-native-bridge.d.ts +1 -1
  67. package/dist/runtime/react-native-bridge.d.ts.map +1 -1
  68. package/package.json +56 -15
  69. package/dist/cli-support.d.ts.map +0 -1
  70. package/dist/cli-support.js.map +0 -1
  71. package/dist/cli.d.ts.map +0 -1
  72. package/dist/cli.js.map +0 -1
  73. package/dist/json.d.ts.map +0 -1
  74. package/dist/json.js.map +0 -1
  75. package/dist/runner.d.ts.map +0 -1
  76. package/dist/runner.js +0 -1172
  77. package/dist/runner.js.map +0 -1
  78. package/dist/types.d.ts.map +0 -1
  79. package/dist/types.js.map +0 -1
  80. package/dist/writers.d.ts.map +0 -1
  81. package/dist/writers.js.map +0 -1
  82. /package/dist/{json.d.ts → core/json.d.ts} +0 -0
  83. /package/dist/{json.js → core/json.js} +0 -0
  84. /package/dist/{runner.d.ts → core/runner.d.ts} +0 -0
  85. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  86. /package/dist/{types.js → core/types.js} +0 -0
@@ -0,0 +1,102 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { isRecord } from './json.js';
3
+ export function evaluateNodeGate(node, outputs) {
4
+ const root = Object.fromEntries(outputs);
5
+ if (node.when != null && !evaluatePredicate(root, node.when)) {
6
+ return { run: false, reason: 'when predicate was false' };
7
+ }
8
+ if (node.unless != null && evaluatePredicate(root, node.unless)) {
9
+ return { run: false, reason: 'unless predicate was true' };
10
+ }
11
+ return { run: true };
12
+ }
13
+ export function evaluatePredicate(root, predicate) {
14
+ if (!isRecord(predicate))
15
+ throw new Error('Predicate must be an assertion object.');
16
+ if (Array.isArray(predicate.all))
17
+ return predicate.all.every((entry) => evaluatePredicate(root, entry));
18
+ if (Array.isArray(predicate.any))
19
+ return predicate.any.some((entry) => evaluatePredicate(root, entry));
20
+ if (Array.isArray(predicate.none))
21
+ return !predicate.none.some((entry) => evaluatePredicate(root, entry));
22
+ const pathValue = typeof predicate.path === 'string' ? predicate.path : '$';
23
+ const actual = pathValue === '$' ? root : getPathValue(root, pathValue.replace(/^\$\./, ''));
24
+ const operator = typeof predicate.operator === 'string' ? predicate.operator : 'truthy';
25
+ const expected = predicate.value;
26
+ switch (operator) {
27
+ case 'exists':
28
+ return actual !== undefined;
29
+ case 'not_null':
30
+ return actual != null;
31
+ case 'truthy':
32
+ return Boolean(actual);
33
+ case 'falsy':
34
+ return !actual;
35
+ case 'eq':
36
+ return actual === expected;
37
+ case 'ne':
38
+ case 'neq':
39
+ return actual !== expected;
40
+ case 'deep_eq':
41
+ return isDeepStrictEqual(actual, expected);
42
+ case 'contains':
43
+ return containsValue(actual, expected);
44
+ case 'not_contains':
45
+ return !containsValue(actual, expected);
46
+ case 'matches':
47
+ return typeof expected === 'string' && new RegExp(expected).test(String(actual ?? ''));
48
+ case 'one_of':
49
+ return Array.isArray(expected) && expected.some((entry) => isDeepStrictEqual(entry, actual));
50
+ case 'gt':
51
+ case 'gte':
52
+ case 'lt':
53
+ case 'lte':
54
+ return compareNumbers(actual, expected, operator);
55
+ case 'length_eq':
56
+ case 'length_gt':
57
+ case 'length_gte':
58
+ case 'length_lt':
59
+ case 'length_lte':
60
+ return compareNumbers(lengthOf(actual), expected, operator.replace('length_', ''));
61
+ default:
62
+ throw new Error(`Unsupported predicate operator ${operator}.`);
63
+ }
64
+ }
65
+ export function getPathValue(root, dottedPath) {
66
+ return dottedPath
67
+ .split('.')
68
+ .filter(Boolean)
69
+ .reduce((current, segment) => {
70
+ if (!isRecord(current) && !Array.isArray(current))
71
+ return undefined;
72
+ if (Array.isArray(current))
73
+ return current[Number(segment)];
74
+ return current[segment];
75
+ }, root);
76
+ }
77
+ function containsValue(actual, expected) {
78
+ if (typeof actual === 'string')
79
+ return actual.includes(String(expected));
80
+ if (Array.isArray(actual))
81
+ return actual.some((entry) => isDeepStrictEqual(entry, expected));
82
+ return false;
83
+ }
84
+ function lengthOf(actual) {
85
+ if (typeof actual === 'string' || Array.isArray(actual))
86
+ return actual.length;
87
+ if (isRecord(actual))
88
+ return Object.keys(actual).length;
89
+ return Number.NaN;
90
+ }
91
+ function compareNumbers(actual, expected, operator) {
92
+ if (typeof actual !== 'number' || typeof expected !== 'number')
93
+ return false;
94
+ if (operator === 'gt')
95
+ return actual > expected;
96
+ if (operator === 'gte')
97
+ return actual >= expected;
98
+ if (operator === 'lt')
99
+ return actual < expected;
100
+ return actual <= expected;
101
+ }
102
+ //# sourceMappingURL=predicates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"predicates.js","sourceRoot":"","sources":["../../src/core/predicates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,MAAM,UAAU,gBAAgB,CAC9B,IAA6B,EAC7B,OAAqC;IAErC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7D,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAC;IAC5D,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,IAAI,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAChE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC;IAC7D,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAa,EAAE,SAAkB;IACjE,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACpF,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC;QAC9B,OAAO,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IACxE,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC;QAC9B,OAAO,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC;QAC/B,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IAEzE,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5E,MAAM,MAAM,GAAG,SAAS,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7F,MAAM,QAAQ,GAAG,OAAO,SAAS,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxF,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC;IACjC,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,MAAM,KAAK,SAAS,CAAC;QAC9B,KAAK,UAAU;YACb,OAAO,MAAM,IAAI,IAAI,CAAC;QACxB,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;QACzB,KAAK,OAAO;YACV,OAAO,CAAC,MAAM,CAAC;QACjB,KAAK,IAAI;YACP,OAAO,MAAM,KAAK,QAAQ,CAAC;QAC7B,KAAK,IAAI,CAAC;QACV,KAAK,KAAK;YACR,OAAO,MAAM,KAAK,QAAQ,CAAC;QAC7B,KAAK,SAAS;YACZ,OAAO,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAC7C,KAAK,UAAU;YACb,OAAO,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACzC,KAAK,cAAc;YACjB,OAAO,CAAC,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAC1C,KAAK,SAAS;YACZ,OAAO,OAAO,QAAQ,KAAK,QAAQ,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC;QACzF,KAAK,QAAQ;YACX,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;QAC/F,KAAK,IAAI,CAAC;QACV,KAAK,KAAK,CAAC;QACX,KAAK,IAAI,CAAC;QACV,KAAK,KAAK;YACR,OAAO,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACpD,KAAK,WAAW,CAAC;QACjB,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY,CAAC;QAClB,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY;YACf,OAAO,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QACrF;YACE,MAAM,IAAI,KAAK,CAAC,kCAAkC,QAAQ,GAAG,CAAC,CAAC;IACnE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAa,EAAE,UAAkB;IAC5D,OAAO,UAAU;SACd,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,OAAO,CAAC;SACf,MAAM,CAAU,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;QACpC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,OAAO,SAAS,CAAC;QACpE,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QAC5D,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1B,CAAC,EAAE,IAAI,CAAC,CAAC;AACb,CAAC;AAED,SAAS,aAAa,CAAC,MAAe,EAAE,QAAiB;IACvD,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzE,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC7F,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,MAAe;IAC/B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC;IAC9E,IAAI,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IACxD,OAAO,MAAM,CAAC,GAAG,CAAC;AACpB,CAAC;AAED,SAAS,cAAc,CAAC,MAAe,EAAE,QAAiB,EAAE,QAAgB;IAC1E,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC7E,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,MAAM,GAAG,QAAQ,CAAC;IAChD,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,MAAM,IAAI,QAAQ,CAAC;IAClD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,MAAM,GAAG,QAAQ,CAAC;IAChD,OAAO,MAAM,IAAI,QAAQ,CAAC;AAC5B,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/core/runner.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,aAAa,EACb,yBAAyB,EAIzB,YAAY,EAKb,MAAM,YAAY,CAAC;AAapB,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAGzE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,YAAY,CA8CnF"}
@@ -0,0 +1,532 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getRecipeActionManifestActionNames, validateRecipeArtifactPackage, } from '@farmslot/protocol';
4
+ import { JsonArtifactWriter, JsonSummaryWriter, JsonTraceWriter } from '../node/writers.js';
5
+ import { collectFlows, executeInlineFlowCall } from './flows.js';
6
+ import { extractWorkflowGraph, normalizePreconditionResult, resolveNextNode, } from './graph.js';
7
+ import { buildHudNode } from './hud.js';
8
+ import { isRecord, normalizeRelativePath, readJsonFile } from './json.js';
9
+ import { evaluateNodeGate } from './predicates.js';
10
+ import { recordSyntheticFailure, traceNodeMetadata } from './trace.js';
11
+ import { assertManifestIsValid, assertRecipeMatchesManifest } from './validation.js';
12
+ const HARNESS_VERSION = '0.1.0';
13
+ const RUNNER_BUILT_IN_ACTIONS = new Set(['call']);
14
+ const DEFAULT_MAX_FLOW_CALL_DEPTH = 8;
15
+ const noopLogger = {
16
+ info() { },
17
+ warn() { },
18
+ error() { },
19
+ };
20
+ export function defineActionAdapter(adapter) {
21
+ if (!adapter.action.trim())
22
+ throw new Error('Action adapter action must be a non-empty string.');
23
+ return adapter;
24
+ }
25
+ export function createRecipeRunner(options) {
26
+ assertManifestIsValid(options.actionManifest);
27
+ const declaredActions = new Set(getRecipeActionManifestActionNames(options.actionManifest));
28
+ const adapterMap = new Map();
29
+ const preconditionMap = new Map();
30
+ const declaredPreconditions = new Set((options.actionManifest.pre_conditions ?? []).map((entry) => entry.id));
31
+ for (const checker of options.preconditions ?? []) {
32
+ if (!checker.id.trim())
33
+ throw new Error('Precondition checker id must be a non-empty string.');
34
+ if (!declaredPreconditions.has(checker.id)) {
35
+ throw new Error(`Precondition checker ${checker.id} is not declared by the action manifest.`);
36
+ }
37
+ if (preconditionMap.has(checker.id)) {
38
+ throw new Error(`Precondition checker ${checker.id} is registered more than once.`);
39
+ }
40
+ preconditionMap.set(checker.id, checker);
41
+ }
42
+ for (const adapter of options.adapters) {
43
+ if (!adapter.action.trim())
44
+ throw new Error('Action adapter action must be a non-empty string.');
45
+ if (!declaredActions.has(adapter.action) && adapter.testOnly !== true) {
46
+ throw new Error(`Adapter ${adapter.action} is not declared by the recipe action manifest.`);
47
+ }
48
+ if (adapterMap.has(adapter.action)) {
49
+ throw new Error(`Adapter ${adapter.action} is registered more than once.`);
50
+ }
51
+ adapterMap.set(adapter.action, adapter);
52
+ }
53
+ for (const action of declaredActions) {
54
+ if (!adapterMap.has(action) && !RUNNER_BUILT_IN_ACTIONS.has(action)) {
55
+ throw new Error(`Manifest action ${action} has no registered adapter.`);
56
+ }
57
+ }
58
+ return new DefaultRecipeRunner(options.actionManifest, adapterMap, preconditionMap, options.logger ?? noopLogger, options.hud, options.runner);
59
+ }
60
+ class DefaultRecipeRunner {
61
+ #actionManifest;
62
+ #adapters;
63
+ #preconditions;
64
+ #logger;
65
+ #hud;
66
+ #runnerProvenance;
67
+ constructor(actionManifest, adapters, preconditions, logger, hud, runnerProvenance) {
68
+ this.#actionManifest = actionManifest;
69
+ this.#adapters = adapters;
70
+ this.#preconditions = preconditions;
71
+ this.#logger = logger;
72
+ this.#hud = hud;
73
+ this.#runnerProvenance = runnerProvenance;
74
+ }
75
+ async run(request) {
76
+ if (!request.recipePath && request.recipeDocument == null) {
77
+ throw new Error('Recipe run requires recipePath or recipeDocument.');
78
+ }
79
+ const projectRoot = path.resolve(request.projectRoot ?? process.cwd());
80
+ const artifactsDir = path.resolve(request.artifactsDir);
81
+ const startedAt = new Date();
82
+ await mkdir(artifactsDir, { recursive: true });
83
+ const sourceRecipePath = request.recipePath ? path.resolve(request.recipePath) : undefined;
84
+ const recipe = request.recipeDocument ?? (await readJsonFile(sourceRecipePath));
85
+ assertRecipeMatchesManifest(recipe, this.#actionManifest);
86
+ const graph = extractWorkflowGraph(recipe);
87
+ const flowCatalog = await collectFlows(recipe, {
88
+ projectRoot,
89
+ recipeDir: sourceRecipePath ? path.dirname(sourceRecipePath) : projectRoot,
90
+ });
91
+ const artifactWriter = new JsonArtifactWriter(artifactsDir);
92
+ const traceWriter = new JsonTraceWriter(artifactsDir, this.#runnerProvenance);
93
+ const summaryWriter = new JsonSummaryWriter(artifactsDir);
94
+ const outputs = new Map();
95
+ const recipePath = await artifactWriter.copyRecipe(recipe);
96
+ let status = 'unknown';
97
+ let currentNodeId = graph.entry;
98
+ let mainStatus = 'unknown';
99
+ let runningTeardown = false;
100
+ const visited = new Set();
101
+ let transitionCount = 0;
102
+ const maxTransitions = Object.keys(graph.nodes).length * 3;
103
+ const preconditionStatus = await this.#runPreconditions({
104
+ graph,
105
+ recipe,
106
+ projectRoot,
107
+ artifactsDir,
108
+ request,
109
+ outputs,
110
+ artifactWriter,
111
+ traceWriter,
112
+ });
113
+ if (preconditionStatus === 'fail') {
114
+ status = 'fail';
115
+ currentNodeId = undefined;
116
+ }
117
+ while (currentNodeId) {
118
+ const activeNodeId = currentNodeId;
119
+ transitionCount += 1;
120
+ if (transitionCount > maxTransitions) {
121
+ const error = new Error('Recipe graph exceeded its maximum transition count.');
122
+ recordSyntheticFailure(traceWriter, activeNodeId, error);
123
+ status = 'fail';
124
+ break;
125
+ }
126
+ visited.add(activeNodeId);
127
+ const node = graph.nodes[activeNodeId];
128
+ if (!node) {
129
+ const error = new Error(`Recipe node ${activeNodeId} does not exist.`);
130
+ recordSyntheticFailure(traceWriter, activeNodeId, error);
131
+ status = 'fail';
132
+ break;
133
+ }
134
+ const action = String(node.action);
135
+ const adapter = action === 'call' ? undefined : this.#adapters.get(action);
136
+ if (action !== 'call' && !adapter) {
137
+ const error = new Error(`No adapter registered for action ${action}.`);
138
+ recordSyntheticFailure(traceWriter, activeNodeId, error, action);
139
+ status = 'fail';
140
+ break;
141
+ }
142
+ const nodeStartedAt = new Date();
143
+ const context = {
144
+ nodeId: activeNodeId,
145
+ recipe,
146
+ projectRoot,
147
+ artifactsDir,
148
+ env: request.env ?? {},
149
+ outputs,
150
+ getOutput(nodeId) {
151
+ if (!outputs.has(nodeId))
152
+ throw new Error(`No output recorded for node ${nodeId}.`);
153
+ return outputs.get(nodeId);
154
+ },
155
+ resolveProjectPath(relativePath) {
156
+ return path.join(projectRoot, normalizeRelativePath(relativePath));
157
+ },
158
+ resolveArtifactPath(relativePath) {
159
+ return path.join(artifactsDir, normalizeRelativePath(relativePath));
160
+ },
161
+ registerArtifact(entry) {
162
+ artifactWriter.register(entry);
163
+ },
164
+ logger: this.#logger,
165
+ };
166
+ try {
167
+ const gate = evaluateNodeGate(node, context.outputs);
168
+ if (!gate.run) {
169
+ const next = resolveNextNode(node, {});
170
+ traceWriter.record({
171
+ nodeId: activeNodeId,
172
+ action,
173
+ ...traceNodeMetadata(node),
174
+ startedAt: nodeStartedAt.toISOString(),
175
+ endedAt: new Date().toISOString(),
176
+ durationMs: Date.now() - nodeStartedAt.getTime(),
177
+ ok: true,
178
+ next,
179
+ output: { skipped: true, reason: gate.reason },
180
+ });
181
+ currentNodeId = next;
182
+ continue;
183
+ }
184
+ const hudStarted = await this.#publishHudProgressOrRecord(traceWriter, 'running', {
185
+ nodeId: activeNodeId,
186
+ action,
187
+ node,
188
+ recipe,
189
+ index: transitionCount,
190
+ total: Object.keys(graph.nodes).length,
191
+ context,
192
+ });
193
+ if (!hudStarted) {
194
+ status = 'fail';
195
+ break;
196
+ }
197
+ const result = action === 'call'
198
+ ? await executeInlineFlowCall({
199
+ callNodeId: activeNodeId,
200
+ node,
201
+ context,
202
+ flowCatalog,
203
+ adapters: this.#adapters,
204
+ traceWriter,
205
+ callStack: [],
206
+ maxCallDepth: DEFAULT_MAX_FLOW_CALL_DEPTH,
207
+ })
208
+ : await adapter.execute(node, context);
209
+ if (result.output !== undefined)
210
+ outputs.set(activeNodeId, result.output);
211
+ for (const artifact of result.artifacts ?? [])
212
+ artifactWriter.register(artifact);
213
+ const next = resolveNextNode(node, result);
214
+ traceWriter.record({
215
+ nodeId: activeNodeId,
216
+ action,
217
+ ...traceNodeMetadata(node, result),
218
+ startedAt: nodeStartedAt.toISOString(),
219
+ endedAt: new Date().toISOString(),
220
+ durationMs: Date.now() - nodeStartedAt.getTime(),
221
+ ok: true,
222
+ next,
223
+ status: result.status,
224
+ output: result.output,
225
+ });
226
+ const hudCompleted = await this.#publishHudProgressOrRecord(traceWriter, result.status === 'fail' ? 'fail' : 'pass', {
227
+ nodeId: activeNodeId,
228
+ action,
229
+ node,
230
+ recipe,
231
+ index: transitionCount,
232
+ total: Object.keys(graph.nodes).length,
233
+ context,
234
+ });
235
+ if (!hudCompleted) {
236
+ status = 'fail';
237
+ break;
238
+ }
239
+ if (result.status) {
240
+ if (runningTeardown) {
241
+ status = mainStatus === 'fail' ? 'fail' : result.status;
242
+ break;
243
+ }
244
+ mainStatus = result.status;
245
+ if (graph.teardownEntry) {
246
+ runningTeardown = true;
247
+ currentNodeId = graph.teardownEntry;
248
+ continue;
249
+ }
250
+ status = result.status;
251
+ break;
252
+ }
253
+ currentNodeId = next;
254
+ }
255
+ catch (error) {
256
+ const message = error instanceof Error ? error.message : String(error);
257
+ traceWriter.record({
258
+ nodeId: activeNodeId,
259
+ action,
260
+ ...traceNodeMetadata(node),
261
+ startedAt: nodeStartedAt.toISOString(),
262
+ endedAt: new Date().toISOString(),
263
+ durationMs: Date.now() - nodeStartedAt.getTime(),
264
+ ok: false,
265
+ error: message,
266
+ });
267
+ this.#logger.error(message);
268
+ await this.#publishHudProgressOrRecord(traceWriter, 'fail', {
269
+ nodeId: activeNodeId,
270
+ action,
271
+ node,
272
+ recipe,
273
+ index: transitionCount,
274
+ total: Object.keys(graph.nodes).length,
275
+ context,
276
+ error: message,
277
+ });
278
+ status = 'fail';
279
+ if (!runningTeardown && graph.teardownEntry) {
280
+ mainStatus = 'fail';
281
+ runningTeardown = true;
282
+ currentNodeId = graph.teardownEntry;
283
+ continue;
284
+ }
285
+ break;
286
+ }
287
+ }
288
+ artifactWriter.register({
289
+ path: 'summary.json',
290
+ type: 'summary',
291
+ label: 'Run summary',
292
+ category: 'system',
293
+ });
294
+ artifactWriter.register({
295
+ path: 'trace.json',
296
+ type: 'trace',
297
+ label: 'Execution trace',
298
+ category: 'system',
299
+ });
300
+ if (status === 'pass') {
301
+ const runHudStartedAt = new Date();
302
+ try {
303
+ await this.#publishRunHud(status, {
304
+ recipe,
305
+ projectRoot,
306
+ artifactsDir,
307
+ env: request.env ?? {},
308
+ outputs,
309
+ });
310
+ }
311
+ catch (error) {
312
+ const message = error instanceof Error ? error.message : String(error);
313
+ traceWriter.record({
314
+ nodeId: 'recipe-complete:hud',
315
+ action: 'app.hud',
316
+ startedAt: runHudStartedAt.toISOString(),
317
+ endedAt: new Date().toISOString(),
318
+ durationMs: Date.now() - runHudStartedAt.getTime(),
319
+ ok: false,
320
+ error: message,
321
+ });
322
+ this.#logger.error(`app.hud complete update failed: ${message}`);
323
+ status = 'fail';
324
+ }
325
+ }
326
+ const endedAt = new Date();
327
+ const tracePath = await traceWriter.write();
328
+ const trace = traceWriter.list();
329
+ const summary = {
330
+ status,
331
+ total: trace.length,
332
+ passed: trace.filter((entry) => entry.ok).length,
333
+ failed: trace.filter((entry) => !entry.ok).length,
334
+ startedAt: startedAt.toISOString(),
335
+ endedAt: endedAt.toISOString(),
336
+ durationMs: endedAt.getTime() - startedAt.getTime(),
337
+ harness: {
338
+ name: '@farmslot/recipe-harness',
339
+ version: HARNESS_VERSION,
340
+ runner_protocol_version: this.#actionManifest.runner_protocol_version,
341
+ action_registry_version: this.#actionManifest.action_registry_version,
342
+ },
343
+ ...(this.#runnerProvenance ? { runner: this.#runnerProvenance } : {}),
344
+ };
345
+ const summaryPath = await summaryWriter.write(summary);
346
+ const artifactManifestPath = await artifactWriter.write(status, this.#runnerProvenance);
347
+ const packageValidation = validateRecipeArtifactPackage({
348
+ recipe,
349
+ manifest: {
350
+ version: 1,
351
+ runStatus: status,
352
+ ...(this.#runnerProvenance ? { provenance: { runner: this.#runnerProvenance } } : {}),
353
+ artifacts: artifactWriter.list(),
354
+ },
355
+ artifactPaths: [
356
+ 'recipe.json',
357
+ 'summary.json',
358
+ 'trace.json',
359
+ 'artifact-manifest.json',
360
+ ...artifactWriter.list().map((entry) => entry.path),
361
+ ],
362
+ });
363
+ if (packageValidation.status === 'invalid') {
364
+ throw new Error(`Generated artifact package is invalid: ${packageValidation.findings
365
+ .map((finding) => `${finding.code} ${finding.path}`)
366
+ .join(', ')}`);
367
+ }
368
+ return { status, summaryPath, tracePath, artifactManifestPath, recipePath };
369
+ }
370
+ async #runPreconditions({ graph, recipe, projectRoot, artifactsDir, request, outputs, artifactWriter, traceWriter, }) {
371
+ for (const gate of graph.preconditions) {
372
+ const nodeId = `pre_conditions:${gate.id}`;
373
+ const checker = this.#preconditions.get(gate.id);
374
+ if (!checker) {
375
+ recordSyntheticFailure(traceWriter, nodeId, new Error(`Precondition ${gate.id} is declared by the recipe but has no checker registered.`), 'pre_condition');
376
+ return 'fail';
377
+ }
378
+ const startedAt = new Date();
379
+ const context = this.#createExecutionContext({
380
+ nodeId,
381
+ recipe,
382
+ projectRoot,
383
+ artifactsDir,
384
+ env: request.env ?? {},
385
+ outputs,
386
+ artifactWriter,
387
+ });
388
+ try {
389
+ const rawResult = await checker.execute(gate, context);
390
+ const result = normalizePreconditionResult(rawResult);
391
+ if (result.output !== undefined)
392
+ outputs.set(nodeId, result.output);
393
+ if (result.ok === false) {
394
+ throw new Error(result.error ?? `Precondition ${gate.id} failed.`);
395
+ }
396
+ traceWriter.record({
397
+ nodeId,
398
+ action: 'pre_condition',
399
+ startedAt: startedAt.toISOString(),
400
+ endedAt: new Date().toISOString(),
401
+ durationMs: Date.now() - startedAt.getTime(),
402
+ ok: true,
403
+ output: result.output,
404
+ });
405
+ }
406
+ catch (error) {
407
+ const message = error instanceof Error ? error.message : String(error);
408
+ traceWriter.record({
409
+ nodeId,
410
+ action: 'pre_condition',
411
+ startedAt: startedAt.toISOString(),
412
+ endedAt: new Date().toISOString(),
413
+ durationMs: Date.now() - startedAt.getTime(),
414
+ ok: false,
415
+ error: message,
416
+ });
417
+ return 'fail';
418
+ }
419
+ }
420
+ return 'pass';
421
+ }
422
+ #createExecutionContext({ nodeId, recipe, projectRoot, artifactsDir, env, outputs, artifactWriter, }) {
423
+ return {
424
+ nodeId,
425
+ recipe,
426
+ projectRoot,
427
+ artifactsDir,
428
+ env,
429
+ outputs,
430
+ getOutput(nodeId) {
431
+ if (!outputs.has(nodeId))
432
+ throw new Error(`No output recorded for node ${nodeId}.`);
433
+ return outputs.get(nodeId);
434
+ },
435
+ resolveProjectPath(relativePath) {
436
+ return path.join(projectRoot, normalizeRelativePath(relativePath));
437
+ },
438
+ resolveArtifactPath(relativePath) {
439
+ return path.join(artifactsDir, normalizeRelativePath(relativePath));
440
+ },
441
+ registerArtifact(entry) {
442
+ artifactWriter.register(entry);
443
+ },
444
+ logger: this.#logger,
445
+ };
446
+ }
447
+ #hudAction() {
448
+ if (this.#hud === false || this.#hud?.enabled === false)
449
+ return undefined;
450
+ return this.#adapters.has('app.hud') ? 'app.hud' : undefined;
451
+ }
452
+ async #publishHudProgressOrRecord(traceWriter, status, event) {
453
+ const startedAt = new Date();
454
+ try {
455
+ await this.#publishHudProgress(status, event);
456
+ return true;
457
+ }
458
+ catch (error) {
459
+ // HUD is first-class when advertised, so rendering failures fail the run.
460
+ // They are still recorded instead of aborting artifact package writing.
461
+ const message = error instanceof Error ? error.message : String(error);
462
+ traceWriter.record({
463
+ nodeId: `${event.nodeId}:hud:${status}`,
464
+ action: 'app.hud',
465
+ startedAt: startedAt.toISOString(),
466
+ endedAt: new Date().toISOString(),
467
+ durationMs: Date.now() - startedAt.getTime(),
468
+ ok: false,
469
+ error: message,
470
+ });
471
+ this.#logger.error(`app.hud ${status} update failed for ${event.nodeId}: ${message}`);
472
+ return false;
473
+ }
474
+ }
475
+ async #publishHudProgress(status, event) {
476
+ const hudAction = this.#hudAction();
477
+ if (!hudAction || event.action === hudAction)
478
+ return;
479
+ const adapter = this.#adapters.get(hudAction);
480
+ if (!adapter)
481
+ return;
482
+ await adapter.execute(buildHudNode(status, event, this.#hud), event.context);
483
+ }
484
+ async #publishRunHud(status, request) {
485
+ const hudAction = this.#hudAction();
486
+ if (!hudAction)
487
+ return;
488
+ const adapter = this.#adapters.get(hudAction);
489
+ if (!adapter)
490
+ return;
491
+ const context = {
492
+ nodeId: 'recipe-complete',
493
+ recipe: request.recipe,
494
+ projectRoot: request.projectRoot,
495
+ artifactsDir: request.artifactsDir,
496
+ env: request.env,
497
+ outputs: request.outputs,
498
+ getOutput(nodeId) {
499
+ if (!request.outputs.has(nodeId))
500
+ throw new Error(`No output recorded for node ${nodeId}.`);
501
+ return request.outputs.get(nodeId);
502
+ },
503
+ resolveProjectPath(relativePath) {
504
+ return path.join(request.projectRoot, normalizeRelativePath(relativePath));
505
+ },
506
+ resolveArtifactPath(relativePath) {
507
+ return path.join(request.artifactsDir, normalizeRelativePath(relativePath));
508
+ },
509
+ registerArtifact() { },
510
+ logger: this.#logger,
511
+ };
512
+ await adapter.execute({
513
+ action: hudAction,
514
+ title: this.#hudTitle(request.recipe),
515
+ status,
516
+ node_id: 'recipe-complete',
517
+ phase: 'complete',
518
+ flow: 'run',
519
+ detail: status === 'pass' ? 'Recipe completed' : 'Recipe failed',
520
+ text: status === 'pass' ? 'Recipe completed' : 'Recipe failed',
521
+ progress: { complete: true },
522
+ }, context);
523
+ }
524
+ #hudTitle(recipe) {
525
+ if (this.#hud && this.#hud.title)
526
+ return this.#hud.title;
527
+ if (isRecord(recipe) && typeof recipe.title === 'string')
528
+ return recipe.title;
529
+ return 'Recipe run';
530
+ }
531
+ }
532
+ //# sourceMappingURL=runner.js.map