@farmslot/recipe-cli 0.4.1 → 0.6.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 (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +11 -11
  3. package/dist/harness/acceptance-ledger.d.ts +18 -0
  4. package/dist/harness/acceptance-ledger.d.ts.map +1 -0
  5. package/dist/harness/acceptance-ledger.js +119 -0
  6. package/dist/harness/acceptance-ledger.js.map +1 -0
  7. package/dist/harness/adapter-plugins.d.ts +105 -0
  8. package/dist/harness/adapter-plugins.d.ts.map +1 -0
  9. package/dist/harness/adapter-plugins.js +388 -0
  10. package/dist/harness/adapter-plugins.js.map +1 -0
  11. package/dist/harness/catalog.d.ts +71 -0
  12. package/dist/harness/catalog.d.ts.map +1 -0
  13. package/dist/harness/catalog.js +280 -0
  14. package/dist/harness/catalog.js.map +1 -0
  15. package/dist/harness/commands/call.d.ts +18 -0
  16. package/dist/harness/commands/call.d.ts.map +1 -0
  17. package/dist/harness/commands/call.js +667 -0
  18. package/dist/harness/commands/call.js.map +1 -0
  19. package/dist/harness/commands/discover.d.ts +29 -0
  20. package/dist/harness/commands/discover.d.ts.map +1 -0
  21. package/dist/harness/commands/discover.js +563 -0
  22. package/dist/harness/commands/discover.js.map +1 -0
  23. package/dist/harness/commands/run.d.ts +40 -0
  24. package/dist/harness/commands/run.d.ts.map +1 -0
  25. package/dist/harness/commands/run.js +760 -0
  26. package/dist/harness/commands/run.js.map +1 -0
  27. package/dist/harness/heal-bounds.d.ts +8 -0
  28. package/dist/harness/heal-bounds.d.ts.map +1 -1
  29. package/dist/harness/heal-bounds.js +10 -0
  30. package/dist/harness/heal-bounds.js.map +1 -1
  31. package/dist/harness/host.d.ts +5 -0
  32. package/dist/harness/host.d.ts.map +1 -1
  33. package/dist/harness/host.js +7 -0
  34. package/dist/harness/host.js.map +1 -1
  35. package/dist/harness/index.d.ts +10 -1
  36. package/dist/harness/index.d.ts.map +1 -1
  37. package/dist/harness/index.js +10 -1
  38. package/dist/harness/index.js.map +1 -1
  39. package/dist/harness/live-adapter-contract.d.ts +9 -0
  40. package/dist/harness/live-adapter-contract.d.ts.map +1 -1
  41. package/dist/harness/live-adapter-contract.js +38 -7
  42. package/dist/harness/live-adapter-contract.js.map +1 -1
  43. package/dist/harness/network-observation.d.ts +16 -0
  44. package/dist/harness/network-observation.d.ts.map +1 -0
  45. package/dist/harness/network-observation.js +197 -0
  46. package/dist/harness/network-observation.js.map +1 -0
  47. package/dist/harness/plugin-imports.d.ts +23 -0
  48. package/dist/harness/plugin-imports.d.ts.map +1 -0
  49. package/dist/harness/plugin-imports.js +126 -0
  50. package/dist/harness/plugin-imports.js.map +1 -0
  51. package/dist/harness/recipe-library.d.ts +65 -0
  52. package/dist/harness/recipe-library.d.ts.map +1 -0
  53. package/dist/harness/recipe-library.js +182 -0
  54. package/dist/harness/recipe-library.js.map +1 -0
  55. package/dist/harness/recipe-validation.d.ts +29 -0
  56. package/dist/harness/recipe-validation.d.ts.map +1 -0
  57. package/dist/harness/recipe-validation.js +291 -0
  58. package/dist/harness/recipe-validation.js.map +1 -0
  59. package/dist/harness/run-engine.d.ts +98 -0
  60. package/dist/harness/run-engine.d.ts.map +1 -0
  61. package/dist/harness/run-engine.js +632 -0
  62. package/dist/harness/run-engine.js.map +1 -0
  63. package/dist/harness/run-observers.d.ts +10 -0
  64. package/dist/harness/run-observers.d.ts.map +1 -0
  65. package/dist/harness/run-observers.js +24 -0
  66. package/dist/harness/run-observers.js.map +1 -0
  67. package/package.json +3 -3
@@ -0,0 +1,760 @@
1
+ // run — validate a recipe, then execute it through the engine with bounded
2
+ // healing, observation, and the run report; or describe its plan (--plan).
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { redactRecipeParams } from '@farmslot/recipe-runner';
6
+ import { missingActionCapabilities } from '../../action-catalog.js';
7
+ import { recordRecipeAcceptance } from '../acceptance-ledger.js';
8
+ import { harnessAdapter } from '../adapters.js';
9
+ import { actionLibraryContextArgs, resolveActionCapabilityMatrix, } from '../catalog.js';
10
+ import { acquireCheckoutLock } from '../checkout-lock.js';
11
+ import { color } from '../cli-color.js';
12
+ import { recordCommandEvidence } from '../command-journal.js';
13
+ import { ProvenanceDriftError } from '../execution-provenance.js';
14
+ import { recipeRunning, recipeRunningRefusal } from '../heal-bounds.js';
15
+ import { harnessHost } from '../host.js';
16
+ import { JsonStreamWriter } from '../json-stream.js';
17
+ import { isRecord, optionFlag, optionString, parseArgs, parseRecipeParamAssignments, resolveAdapter, shellQuote, targetPath, usageError, } from '../parse-args.js';
18
+ import { validateRunRecipeStatic } from '../recipe-validation.js';
19
+ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument, } from '../run-diagnostics.js';
20
+ import { activateRecipeRuntimeEnvironment, countRecipeNodes, emitHealViolation, executeWithHealBounds, persistRunEffects, preflightRecipe, prepareHeal, runRecipe, } from '../run-engine.js';
21
+ import { startRunObservers } from '../run-observers.js';
22
+ import { recipeRunOptionsFromCli } from '../run-options.js';
23
+ import { indexProductProvenanceArtifact, writeRunReport } from '../run-report.js';
24
+ import { checkoutBusyOut, EXIT, usageOut, writeInteractiveProgress } from '../shared.js';
25
+ import { recipeTrustFailure } from '../trust.js';
26
+ import { handleDescribeRecipe, handleListExecutables } from './discover.js';
27
+ async function validationCapabilityRefusals(catalog, adapter, findings, librarySources) {
28
+ const actionNames = findings.flatMap((finding) => {
29
+ if (finding.code !== 'recipe.action_not_declared_by_manifest')
30
+ return [];
31
+ const match = /^Recipe action ([^ ]+) is not declared by the runner action manifest[.]?$/u.exec(finding.message);
32
+ return match?.[1] ? [match[1]] : [];
33
+ });
34
+ if (actionNames.length === 0)
35
+ return [];
36
+ return missingActionCapabilities(adapter, actionNames, await resolveActionCapabilityMatrix(catalog, librarySources));
37
+ }
38
+ function capabilityValidationUserAction(adapter, refusals, libraryContextArgs) {
39
+ const capabilities = refusals
40
+ .map((refusal) => `"${refusal.capability}" [${refusal.satisfyingAdapters.join(', ')}]`)
41
+ .join('; ');
42
+ return (`Missing action capabilities for ${adapter}, with satisfying adapters: ${capabilities}. ` +
43
+ `Inspect: ${harnessHost().name} actions --matrix${libraryContextArgs} --json; ` +
44
+ 'then rerun from a checkout matching ' +
45
+ 'a satisfying adapter, splitting the recipe if no single adapter satisfies every capability');
46
+ }
47
+ export async function handleRun(argv, commandOptions) {
48
+ const parsed = parseArgs(argv);
49
+ const host = harnessHost().name;
50
+ const stream = new JsonStreamWriter('run', optionFlag(parsed.options, 'jsonStream'));
51
+ const restoreStdout = stream.isolateStdout();
52
+ const target = targetPath(parsed.options);
53
+ try {
54
+ const exitCode = await handleRunInner(parsed.positional, parsed.options, stream, commandOptions);
55
+ stream.complete(exitCode === EXIT.ok ? 'pass' : 'fail', exitCode);
56
+ return exitCode;
57
+ }
58
+ catch (error) {
59
+ if (error instanceof ProvenanceDriftError) {
60
+ const failure = provenanceFailure(error);
61
+ if (stream.enabled) {
62
+ stream.error(failure);
63
+ stream.complete('fail', error.exitCode);
64
+ }
65
+ else if (optionFlag(parsed.options, 'json')) {
66
+ console.log(JSON.stringify({
67
+ schemaVersion: 1,
68
+ command: 'run',
69
+ status: 'fail',
70
+ error: failure,
71
+ exitCode: error.exitCode,
72
+ }, null, 2));
73
+ }
74
+ else {
75
+ console.error(`✗ ${host} run: ${error.message}`);
76
+ console.error(` Next: ${error.userAction}`);
77
+ }
78
+ return error.exitCode;
79
+ }
80
+ const trustFailure = recipeTrustFailure(error);
81
+ if (trustFailure) {
82
+ if (stream.enabled) {
83
+ stream.error(trustFailure);
84
+ stream.complete('fail', EXIT.validation);
85
+ }
86
+ else {
87
+ reportTrustFailure('run', trustFailure, optionFlag(parsed.options, 'json'));
88
+ }
89
+ return EXIT.validation;
90
+ }
91
+ const exitCode = error !== null &&
92
+ typeof error === 'object' &&
93
+ 'exitCode' in error &&
94
+ typeof error.exitCode === 'number'
95
+ ? error.exitCode
96
+ : EXIT.runtime;
97
+ stream.error({
98
+ code: exitCode === EXIT.usage ? 'CLI_USAGE_ERROR' : 'RUN_FAILED',
99
+ message: error instanceof Error ? error.message : String(error),
100
+ userAction: `${host} doctor --target ${shellQuote(target)} --json`,
101
+ });
102
+ stream.complete('fail', exitCode);
103
+ throw error;
104
+ }
105
+ finally {
106
+ restoreStdout();
107
+ }
108
+ }
109
+ /** The drift a provenance failure reports in an envelope's `error`. */
110
+ export function provenanceFailure(error) {
111
+ return {
112
+ code: error.code,
113
+ message: error.message,
114
+ userAction: error.userAction,
115
+ provenancePath: error.provenancePath,
116
+ drift: error.drift,
117
+ };
118
+ }
119
+ /**
120
+ * A recipe trust or approval failure: the `--json` envelope, or the human
121
+ * error with at most ten restricted plan nodes.
122
+ */
123
+ export function reportTrustFailure(command, failure, json) {
124
+ if (json) {
125
+ console.log(JSON.stringify({
126
+ schemaVersion: 1,
127
+ command,
128
+ status: 'fail',
129
+ error: failure,
130
+ exitCode: EXIT.validation,
131
+ }, null, 2));
132
+ return;
133
+ }
134
+ console.error(`✗ ${harnessHost().name} ${command}: ${failure.message}`);
135
+ const blocked = failure.details?.blocked;
136
+ if (blocked?.length) {
137
+ console.error(' Restricted plan nodes:');
138
+ for (const node of blocked.slice(0, 10)) {
139
+ const implementation = node.implementation?.digest
140
+ ? ` implementation=${node.implementation.kind ?? 'custom'}@${node.implementation.digest}`
141
+ : '';
142
+ console.error(` - ${node.nodeId}: ${node.action} [${node.capabilities.join(', ')}] source=${node.source}${implementation}`);
143
+ }
144
+ if (blocked.length > 10)
145
+ console.error(` - … ${blocked.length - 10} more (use --json)`);
146
+ }
147
+ console.error(` Next: ${failure.userAction}`);
148
+ }
149
+ async function handleRunInner(positional, options, stream, commandOptions) {
150
+ const { engine } = commandOptions;
151
+ const host = harnessHost().name;
152
+ if (!optionFlag(options, 'list') &&
153
+ ['domain', 'source', 'sort'].some((name) => optionString(options, name) !== undefined)) {
154
+ throw usageError('--domain, --source, and --sort require run --list.');
155
+ }
156
+ if (optionFlag(options, 'describe') &&
157
+ (optionFlag(options, 'list') || optionFlag(options, 'plan'))) {
158
+ const message = '--describe cannot be combined with --list or --plan.';
159
+ const userAction = `choose one: ${host} run --list, ${host} run <recipe> --describe, or ${host} run <recipe> --plan`;
160
+ if (stream.enabled) {
161
+ stream.error({ code: 'CLI_USAGE_ERROR', message, userAction });
162
+ return EXIT.usage;
163
+ }
164
+ return usageOut(optionFlag(options, 'json'), 'run', message, userAction);
165
+ }
166
+ // --list: complete recipes accepted by `run` for the detected adapter.
167
+ if (optionFlag(options, 'list')) {
168
+ if (stream.enabled) {
169
+ const message = '--json-stream is for recipe execution; use run --list --json for discovery.';
170
+ stream.error({
171
+ code: 'JSON_STREAM_UNSUPPORTED_MODE',
172
+ message,
173
+ userAction: `${host} run --list --json`,
174
+ });
175
+ return EXIT.usage;
176
+ }
177
+ return handleListExecutables('run', options, { catalog: engine });
178
+ }
179
+ const targetRecipe = positional[0];
180
+ if (!targetRecipe)
181
+ throw usageError('run requires <recipe.json>.');
182
+ const paramAssignments = positional.slice(1);
183
+ if (optionFlag(options, 'describe')) {
184
+ if (paramAssignments.length > 0) {
185
+ throw usageError('run --describe does not accept parameter values; inspect the declaration first.');
186
+ }
187
+ if (stream.enabled) {
188
+ const message = '--json-stream is for recipe execution; use --describe --json for discovery.';
189
+ stream.error({
190
+ code: 'JSON_STREAM_UNSUPPORTED_MODE',
191
+ message,
192
+ userAction: `${host} run ${shellQuote(targetRecipe)} --describe --json`,
193
+ });
194
+ return EXIT.usage;
195
+ }
196
+ return handleDescribeRecipe(targetRecipe, options, { catalog: engine });
197
+ }
198
+ const params = parseRecipeParamAssignments(paramAssignments);
199
+ if (optionFlag(options, 'plan')) {
200
+ return handleRunPlan(targetRecipe, params, options, stream, commandOptions);
201
+ }
202
+ // Auto-detect the adapter from the target/cwd when --adapter is absent (parity
203
+ // with call/doctor); teaches when the repo type cannot be detected.
204
+ const { adapter, target } = resolveAdapter(options);
205
+ const json = optionFlag(options, 'json');
206
+ const jsonOutput = json && !stream.enabled;
207
+ const machine = json || stream.enabled;
208
+ stream.phase('resolve', { adapter, target, recipe: targetRecipe });
209
+ if (!fs.existsSync(target)) {
210
+ const message = `target does not exist: ${target}`;
211
+ const userAction = `pass --target <${harnessHost().product.toLowerCase()}-checkout> pointing to an existing checkout`;
212
+ if (stream.enabled) {
213
+ stream.error({ code: 'TARGET_NOT_FOUND', message, userAction });
214
+ return EXIT.usage;
215
+ }
216
+ return usageOut(jsonOutput, 'run', message, userAction);
217
+ }
218
+ writeInteractiveProgress(machine, `→ recipe run — validating ${targetRecipe} · ${adapter}`);
219
+ harnessAdapter(adapter).resolveSlotPorts(target);
220
+ // Resolve/gate the device target before any engine path reads process.env.
221
+ const device = commandOptions.targetDevice?.('run', adapter, options);
222
+ if (device && !device.ok) {
223
+ return emitRunUsageError(jsonOutput, stream, adapter, targetRecipe, device.code, device.message, device.userAction);
224
+ }
225
+ if (recipeRunning(target))
226
+ return emitRunRecipeRunning(jsonOutput, stream, target);
227
+ stream.phase('validate');
228
+ const validated = await validateRunRecipeStatic(engine, targetRecipe, adapter, options, params);
229
+ if (validated.usageError) {
230
+ const userAction = runUsageRecovery(validated.usageError.code, validated.usageError.message, adapter, validated.recipeFile);
231
+ return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message, userAction);
232
+ }
233
+ if (validated.errorCount > 0) {
234
+ const capabilityRefusals = optionString(options, 'actionManifest')
235
+ ? []
236
+ : await validationCapabilityRefusals(engine, adapter, validated.findings, validated.librarySources);
237
+ return emitRunValidationError(jsonOutput, stream, adapter, validated.recipeFile, validated.findings, validated.errorCount, capabilityRefusals, actionLibraryContextArgs(engine, validated.librarySources));
238
+ }
239
+ const depsBlock = (await harnessAdapter(adapter).run?.dependencyBlock?.(target, {
240
+ recipe: validated.recipe,
241
+ librarySources: validated.librarySources,
242
+ })) ?? null;
243
+ if (depsBlock) {
244
+ return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
245
+ }
246
+ let artifactsDir;
247
+ try {
248
+ artifactsDir = resolveRunArtifactsDir(target, optionString(options, 'artifactsDir'));
249
+ }
250
+ catch (error) {
251
+ const message = error instanceof Error ? error.message : String(error);
252
+ return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, 'ARTIFACT_DIR_INVALID', message, 'set RECIPE_TASK_DIR/FARMSLOT_TASK_DIR inside the checkout or pass --artifacts-dir <path>');
253
+ }
254
+ recordCommandEvidence(artifactsDir);
255
+ // Reuse the sources validateRunRecipeStatic already resolved (same --library
256
+ // input) instead of a second resolution.
257
+ const librarySources = validated.librarySources;
258
+ let observers;
259
+ const runtimeOptions = {
260
+ ...recipeRunOptionsFromCli(adapter, options),
261
+ cli: options,
262
+ params,
263
+ ...(librarySources ? { librarySources } : {}),
264
+ stdoutIsMachineContract: machine,
265
+ onActionEvent: ({ nodeId, action, status }) => {
266
+ stream.node(nodeId, action, status);
267
+ observers?.onActionEvent({ nodeId, action, status });
268
+ },
269
+ };
270
+ stream.phase('authorize');
271
+ // One environment scope for the run: preflight, the runtime check, the
272
+ // observers and the execution all see the same ports.
273
+ const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(adapter, target, runtimeOptions);
274
+ try {
275
+ let preflightedExecution = await preflightRecipe(engine, adapter, validated.recipeFile, artifactsDir, target, optionString(options, 'actionManifest'), runtimeOptions);
276
+ const lock = acquireCheckoutLock(target, 'run');
277
+ if ('message' in lock) {
278
+ const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
279
+ stream.error({ code: 'SANDBOX_BUSY', message: lock.message, userAction });
280
+ return checkoutBusyOut(jsonOutput, 'run', lock.message, lock.path);
281
+ }
282
+ try {
283
+ const prepared = await prepareHeal(adapter, target, options, machine, {
284
+ onPhase: (phase, fields) => stream.phase(phase, fields),
285
+ });
286
+ if (typeof prepared === 'number') {
287
+ stream.error({
288
+ code: 'RUN_PREPARE_FAILED',
289
+ message: 'runtime preparation failed; inspect stderr for the exact probe',
290
+ userAction: `${host} doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`,
291
+ });
292
+ return prepared;
293
+ }
294
+ const { state } = prepared;
295
+ preflightedExecution = await preflightRecipe(engine, adapter, validated.recipeFile, artifactsDir, target, optionString(options, 'actionManifest'), runtimeOptions);
296
+ const started = await startRunObservers(adapter, target, artifactsDir);
297
+ observers = started;
298
+ stream.phase('execute');
299
+ let executionResult;
300
+ try {
301
+ executionResult = await executeWithHealBounds(
302
+ // validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
303
+ // that only the resolver knows how to turn into a file.
304
+ () => {
305
+ const execution = preflightedExecution;
306
+ preflightedExecution = undefined;
307
+ return runRecipe(engine, adapter, validated.recipeFile, artifactsDir, target, optionString(options, 'actionManifest'), runtimeOptions, execution, state);
308
+ }, target, state);
309
+ }
310
+ catch (error) {
311
+ await started.abandon();
312
+ observers = undefined;
313
+ throw error;
314
+ }
315
+ const { result, violation } = executionResult;
316
+ await started.finalize(result.artifactManifestPath);
317
+ observers = undefined;
318
+ for (const mutation of state.mutations)
319
+ stream.mutation(mutation);
320
+ for (const recovery of state.recovered)
321
+ stream.recovery(recovery);
322
+ persistRunEffects(result.summaryPath, result.artifactManifestPath, state);
323
+ indexProductProvenanceArtifact(result.artifactManifestPath, target, adapter, result.browser ?? null);
324
+ recordRunAcceptance(target, result);
325
+ if (violation !== null) {
326
+ const userAction = violation.userAction ??
327
+ `inspect ${shellQuote(result.summaryPath)} and ${shellQuote(result.tracePath)}; fix the application or recipe failure before retrying`;
328
+ stream.error({
329
+ code: violation.code,
330
+ message: violation.message,
331
+ userAction,
332
+ originalError: violation.originalError ?? null,
333
+ });
334
+ return emitHealViolation(jsonOutput, 'run', result, violation, state, adapter);
335
+ }
336
+ const report = writeRunReport(result);
337
+ const exitCode = result.status === 'pass' ? EXIT.ok : EXIT.runtime;
338
+ const failureUserAction = `${host} last --target ${shellQuote(target)} --json`;
339
+ if (stream.enabled) {
340
+ if (result.status === 'fail') {
341
+ stream.error({
342
+ code: 'RECIPE_EXECUTION_FAILED',
343
+ message: 'recipe execution failed; inspect the persisted result and evidence paths',
344
+ userAction: failureUserAction,
345
+ });
346
+ }
347
+ stream.complete(result.status, exitCode, {
348
+ adapter,
349
+ reportPath: report.path,
350
+ summaryPath: result.summaryPath,
351
+ tracePath: result.tracePath,
352
+ artifactManifestPath: result.artifactManifestPath,
353
+ recovered: state.recovered,
354
+ mutations: state.mutations,
355
+ });
356
+ }
357
+ else if (json) {
358
+ console.log(JSON.stringify({
359
+ schemaVersion: 1,
360
+ command: 'run',
361
+ adapter,
362
+ status: result.status,
363
+ exitCode,
364
+ recovered: state.recovered,
365
+ mutations: state.mutations,
366
+ reportPath: report.path,
367
+ result,
368
+ ...(result.status === 'fail'
369
+ ? {
370
+ error: {
371
+ code: 'RECIPE_EXECUTION_FAILED',
372
+ message: 'recipe execution failed; inspect the persisted result and evidence paths',
373
+ userAction: failureUserAction,
374
+ },
375
+ }
376
+ : {}),
377
+ }, null, 2));
378
+ }
379
+ else {
380
+ const out = (style, text) => color(style, text, { stream: process.stdout });
381
+ console.log(`${out(result.status === 'pass' ? 'ok' : 'err', result.status.toUpperCase())} ${out('bold', 'recipe run')} ${out('dim', `[${adapter}]`)}`);
382
+ if (report.preview.length > 0) {
383
+ console.log(out('label', 'summary:'));
384
+ for (const line of report.preview)
385
+ console.log(` ${formatPreviewLine(line, out)}`);
386
+ }
387
+ const artifacts = runArtifactInventory(result.artifactManifestPath);
388
+ console.log(out('label', `artifacts (${artifacts.length}):`));
389
+ for (const artifact of artifacts) {
390
+ console.log(` ${out('dim', `${artifact.label}:`)} ${out('path', artifact.absolutePath)}`);
391
+ }
392
+ if (result.status === 'fail') {
393
+ console.error(` Next: ${failureUserAction}`);
394
+ }
395
+ console.log(out('label', 'diagnostics:'));
396
+ const diagnostics = readRunDiagnosticsDocument(result.diagnosticsPath);
397
+ for (const line of formatRunDiagnosticsForHuman(diagnostics, adapter)) {
398
+ console.log(` ${formatDiagnosticLine(line, out)}`);
399
+ }
400
+ }
401
+ return exitCode;
402
+ }
403
+ finally {
404
+ lock.release();
405
+ }
406
+ }
407
+ finally {
408
+ restoreRuntimeEnvironment();
409
+ }
410
+ }
411
+ function formatDiagnosticLine(line, out) {
412
+ const match = /^(CLEAN|REVIEW|UNAVAILABLE|N\/A|WARNING|ERROR|EXCEPTION)(.*)$/u.exec(line);
413
+ if (!match)
414
+ return line;
415
+ const status = match[1];
416
+ const style = status === 'CLEAN' ? 'ok' : status === 'ERROR' || status === 'EXCEPTION' ? 'err' : 'warn';
417
+ return `${out(style, status)}${match[2]}`;
418
+ }
419
+ function resolveRunArtifactsDir(target, explicit) {
420
+ if (explicit !== undefined)
421
+ return path.resolve(explicit);
422
+ const taskDir = runTaskDir(target);
423
+ if (taskDir)
424
+ return path.join(taskDir, 'artifacts');
425
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/gu, '');
426
+ return path.join(target, 'temp', 'recipe', 'runs', `${stamp}-${process.pid}`);
427
+ }
428
+ /** The run's task dir (RECIPE_TASK_DIR / FARMSLOT_TASK_DIR), resolved inside the checkout, or null. */
429
+ function runTaskDir(target) {
430
+ const taskDir = process.env.RECIPE_TASK_DIR || process.env.FARMSLOT_TASK_DIR;
431
+ if (!taskDir)
432
+ return null;
433
+ const resolvedTask = path.resolve(target, taskDir);
434
+ const relative = path.relative(path.resolve(target), resolvedTask);
435
+ if (!relative.startsWith(`..${path.sep}`) && relative !== '..')
436
+ return resolvedTask;
437
+ throw new Error(`task directory must be inside the target checkout: ${taskDir}`);
438
+ }
439
+ // The task's acceptance ledger from the recipe's proof targets. The run's result
440
+ // stands either way: a ledger the run can't write is a warning, not a failure.
441
+ function recordRunAcceptance(target, result) {
442
+ try {
443
+ const taskDir = runTaskDir(target);
444
+ if (!taskDir)
445
+ return;
446
+ const { recorded, refused } = recordRecipeAcceptance(taskDir, target, result);
447
+ for (const reason of refused)
448
+ console.error(`acceptance ledger: not recorded ${reason}`);
449
+ if (recorded.length > 0 && recorded.every((entry) => entry.evidence.length === 0)) {
450
+ console.error(`acceptance ledger: verdicts recorded with no evidence inside the task dir (artifacts dir: ${path.dirname(result.tracePath)})`);
451
+ }
452
+ }
453
+ catch (error) {
454
+ // The stack too: an unexpected error here is a bug, not a ledger state.
455
+ console.error(`acceptance ledger: not written: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
456
+ }
457
+ }
458
+ function runArtifactInventory(manifestPathValue) {
459
+ const manifestPath = path.resolve(String(manifestPathValue));
460
+ const root = path.dirname(manifestPath);
461
+ let manifest;
462
+ try {
463
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
464
+ }
465
+ catch {
466
+ return [
467
+ {
468
+ absolutePath: manifestPath,
469
+ basename: path.basename(manifestPath),
470
+ label: 'Artifact manifest',
471
+ },
472
+ ];
473
+ }
474
+ const entries = isRecord(manifest) && Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
475
+ const artifacts = [];
476
+ for (const entry of entries) {
477
+ if (!isRecord(entry) || typeof entry.path !== 'string' || entry.path.length === 0)
478
+ continue;
479
+ const absolutePath = path.resolve(root, entry.path);
480
+ const basename = path.basename(absolutePath);
481
+ const label = typeof entry.label === 'string' && entry.label.length > 0 ? entry.label : basename;
482
+ artifacts.push({ absolutePath, basename, label });
483
+ }
484
+ artifacts.push({
485
+ absolutePath: manifestPath,
486
+ basename: path.basename(manifestPath),
487
+ label: 'Artifact manifest',
488
+ });
489
+ const priority = new Map([
490
+ ['diagnostics.json', 0],
491
+ ['report.md', 1],
492
+ ['summary.json', 2],
493
+ ['trace.json', 3],
494
+ ['recipe.json', 4],
495
+ ['artifact-manifest.json', 100],
496
+ ]);
497
+ return artifacts
498
+ .filter((artifact, index, all) => all.findIndex((candidate) => candidate.absolutePath === artifact.absolutePath) === index)
499
+ .sort((left, right) => (priority.get(left.basename) ?? 50) - (priority.get(right.basename) ?? 50));
500
+ }
501
+ function formatPreviewLine(line, out) {
502
+ const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
503
+ if (!match)
504
+ return line;
505
+ const status = match[1];
506
+ const rest = match[2];
507
+ return `${out(status === 'PASS' ? 'ok' : 'err', status)} ${rest}`;
508
+ }
509
+ async function handleRunPlan(recipeArg, params, options, stream, commandOptions) {
510
+ const { engine } = commandOptions;
511
+ const json = optionFlag(options, 'json');
512
+ const jsonOutput = json && !stream.enabled;
513
+ const { adapter, target } = resolveAdapter(options);
514
+ stream.phase('resolve', { adapter, target, recipe: recipeArg });
515
+ stream.phase('validate');
516
+ const validated = await validateRunRecipeStatic(engine, recipeArg, adapter, options, params);
517
+ if (validated.usageError) {
518
+ const userAction = runUsageRecovery(validated.usageError.code, validated.usageError.message, adapter, validated.recipeFile);
519
+ return emitPlanUsageError(jsonOutput, stream, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message, userAction);
520
+ }
521
+ const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid, effectiveParams, librarySources, } = validated;
522
+ const status = errorCount === 0 ? 'pass' : 'fail';
523
+ const capabilityRefusals = status === 'fail' && !optionString(options, 'actionManifest')
524
+ ? await validationCapabilityRefusals(engine, adapter, findings, librarySources)
525
+ : [];
526
+ const failureUserAction = capabilityRefusals.length > 0
527
+ ? capabilityValidationUserAction(adapter, capabilityRefusals, actionLibraryContextArgs(engine, librarySources))
528
+ : runPlanProbe(adapter, recipeFile);
529
+ const nodeCount = countRecipeNodes(recipe);
530
+ const artifactsDir = optionString(options, 'artifactsDir');
531
+ const plan = [
532
+ { step: 'resolve.recipe', confidence: 'static', status: 'ok', detail: recipeFile },
533
+ { step: 'resolve.adapter', confidence: 'static', status: 'ok', detail: adapter },
534
+ {
535
+ step: 'resolve.artifactsDir',
536
+ confidence: 'static',
537
+ status: 'ok',
538
+ detail: artifactsDir
539
+ ? path.resolve(artifactsDir)
540
+ : '(resolved to the slot artifacts dir at run time)',
541
+ },
542
+ {
543
+ step: 'validate.manifest',
544
+ confidence: 'static',
545
+ status: manifestOk ? 'ok' : 'error',
546
+ detail: manifestOk ? 'action manifest is well-formed' : 'action manifest failed validation',
547
+ },
548
+ {
549
+ step: 'validate.schema',
550
+ confidence: 'static',
551
+ status: manifestOk && schemaValid ? 'ok' : 'error',
552
+ detail: 'recipe document schema + action existence/platform vs the adapter manifest',
553
+ },
554
+ ...(optionFlag(options, 'proof')
555
+ ? [
556
+ {
557
+ step: 'validate.proofBindings',
558
+ confidence: 'static',
559
+ status: findings.some((finding) => finding.code.startsWith('proof.'))
560
+ ? 'error'
561
+ : errorCount
562
+ ? 'planned'
563
+ : 'ok',
564
+ detail: 'checks possible behavioral assertion bindings only; runtime order, branches, results and proof artifact indexing remain unverified',
565
+ },
566
+ ]
567
+ : []),
568
+ ...(commandOptions.plan?.steps?.(target) ?? []),
569
+ {
570
+ step: 'overlay.ensure',
571
+ confidence: 'conditional',
572
+ status: 'planned',
573
+ detail: 'would auto-ensure the runtime overlay if missing (install phase)',
574
+ },
575
+ ...(harnessAdapter(adapter).headless
576
+ ? []
577
+ : [
578
+ {
579
+ step: 'launch.app',
580
+ confidence: 'conditional',
581
+ status: 'planned',
582
+ detail: commandOptions.plan?.launchDetail ??
583
+ 'would launch/attach the app + heal transport before executing',
584
+ },
585
+ ]),
586
+ {
587
+ step: 'execute.nodes',
588
+ confidence: 'conditional',
589
+ status: 'planned',
590
+ detail: nodeCount === undefined
591
+ ? 'would execute the recipe nodes'
592
+ : `would execute ${nodeCount} recipe node(s)`,
593
+ },
594
+ ];
595
+ const payload = {
596
+ schemaVersion: 1,
597
+ command: 'run',
598
+ mode: 'plan',
599
+ status,
600
+ adapter,
601
+ recipe: recipeFile,
602
+ params: redactRecipeParams(effectiveParams, isRecord(recipe) ? recipe.paramsSchema : undefined)
603
+ .params,
604
+ findings,
605
+ plan,
606
+ };
607
+ if (status === 'fail') {
608
+ payload.error = {
609
+ code: 'RECIPE_VALIDATION_FAILED',
610
+ message: `recipe validation found ${errorCount} error(s)`,
611
+ ...(capabilityRefusals.length > 0 ? { missingCapabilities: capabilityRefusals } : {}),
612
+ userAction: failureUserAction,
613
+ };
614
+ }
615
+ if (stream.enabled) {
616
+ if (status === 'fail')
617
+ stream.error(payload.error);
618
+ stream.complete(status, status === 'pass' ? EXIT.ok : EXIT.validation, {
619
+ mode: 'plan',
620
+ adapter,
621
+ recipe: recipeFile,
622
+ findings,
623
+ plan,
624
+ });
625
+ }
626
+ else if (json) {
627
+ console.log(JSON.stringify(payload, null, 2));
628
+ }
629
+ else {
630
+ console.log(`plan ${status} — ${adapter} — ${recipeFile}`);
631
+ for (const item of plan) {
632
+ const mark = item.status === 'error' ? '✗' : item.status === 'ok' ? '✓' : '·';
633
+ console.log(` ${mark} [${item.confidence}] ${item.step}: ${item.detail}`);
634
+ }
635
+ if (findings.length) {
636
+ console.log('findings:');
637
+ for (const finding of findings) {
638
+ console.log(` ${finding.severity === 'error' ? '✗' : '⚠'} ${finding.code} ${finding.path} — ${finding.message}`);
639
+ }
640
+ }
641
+ if (status === 'fail')
642
+ console.error(` Next: ${failureUserAction}`);
643
+ }
644
+ return status === 'pass' ? EXIT.ok : EXIT.validation;
645
+ }
646
+ function emitPlanUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
647
+ stream.error({ code, message, userAction, mode: 'plan', adapter, recipe: recipeFile });
648
+ if (json) {
649
+ console.log(JSON.stringify({
650
+ schemaVersion: 1,
651
+ command: 'run',
652
+ mode: 'plan',
653
+ status: 'fail',
654
+ adapter,
655
+ recipe: recipeFile,
656
+ error: { code, message, userAction },
657
+ }, null, 2));
658
+ }
659
+ else {
660
+ console.error(`✗ run --plan: ${message}\n Next: ${userAction}`);
661
+ }
662
+ return EXIT.usage;
663
+ }
664
+ function emitRunRecipeRunning(json, stream, target) {
665
+ const host = harnessHost().name;
666
+ const { message, userAction } = recipeRunningRefusal(shellQuote(target));
667
+ stream.error({ code: 'RECIPE_RUNNING', message, userAction, recoverable: false });
668
+ if (json) {
669
+ console.log(JSON.stringify({
670
+ schemaVersion: 1,
671
+ status: 'fail',
672
+ recoverable: false,
673
+ error: { code: 'RECIPE_RUNNING', message, userAction },
674
+ }, null, 2));
675
+ }
676
+ else {
677
+ console.error(`✗ ${host}: ${message}\n Next: ${userAction}`);
678
+ }
679
+ return EXIT.bounded;
680
+ }
681
+ function emitRunUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
682
+ stream.error({ code, message, userAction });
683
+ if (json) {
684
+ console.log(JSON.stringify({
685
+ schemaVersion: 1,
686
+ command: 'run',
687
+ adapter,
688
+ status: 'fail',
689
+ exitCode: EXIT.usage,
690
+ recipe: recipeFile,
691
+ error: { code, message, userAction },
692
+ }, null, 2));
693
+ }
694
+ else {
695
+ console.error(`✗ run: ${message}`);
696
+ console.error(` Next: ${userAction}`);
697
+ }
698
+ return EXIT.usage;
699
+ }
700
+ function emitRunValidationError(json, stream, adapter, recipeFile, findings, errorCount, capabilityRefusals, libraryContextArgs) {
701
+ const message = `recipe validation found ${errorCount} error(s)`;
702
+ const userAction = capabilityRefusals.length > 0
703
+ ? capabilityValidationUserAction(adapter, capabilityRefusals, libraryContextArgs)
704
+ : runPlanProbe(adapter, recipeFile);
705
+ const capabilityDetails = capabilityRefusals.length > 0 ? { missingCapabilities: capabilityRefusals } : {};
706
+ stream.error({
707
+ code: 'RECIPE_VALIDATION_FAILED',
708
+ message,
709
+ userAction,
710
+ findings,
711
+ ...capabilityDetails,
712
+ });
713
+ if (json) {
714
+ console.log(JSON.stringify({
715
+ schemaVersion: 1,
716
+ command: 'run',
717
+ adapter,
718
+ status: 'fail',
719
+ exitCode: EXIT.validation,
720
+ recovered: [],
721
+ mutations: [],
722
+ recipe: recipeFile,
723
+ findings,
724
+ error: {
725
+ code: 'RECIPE_VALIDATION_FAILED',
726
+ message,
727
+ userAction,
728
+ ...capabilityDetails,
729
+ },
730
+ }, null, 2));
731
+ }
732
+ else {
733
+ console.error(`✗ run: ${message}`);
734
+ for (const finding of findings) {
735
+ if (finding.severity === 'error')
736
+ console.error(` ${finding.code} ${finding.path} — ${finding.message}`);
737
+ }
738
+ console.error(` Next: ${userAction}`);
739
+ }
740
+ return EXIT.validation;
741
+ }
742
+ function runPlanProbe(adapter, recipeFile) {
743
+ return `${harnessHost().name} run --plan ${shellQuote(recipeFile)} --adapter ${adapter} --json`;
744
+ }
745
+ function runUsageRecovery(code, message, adapter, recipeFile) {
746
+ const host = harnessHost().name;
747
+ const actionCommand = new RegExp(`This is an action, not a recipe\\. Use: (${escapeRegExp(host)} call .+)\\.$`, 'u').exec(message)?.[1];
748
+ if (actionCommand)
749
+ return actionCommand;
750
+ if (code === 'RECIPE_NOT_FOUND')
751
+ return `${host} run --list --adapter ${adapter} --json`;
752
+ if (code === 'RECIPE_UNPARSEABLE') {
753
+ return `fix the JSON syntax in ${shellQuote(recipeFile)}, then retry: ${runPlanProbe(adapter, recipeFile)}`;
754
+ }
755
+ return runPlanProbe(adapter, recipeFile);
756
+ }
757
+ function escapeRegExp(value) {
758
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
759
+ }
760
+ //# sourceMappingURL=run.js.map