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