acpus 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { At as __require, Ct as writeRunIndex, Dt as runDir, Et as projectWorkflowsDir, O as compileExecutionPlan, Ot as runsDir, S as syncRun, St as readRunIndex, Tt as globalWorkflowsDir, _ as applyInputDefaults, a as previewRunView, b as issue, d as recoverDriver, f as runWorkflowWorker, ft as WorkflowSpecSchema, g as lintWorkflowSpec, gt as resolveRunLocator, h as loadWorkflowSpec, jt as __toESM, kt as __commonJSMin, l as buildRunMonitorView, m as workerIsActive, mt as getOutputContract, o as runViewFromIndex, p as spawnBackgroundWorker, t as prepareRun, u as buildTaskDetailView, v as validateWorkflowInput, wt as draftsDir, x as resultFromIssues, xt as appendEvent, yt as buildRunDiagnosticsView } from "./run-workflow-CbxKhAqF.mjs";
2
+ import { A as terminalRunStatus, At as appendEvent, D as readFinalOutput, E as buildTaskDetailView, Ft as runDir, It as runsDir, Lt as __commonJSMin, M as workerSummary, Mt as writeRunIndex, N as syncRun, Nt as globalWorkflowsDir, O as runWorkflowWorker, Ot as WorkflowSpecSchema, Pt as projectWorkflowsDir, Rt as __require, S as resultFromIssues, T as buildRunMonitorView, _ as applyInputDefaults, d as previewRunView, g as lintWorkflowSpec, h as stringifyWorkflowSpec, j as workerIsActive, jt as readRunIndex, k as spawnBackgroundWorker, m as loadWorkflowSpec, r as compileExecutionPlan, t as prepareRun, v as validateWorkflowInput, x as issue, y as staticLimitOrDefault, zt as __toESM } from "./run-workflow-DdIAC8Zu.mjs";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs/promises";
5
+ import YAML from "yaml";
5
6
  import React from "react";
6
7
  import { render } from "ink";
7
8
  import { fileURLToPath } from "node:url";
@@ -3016,110 +3017,158 @@ const { program: program$1, createCommand, createArgument, createOption, Command
3016
3017
  exports.InvalidOptionArgumentError = InvalidArgumentError;
3017
3018
  })))(), 1)).default;
3018
3019
  //#endregion
3019
- //#region src/runtime/diagnose-run.ts
3020
- async function startDiagnosticRun(cwd, logicalRunId) {
3021
- const index = await readRunIndex(cwd, logicalRunId);
3022
- const dir = runDir(logicalRunId, cwd);
3023
- const spec = WorkflowSpecSchema.parse(JSON.parse(await fs.readFile(path.join(dir, "workflow.spec.json"), "utf8")));
3024
- const role = recoveryRole(spec.roles);
3025
- const diagnosticId = `diagnostic-${index.agentUsage.recoveryCalls + 1}`;
3026
- const output = {
3027
- status: "completed",
3028
- summary: "Diagnostic prompt prepared. Run recovery review as a normal workflow stage in a follow-up run if needed.",
3029
- artifacts: [{
3030
- kind: "prompt",
3031
- path: `prompts/${diagnosticId}.md`,
3032
- label: "Diagnostic prompt"
3033
- }],
3034
- nextFocus: "review recovery plan",
3035
- data: {
3036
- role,
3037
- contract: getOutputContract("diagnostic").schemaForPrompt
3038
- }
3039
- };
3040
- await fs.mkdir(path.join(dir, "prompts"), { recursive: true });
3041
- const diagnostics = await buildRunDiagnosticsView(cwd, index, { eventTailLimit: 50 });
3042
- await fs.writeFile(path.join(dir, "prompts", `${diagnosticId}.md`), diagnosticPrompt({
3043
- workflowName: spec.name,
3044
- runSnapshot: index,
3045
- outputs: await readExistingOutputs(dir),
3046
- diagnostics: diagnostics.diagnostics,
3047
- eventTail: diagnostics.eventTail
3048
- }), "utf8");
3049
- await fs.mkdir(path.join(dir, "diagnostics"), { recursive: true });
3050
- await fs.writeFile(path.join(dir, "diagnostics", `${diagnosticId}.json`), `${JSON.stringify(output, null, 2)}\n`, "utf8");
3051
- const next = {
3052
- ...index,
3053
- status: index.status === "blocked" || index.status === "failed" ? "diagnosed_blocked" : index.status,
3054
- agentUsage: {
3055
- ...index.agentUsage,
3056
- recoveryCalls: index.agentUsage.recoveryCalls + 1
3020
+ //#region src/run-index/locator.ts
3021
+ async function resolveRunLocator(locator, cwd = process.cwd()) {
3022
+ const candidate = path.resolve(cwd, locator);
3023
+ try {
3024
+ const dir = (await fs.stat(candidate)).isDirectory() ? candidate : path.dirname(candidate);
3025
+ await fs.access(path.join(dir, "run.json"));
3026
+ const runsDir = path.dirname(dir);
3027
+ const orchestratorDir = path.dirname(runsDir);
3028
+ return {
3029
+ cwd: path.dirname(orchestratorDir),
3030
+ runId: path.basename(dir),
3031
+ dir
3032
+ };
3033
+ } catch {
3034
+ return {
3035
+ cwd,
3036
+ runId: locator,
3037
+ dir: path.join(cwd, ".acpus", "runs", locator)
3038
+ };
3039
+ }
3040
+ }
3041
+ //#endregion
3042
+ //#region src/run-index/run-summary.ts
3043
+ async function listRunSummaries(cwd = process.cwd()) {
3044
+ const dir = runsDir(cwd);
3045
+ let entries;
3046
+ try {
3047
+ entries = (await fs.readdir(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).map((name) => ({
3048
+ name,
3049
+ mtimeMs: 0
3050
+ }));
3051
+ } catch (error) {
3052
+ if (error.code === "ENOENT") return {
3053
+ kind: "runs",
3054
+ dir,
3055
+ entries: []
3056
+ };
3057
+ throw error;
3058
+ }
3059
+ const withMtime = await Promise.all(entries.map(async (entry) => {
3060
+ try {
3061
+ const stat = await fs.stat(path.join(dir, entry.name));
3062
+ return {
3063
+ ...entry,
3064
+ mtimeMs: stat.mtimeMs
3065
+ };
3066
+ } catch {
3067
+ return entry;
3057
3068
  }
3069
+ }));
3070
+ const summaries = await Promise.all(withMtime.map((entry) => runSummary(cwd, dir, entry.name, entry.mtimeMs)));
3071
+ summaries.sort((left, right) => parseTime(right.sortTime) - parseTime(left.sortTime) || right.runId.localeCompare(left.runId));
3072
+ return {
3073
+ kind: "runs",
3074
+ dir,
3075
+ entries: summaries
3058
3076
  };
3059
- await writeRunIndex(cwd, next);
3060
- await appendEvent(cwd, logicalRunId, {
3061
- type: "diagnostic_prepared",
3062
- diagnosticId
3063
- });
3064
- return next;
3065
3077
  }
3066
- function recoveryRole(roles) {
3067
- return roles.recovery_reviewer ?? Object.values(roles).find((role) => role.category === "validation" || role.category === "review") ?? {
3068
- category: "review",
3069
- agent: "aiden",
3070
- mode: "readOnly"
3071
- };
3078
+ function formatRunSummaryList(list) {
3079
+ if (list.entries.length === 0) return `runs in ${list.dir}\nNo runs found.\n`;
3080
+ const lines = [`runs in ${list.dir}`];
3081
+ for (const entry of list.entries) {
3082
+ const status = entry.status ?? "invalid";
3083
+ const workflow = entry.workflowName ? ` ${entry.workflowName}` : "";
3084
+ const progress = entry.progress ? ` ${entry.progress.label}` : "";
3085
+ const worker = entry.worker ? ` worker=${entry.worker.status}` : "";
3086
+ const updated = entry.updatedAt ?? entry.sortTime;
3087
+ const invalid = entry.invalid && entry.error ? ` error=${entry.error}` : "";
3088
+ lines.push(`- ${entry.runId} ${status}${progress}${worker}${workflow} updated=${updated}${invalid}`);
3089
+ }
3090
+ return `${lines.join("\n")}\n`;
3072
3091
  }
3073
- async function readExistingOutputs(dir) {
3074
- const outputDir = path.join(dir, "outputs");
3075
- const outputs = {};
3092
+ async function runSummary(cwd, parentDir, runId, mtimeMs) {
3093
+ const runDir = path.join(parentDir, runId);
3094
+ const fallbackTime = timeFromRunId(runId) ?? new Date(mtimeMs || 0).toISOString();
3076
3095
  try {
3077
- const entries = await fs.readdir(outputDir);
3078
- for (const entry of entries) {
3079
- if (!entry.endsWith(".json")) continue;
3080
- outputs[path.basename(entry, ".json")] = JSON.parse(await fs.readFile(path.join(outputDir, entry), "utf8"));
3081
- }
3082
- } catch {}
3083
- return outputs;
3096
+ const index = await readRunIndex(cwd, runId);
3097
+ const progress = stageProgress(index);
3098
+ const terminalAt = terminalTime(index);
3099
+ const updatedAt = index.updatedAt ?? terminalAt;
3100
+ const createdAtMs = parseTimeOrUndefined(index.createdAt);
3101
+ const terminalAtMs = terminalAt ? parseTimeOrUndefined(terminalAt) : void 0;
3102
+ return {
3103
+ runId,
3104
+ runDir,
3105
+ workflowName: index.workflowName,
3106
+ status: index.status,
3107
+ progress,
3108
+ createdAt: index.createdAt,
3109
+ updatedAt,
3110
+ durationMs: terminalAtMs !== void 0 && createdAtMs !== void 0 ? terminalAtMs - createdAtMs : void 0,
3111
+ elapsedMs: terminalAt ? void 0 : createdAtMs !== void 0 ? Date.now() - createdAtMs : void 0,
3112
+ worker: workerSummary(index.worker),
3113
+ sortTime: firstValidTime(updatedAt, index.createdAt, fallbackTime)
3114
+ };
3115
+ } catch (error) {
3116
+ return {
3117
+ runId,
3118
+ runDir,
3119
+ invalid: true,
3120
+ error: error instanceof Error ? error.message : String(error),
3121
+ sortTime: fallbackTime
3122
+ };
3123
+ }
3084
3124
  }
3085
- function diagnosticPrompt(input) {
3086
- return `You are the recovery reviewer for a blocked acpus workflow.
3087
-
3088
- Workflow: ${input.workflowName}
3089
-
3090
- Run snapshot:
3091
- \`\`\`json
3092
- ${JSON.stringify(input.runSnapshot, null, 2)}
3093
- \`\`\`
3094
-
3095
- Author stage outputs:
3096
- \`\`\`json
3097
- ${JSON.stringify(input.outputs, null, 2)}
3098
- \`\`\`
3099
-
3100
- Runtime diagnostics:
3101
- \`\`\`json
3102
- ${JSON.stringify(input.diagnostics, null, 2)}
3103
- \`\`\`
3104
-
3105
- Recent runtime events:
3106
- \`\`\`json
3107
- ${JSON.stringify(input.eventTail, null, 2)}
3108
- \`\`\`
3109
-
3110
- Diagnose why the run is blocked and recommend the smallest safe recovery plan. Do not edit files. Do not rerun prior edit work.
3111
-
3112
- End the response with exactly one valid, parseable JSON object matching the diagnostic contract. Do not wrap the final JSON object in Markdown code fences. Do not use \`\`\`json.`;
3125
+ function stageProgress(index) {
3126
+ const stages = Object.values(index.stages);
3127
+ const completed = stages.filter((stage) => stage.status === "completed" || stage.status === "skipped").length;
3128
+ return {
3129
+ completedStages: completed,
3130
+ totalStages: stages.length,
3131
+ label: `${completed}/${stages.length} stages`
3132
+ };
3133
+ }
3134
+ function terminalTime(index) {
3135
+ return Object.values(index.stages).map((stage) => stage.completedAt).filter((value) => Boolean(value)).sort().at(-1);
3136
+ }
3137
+ function timeFromRunId(runId) {
3138
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/.exec(runId);
3139
+ if (!match) return void 0;
3140
+ const [, year, month, day, hour, minute, second, ms] = match;
3141
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}.${ms}Z`;
3142
+ }
3143
+ function firstValidTime(...values) {
3144
+ return values.find((value) => value !== void 0 && parseTimeOrUndefined(value) !== void 0) ?? (/* @__PURE__ */ new Date(0)).toISOString();
3145
+ }
3146
+ function parseTime(value) {
3147
+ return parseTimeOrUndefined(value) ?? 0;
3148
+ }
3149
+ function parseTimeOrUndefined(value) {
3150
+ const parsed = Date.parse(value);
3151
+ return Number.isFinite(parsed) ? parsed : void 0;
3113
3152
  }
3114
3153
  //#endregion
3115
3154
  //#region src/commands/common.ts
3155
+ async function resolveSpecArg(options) {
3156
+ if (options.spec) {
3157
+ const hasPathSeparator = options.spec.includes("/") || options.spec.includes("\\");
3158
+ const hasSpecExtension = options.spec.endsWith(".yaml") || options.spec.endsWith(".yml");
3159
+ const hasUnsupportedSpecExtension = options.spec.endsWith(".json") || options.spec.endsWith(".workflow.spec.json");
3160
+ if (hasPathSeparator || hasSpecExtension || hasUnsupportedSpecExtension) return options.spec;
3161
+ return path.join(options.global ? globalWorkflowsDir() : projectWorkflowsDir(), options.spec, "workflow.spec.yaml");
3162
+ }
3163
+ throw new Error("Provide a spec file path or workflow name.");
3164
+ }
3116
3165
  async function loadAndLint(specPath) {
3117
3166
  const loaded = await loadWorkflowSpec(specPath);
3118
- if (!loaded.spec) return { result: resultFromIssues("validate", loaded.issues) };
3167
+ if (!loaded.spec) return { result: resultFromIssues("spec", loaded.issues) };
3119
3168
  const issues = [...loaded.issues, ...lintWorkflowSpec(loaded.spec)];
3120
3169
  return {
3121
3170
  spec: loaded.spec,
3122
- result: resultFromIssues("validate", issues)
3171
+ result: resultFromIssues("spec", issues)
3123
3172
  };
3124
3173
  }
3125
3174
  function printJson(value) {
@@ -3145,109 +3194,195 @@ async function ensureEmptyOrOverwrite(target, overwrite) {
3145
3194
  if (error.code !== "ENOENT") throw error;
3146
3195
  }
3147
3196
  }
3148
- async function readJsonFile(filePath) {
3149
- return JSON.parse(await fs.readFile(filePath, "utf8"));
3197
+ async function readInputArg(value) {
3198
+ const trimmed = value.trimStart();
3199
+ if (trimmed.startsWith("{")) return parseJsonObject(value, "--input");
3200
+ return readInputFile(trimmed);
3201
+ }
3202
+ function parseJsonObject(raw, source) {
3203
+ let parsed;
3204
+ try {
3205
+ parsed = JSON.parse(raw);
3206
+ } catch (error) {
3207
+ const message = error instanceof Error ? error.message : String(error);
3208
+ throw new Error(`${source}: invalid JSON: ${message}`);
3209
+ }
3210
+ return assertInputObject(parsed, source);
3211
+ }
3212
+ async function readInputFile(filePath) {
3213
+ const raw = await fs.readFile(filePath, "utf8");
3214
+ if (isYamlPath(filePath)) return parseYamlObject(raw, filePath);
3215
+ return parseJsonObject(raw, filePath);
3150
3216
  }
3151
- function resolveSpecPath(options) {
3152
- if (options.spec && options.workflow) throw new Error("Use only one of --spec or --workflow.");
3153
- if (options.spec) return options.spec;
3154
- if (options.workflow) return path.join(options.global ? globalWorkflowsDir() : projectWorkflowsDir(), options.workflow, "workflow.spec.json");
3155
- throw new Error("Provide --spec <path> or --workflow <name>.");
3217
+ function parseYamlObject(raw, source) {
3218
+ let parsed;
3219
+ try {
3220
+ parsed = YAML.parse(raw);
3221
+ } catch (error) {
3222
+ const message = error instanceof Error ? error.message : String(error);
3223
+ throw new Error(`${source}: invalid YAML: ${message}`);
3224
+ }
3225
+ return assertInputObject(parsed, source);
3226
+ }
3227
+ function assertInputObject(parsed, source) {
3228
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${source} must contain one input object.`);
3229
+ return parsed;
3230
+ }
3231
+ function isYamlPath(filePath) {
3232
+ const lower = filePath.toLowerCase();
3233
+ return lower.endsWith(".yaml") || lower.endsWith(".yml");
3156
3234
  }
3157
3235
  //#endregion
3158
- //#region src/commands/diagnose.ts
3159
- function registerDiagnose(program) {
3160
- program.command("diagnose").argument("<run>", "logical run id or run directory").option("--wait", "wait until diagnostic preparation is reflected in the run index").option("--json", "print JSON").action(async (runArg, options) => {
3161
- const locator = await resolveRunLocator(runArg);
3162
- const started = await startDiagnosticRun(locator.cwd, locator.runId);
3163
- const index = options.wait ? await waitForDiagnostic(locator.cwd, locator.runId) : started;
3164
- const diagnostics = await buildRunDiagnosticsView(locator.cwd, index, { eventTailLimit: 50 });
3165
- const output = {
3166
- ok: true,
3167
- runId: locator.runId,
3168
- diagnosticId: `diagnostic-${index.agentUsage.recoveryCalls}`,
3169
- status: index.status,
3170
- message: options.wait ? "Diagnostic preparation finished." : "Diagnostic preparation started.",
3171
- diagnostics
3172
- };
3173
- if (options.json) printJson(output);
3174
- else process.stdout.write(`${output.message}\n`);
3175
- });
3236
+ //#region src/commands/run-selection.tsx
3237
+ async function resolveOptionalRunArg(input) {
3238
+ if (input.runArg) return input.runArg;
3239
+ if (input.json) {
3240
+ printJson(await listRunSummaries());
3241
+ return;
3242
+ }
3243
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3244
+ process.stdout.write(formatRunSummaryList(await listRunSummaries()));
3245
+ return;
3246
+ }
3247
+ return pickRun(input.title);
3176
3248
  }
3177
- async function waitForDiagnostic(cwd, runId) {
3178
- return syncRun(cwd, runId, { startPending: false });
3249
+ async function pickRun(title) {
3250
+ const { RunPickerApp } = await import("./run-picker-app-utJ2f5CU.mjs");
3251
+ let selected;
3252
+ await render(React.createElement(RunPickerApp, {
3253
+ title,
3254
+ onSelect: (runId) => {
3255
+ selected = runId;
3256
+ }
3257
+ })).waitUntilExit();
3258
+ return selected;
3179
3259
  }
3180
3260
  //#endregion
3181
3261
  //#region src/commands/follow.ts
3262
+ function printNdjson(value) {
3263
+ process.stdout.write(`${JSON.stringify(value)}\n`);
3264
+ }
3182
3265
  function registerFollow(program) {
3183
- program.command("follow").argument("<run>", "logical run id or run directory").option("--json", "print JSON").action(async (runArg, options) => {
3266
+ program.command("follow").argument("[run]", "logical run id or run directory").option("--json", "print JSON").action(async (runArg, options) => {
3267
+ runArg = await resolveOptionalRunArg({
3268
+ runArg,
3269
+ json: options.json,
3270
+ title: "Select a run to follow"
3271
+ });
3272
+ if (!runArg) return;
3184
3273
  const locator = await resolveRunLocator(runArg);
3185
3274
  const index = await syncRun(locator.cwd, locator.runId, { startPending: false });
3186
- const spec = WorkflowSpecSchema.parse(JSON.parse(await fs.readFile(path.join(runDir(locator.runId, locator.cwd), "workflow.spec.json"), "utf8")));
3187
- if (options.json) printJson(await buildRunMonitorView(locator.cwd, spec, index));
3188
- else {
3189
- const view = await runViewFromIndex(locator.cwd, spec, index);
3190
- process.stdout.write(`run ${locator.runId} status=${view.status} workflow=${view.workflowName}${view.gateVerdict ? ` verdict=${view.gateVerdict}` : ""}\n`);
3275
+ const dir = runDir(locator.runId, locator.cwd);
3276
+ const eventsPath = path.join(dir, "events.ndjson");
3277
+ if (terminalRunStatus(index.status)) {
3278
+ if (options.json) {
3279
+ const spec = WorkflowSpecSchema.parse(YAML.parse(await fs.readFile(path.join(dir, "workflow.spec.yaml"), "utf8")));
3280
+ printNdjson(await buildRunMonitorView(locator.cwd, spec, index));
3281
+ } else process.stdout.write(`run ${locator.runId} ${index.status}\n`);
3282
+ return;
3191
3283
  }
3192
- });
3193
- }
3194
- //#endregion
3195
- //#region src/commands/generate.ts
3196
- function registerGenerate(program) {
3197
- program.command("generate").option("--name <name>", "draft name", "draft-workflow").option("--json", "print JSON").action(async (options) => {
3198
- const dir = draftsDir();
3199
- await fs.mkdir(dir, { recursive: true });
3200
- const file = path.join(dir, `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${options.name}.workflow.spec.json`);
3201
- const spec = {
3202
- schemaVersion: "acpus.workflow/v1",
3203
- name: options.name,
3204
- description: "Draft workflow scaffold. Main Agent should edit before running.",
3205
- root: "plan",
3206
- inputs: {
3207
- task: {
3208
- type: "string",
3209
- default: ""
3210
- },
3211
- cwd: {
3212
- type: "path",
3213
- default: "."
3214
- }
3215
- },
3216
- roles: { planner: {
3217
- category: "planning",
3218
- agent: "claude",
3219
- mode: "readOnly"
3220
- } },
3221
- limits: { stageTimeoutMinutes: 30 },
3222
- stages: [{
3223
- id: "plan",
3224
- kind: "agentTask",
3225
- role: "planner",
3226
- prompt: "Plan the requested workflow task: ${task}",
3227
- variables: [{
3228
- name: "task",
3229
- source: "input.task"
3230
- }]
3231
- }, {
3232
- id: "gate",
3233
- kind: "gate",
3234
- dependsOn: ["plan"]
3235
- }]
3284
+ if (!workerIsActive(index.worker)) {
3285
+ if (options.json) {
3286
+ const spec = WorkflowSpecSchema.parse(YAML.parse(await fs.readFile(path.join(dir, "workflow.spec.yaml"), "utf8")));
3287
+ printNdjson(await buildRunMonitorView(locator.cwd, spec, index));
3288
+ } else process.stdout.write(`run ${locator.runId} ${index.status} (no active worker)\n`);
3289
+ return;
3290
+ }
3291
+ let linesRead = 0;
3292
+ let done = false;
3293
+ const onSigint = () => {
3294
+ done = true;
3236
3295
  };
3237
- await fs.writeFile(file, `${JSON.stringify(spec, null, 2)}\n`, "utf8");
3238
- if (options.json) printJson({
3239
- ok: true,
3240
- path: file
3241
- });
3242
- else process.stdout.write(`${file}\n`);
3296
+ process.on("SIGINT", onSigint);
3297
+ linesRead = await readAndPrintNewEvents(eventsPath, linesRead, options.json ?? false);
3298
+ while (!done) {
3299
+ await sleep(500);
3300
+ linesRead = await readAndPrintNewEvents(eventsPath, linesRead, options.json ?? false);
3301
+ try {
3302
+ const idx = await readRunIndex(locator.cwd, locator.runId);
3303
+ if (terminalRunStatus(idx.status)) {
3304
+ if (options.json) {
3305
+ const spec = WorkflowSpecSchema.parse(YAML.parse(await fs.readFile(path.join(dir, "workflow.spec.yaml"), "utf8")));
3306
+ printNdjson(await buildRunMonitorView(locator.cwd, spec, idx));
3307
+ } else process.stdout.write(`run ${locator.runId} ${idx.status}\n`);
3308
+ done = true;
3309
+ }
3310
+ } catch {}
3311
+ }
3312
+ process.removeListener("SIGINT", onSigint);
3243
3313
  });
3244
3314
  }
3315
+ async function readAndPrintNewEvents(eventsPath, linesRead, jsonMode) {
3316
+ let content;
3317
+ try {
3318
+ content = await fs.readFile(eventsPath, "utf8");
3319
+ } catch {
3320
+ return linesRead;
3321
+ }
3322
+ const lines = content.split("\n");
3323
+ const totalLines = content.endsWith("\n") ? lines.length - 1 : lines.length;
3324
+ for (let i = linesRead; i < totalLines; i++) {
3325
+ const line = lines[i];
3326
+ if (!line) continue;
3327
+ if (jsonMode) process.stdout.write(`${line}\n`);
3328
+ else formatEvent(line);
3329
+ }
3330
+ return totalLines;
3331
+ }
3332
+ function formatEvent(line) {
3333
+ let event;
3334
+ try {
3335
+ event = JSON.parse(line);
3336
+ } catch {
3337
+ process.stdout.write(`${line}\n`);
3338
+ return;
3339
+ }
3340
+ const type = typeof event.type === "string" ? event.type : "unknown";
3341
+ switch (type) {
3342
+ case "worker_started":
3343
+ process.stdout.write(`worker started pid=${event.pid ?? "?"} run=${event.runId ?? "?"}\n`);
3344
+ break;
3345
+ case "run_progress": {
3346
+ const stageParts = (Array.isArray(event.changedStages) ? event.changedStages : []).map((s) => `${s.id}=${s.status}`);
3347
+ process.stdout.write(`progress status=${event.status ?? "?"}${stageParts.length > 0 ? ` ${stageParts.join(" ")}` : ""}\n`);
3348
+ break;
3349
+ }
3350
+ case "worker_exited":
3351
+ process.stdout.write(`worker exited pid=${event.pid ?? "?"} status=${event.status ?? "?"}\n`);
3352
+ break;
3353
+ case "runtime_fatal":
3354
+ process.stdout.write(`fatal: ${event.message ?? String(event.error ?? "")}\n`);
3355
+ break;
3356
+ default:
3357
+ process.stdout.write(`event ${type}\n`);
3358
+ break;
3359
+ }
3360
+ }
3361
+ function sleep(ms) {
3362
+ return new Promise((resolve) => setTimeout(resolve, ms));
3363
+ }
3245
3364
  //#endregion
3246
3365
  //#region src/commands/list.ts
3247
3366
  function registerList(program) {
3248
- program.command("list").argument("<kind>", "workflows|runs|drafts").option("--global", "list global workflows").option("--json", "print JSON").action(async (kind, options) => {
3249
- const dir = kind === "workflows" ? options.global ? globalWorkflowsDir() : projectWorkflowsDir() : kind === "runs" ? runsDir() : kind === "drafts" ? draftsDir() : "";
3250
- if (!dir) throw new Error("kind must be workflows, runs, or drafts");
3367
+ const list = program.command("list");
3368
+ list.command("workflows").option("--global", "list global workflows").option("--json", "print JSON").action(async (options) => {
3369
+ const kind = "workflows";
3370
+ const dir = options.global ? globalWorkflowsDir() : projectWorkflowsDir();
3371
+ const entries = await safeList(dir);
3372
+ const output = {
3373
+ kind,
3374
+ dir,
3375
+ entries
3376
+ };
3377
+ if (options.json) printJson(output);
3378
+ else {
3379
+ process.stdout.write(`${kind} in ${dir}\n`);
3380
+ for (const entry of entries) process.stdout.write(`- ${entry}\n`);
3381
+ }
3382
+ });
3383
+ list.command("runs").option("--json", "print JSON").action(async (options) => {
3384
+ const kind = "runs";
3385
+ const dir = runsDir();
3251
3386
  const entries = await safeList(dir);
3252
3387
  const output = {
3253
3388
  kind,
@@ -3272,52 +3407,68 @@ async function safeList(dir) {
3272
3407
  //#endregion
3273
3408
  //#region src/commands/monitor.ts
3274
3409
  function registerMonitor(program) {
3275
- program.command("monitor").argument("<run>", "logical run id or run directory").option("--json", "print JSON").action(async (runArg, options) => {
3276
- if (!optionJson(options)) {
3277
- const { MonitorApp } = await import("./monitor-app-CSjUPe9j.mjs");
3410
+ program.command("monitor").argument("[run]", "logical run id or run directory").option("--json", "print JSON").action(async (runArg, options) => {
3411
+ const json = optionJson(options);
3412
+ runArg = await resolveOptionalRunArg({
3413
+ runArg,
3414
+ json,
3415
+ title: "Select a run to monitor"
3416
+ });
3417
+ if (!runArg) return;
3418
+ if (!json) {
3419
+ const { MonitorApp } = await import("./monitor-app-CPlEcyHR.mjs");
3278
3420
  await render(React.createElement(MonitorApp, { runArg })).waitUntilExit();
3279
3421
  return;
3280
3422
  }
3281
3423
  const { locator, spec, index } = await loadObservedRun(runArg);
3282
3424
  printJson(await buildRunMonitorView(locator.cwd, spec, index));
3283
- }).command("detail").argument("<run>", "logical run id or run directory").argument("<task-id>", "Stage Task id").option("--json", "print JSON").action(async (runArg, taskId, options) => {
3284
- if (!optionJson(options)) throw new Error("Usage: monitor detail <run> <task-id> --json");
3425
+ }).command("detail").argument("<run>", "logical run id or run directory").argument("<task-id>", "Stage Task id").option("--json", "print JSON").action(async (runArg, taskId, options, command) => {
3426
+ if (!optionJson(command ?? options)) throw new Error("Usage: monitor detail <run> <task-id> --json");
3285
3427
  const { locator, spec, index } = await loadObservedRun(runArg);
3286
3428
  printJson(await buildTaskDetailView(locator.cwd, spec, index, taskId));
3287
3429
  });
3288
3430
  }
3289
3431
  function optionJson(options) {
3290
3432
  if ("json" in options && typeof options.json === "boolean") return options.json;
3433
+ if ("getOptionValue" in options && typeof options.getOptionValue === "function" && options.getOptionValue("json")) return true;
3291
3434
  if ("opts" in options && typeof options.opts === "function") {
3292
3435
  if (options.opts().json) return true;
3293
- if (options.getOptionValue?.("json")) return true;
3294
3436
  if (options.parent?.opts().json) return true;
3295
3437
  }
3296
- return process.argv.includes("--json");
3438
+ return false;
3297
3439
  }
3298
3440
  async function loadObservedRun(runArg) {
3299
3441
  const locator = await resolveRunLocator(runArg);
3300
3442
  return {
3301
3443
  locator,
3302
3444
  index: await syncRun(locator.cwd, locator.runId, { startPending: false }),
3303
- spec: WorkflowSpecSchema.parse(JSON.parse(await fs.readFile(path.join(runDir(locator.runId, locator.cwd), "workflow.spec.json"), "utf8")))
3445
+ spec: WorkflowSpecSchema.parse(YAML.parse(await fs.readFile(path.join(runDir(locator.runId, locator.cwd), "workflow.spec.yaml"), "utf8")))
3304
3446
  };
3305
3447
  }
3306
3448
  //#endregion
3307
- //#region src/commands/preview.ts
3308
- function registerPreview(program) {
3309
- program.command("preview").option("--spec <path>", "workflow spec path").option("--workflow <name>", "saved workflow name").option("--global", "resolve saved workflow from global directory").option("--json", "print JSON").action(async (options) => {
3310
- const specPath = resolveSpecPath(options);
3311
- const { spec, result } = await loadAndLint(specPath);
3312
- if (!spec) {
3449
+ //#region src/commands/plan.ts
3450
+ function registerPlan(program) {
3451
+ program.command("plan").argument("<spec>", "spec file path or saved workflow name (auto-detected)").option("--global", "resolve saved workflow from global directory").option("--quiet", "suppress plan preview, show only issues").option("--json", "print JSON").action(async (spec, options) => {
3452
+ const specPath = await resolveSpecArg({
3453
+ spec,
3454
+ global: options.global
3455
+ });
3456
+ const { spec: loaded, result } = await loadAndLint(specPath);
3457
+ if (!loaded) {
3313
3458
  if (options.json) printJson(result);
3314
3459
  else printIssues(result);
3315
3460
  process.exitCode = 1;
3316
3461
  return;
3317
3462
  }
3318
- const view = previewRunView(spec, [...result.warnings, ...result.errors], {
3319
- validate: `acpus validate --spec ${specPath}`,
3320
- run: `acpus run --spec ${specPath}`
3463
+ if (options.quiet) {
3464
+ if (options.json) printJson(result);
3465
+ else printIssues(result);
3466
+ if (!result.ok) process.exitCode = 1;
3467
+ return;
3468
+ }
3469
+ const view = previewRunView(loaded, [...result.warnings, ...result.errors], {
3470
+ plan: `acpus plan ${specPath}`,
3471
+ run: `acpus run ${specPath}`
3321
3472
  });
3322
3473
  if (options.json) printJson(view);
3323
3474
  else {
@@ -3338,27 +3489,6 @@ function registerPreview(program) {
3338
3489
  });
3339
3490
  }
3340
3491
  //#endregion
3341
- //#region src/commands/recover.ts
3342
- function registerRecover(program) {
3343
- program.command("recover").argument("<run>", "logical run id or run directory").option("--json", "print JSON").action(async (runArg, options) => {
3344
- const recovered = await recoverDriver(runArg);
3345
- const output = {
3346
- ok: true,
3347
- runId: recovered.runId,
3348
- runDir: recovered.runDir,
3349
- worker: recovered.worker,
3350
- message: "Run worker recovered."
3351
- };
3352
- if (options.json) printJson(output);
3353
- else {
3354
- process.stdout.write(`${output.message}\n`);
3355
- process.stdout.write(`runId=${output.runId}\n`);
3356
- process.stdout.write(`runDir=${output.runDir}\n`);
3357
- process.stdout.write(`worker=${output.worker.pid} status=${output.worker.status}\n`);
3358
- }
3359
- });
3360
- }
3361
- //#endregion
3362
3492
  //#region src/runtime/resume-policy.ts
3363
3493
  function parseResumePolicyOptions(options) {
3364
3494
  const fanout = {};
@@ -3409,7 +3539,7 @@ function parseResumePolicyOptions(options) {
3409
3539
  issues
3410
3540
  };
3411
3541
  }
3412
- function validateResumePolicy(spec, policy) {
3542
+ function validateResumePolicy(spec, policy, plan) {
3413
3543
  const issues = [];
3414
3544
  const stageIds = spec.stages.map((stage) => stage.id);
3415
3545
  for (const [stageId, fanoutPolicy] of Object.entries(policy.fanout)) {
@@ -3436,15 +3566,15 @@ function validateResumePolicy(spec, policy) {
3436
3566
  }));
3437
3567
  continue;
3438
3568
  }
3439
- const hasEditLane = stage.laneGroups.some((group) => group.lanes.some((lane) => spec.roles[lane.role]?.mode === "edit"));
3569
+ const hasEditLane = stage.lanes.some((lane) => lane.actor.mode === "edit");
3440
3570
  if (fanoutPolicy.allowPartial && hasEditLane) issues.push(issue({
3441
3571
  code: "RESUME_POLICY_PARTIAL_REQUIRES_READONLY",
3442
3572
  severity: "error",
3443
3573
  path: `${policyPath}/allowPartial`,
3444
3574
  message: `Resume cannot enable partial results for edit fanout stage ${stageId}.`,
3445
- suggestions: ["Use diagnose for recovery advice, then start a new run if edit recovery is needed."]
3575
+ suggestions: ["Inspect monitor details, then start a new run if edit recovery is needed."]
3446
3576
  }));
3447
- const compiledMaxItems = stage.limits?.maxFanoutItems ?? 1;
3577
+ const compiledMaxItems = plan?.fanout.find((entry) => entry.stageId === stageId)?.maxItems ?? staticLimitOrDefault(stage.limits?.maxFanoutItems, 1);
3448
3578
  if (fanoutPolicy.maxItems !== void 0 && fanoutPolicy.maxItems > compiledMaxItems) issues.push(issue({
3449
3579
  code: "RESUME_POLICY_MAX_ITEMS_NOT_TIGHTENING",
3450
3580
  severity: "error",
@@ -3487,36 +3617,56 @@ function parseStageInteger(value) {
3487
3617
  }
3488
3618
  //#endregion
3489
3619
  //#region src/commands/resume.ts
3620
+ const TERMINAL_STATUSES = new Set(["completed", "cancelled"]);
3490
3621
  function registerResume(program) {
3491
- program.command("resume").argument("<run>", "logical run id or run directory").option("--allow-partial-fanout <stage...>", "allow partial results for read-only fanout stage(s) on resume").option("--max-fanout-items <stage=count...>", "tighten max fanout items for stage(s), bounded by the compiled snapshot").option("--skip-fanout-item <stage=index...>", "skip zero-based fanout item index(es) on resume").option("--wait", "advance until terminal").option("--json", "print JSON").action(async (runArg, options) => {
3622
+ program.command("resume").argument("<run>", "logical run id or run directory").option("--allow-partial-fanout <stage...>", "allow partial results for read-only fanout stage(s) on resume").option("--max-fanout-items <stage=count...>", "tighten max fanout items for stage(s), bounded by the compiled snapshot").option("--skip-fanout-item <stage=index...>", "skip zero-based fanout item index(es) on resume").option("--force", "bypass active-worker check to restart a stale worker").option("--wait", "advance until terminal").option("--json", "print JSON").action(async (runArg, options) => {
3492
3623
  const locator = await resolveRunLocator(runArg);
3493
3624
  const spec = await readRunSpec(locator.cwd, locator.runId);
3625
+ const plan = await readRunPlan(locator.cwd, locator.runId);
3494
3626
  const parsedPolicy = parseResumePolicyOptions(options);
3495
- const policyIssues = [...parsedPolicy.issues, ...validateResumePolicy(spec, parsedPolicy.policy)];
3627
+ const policyIssues = [...parsedPolicy.issues, ...validateResumePolicy(spec, parsedPolicy.policy, plan)];
3496
3628
  if (policyIssues.some((entry) => entry.severity !== "warning")) {
3497
3629
  printResumeIssues(options.json, policyIssues);
3498
3630
  process.exitCode = 2;
3499
3631
  return;
3500
3632
  }
3501
3633
  const index = await readRunIndex(locator.cwd, locator.runId);
3502
- if (index.status !== "blocked" && index.status !== "failed" && index.status !== "diagnosed_blocked") {
3634
+ const allowedStatuses = options.force ? new Set([
3635
+ "blocked",
3636
+ "failed",
3637
+ "running",
3638
+ "pending"
3639
+ ]) : new Set(["blocked", "failed"]);
3640
+ if (TERMINAL_STATUSES.has(index.status) || !allowedStatuses.has(index.status)) {
3641
+ const hint = options.force && (index.status === "running" || index.status === "pending") ? "Use monitor to observe the active run." : index.status === "running" || index.status === "pending" ? "Use monitor to observe the active run, recover if its worker is stale, or use --force to bypass." : "Start a new run for completed workflows.";
3503
3642
  printResumeIssues(options.json, [issue({
3504
3643
  code: "RESUME_POLICY_INVALID",
3505
3644
  severity: "error",
3506
3645
  path: "/",
3507
- message: `Run ${locator.runId} is ${index.status}; resume is only for blocked, failed, or diagnosed_blocked runs.`,
3508
- suggestions: [index.status === "running" || index.status === "pending" ? "Use monitor to observe the active run, or recover if its worker is stale." : "Start a new run for completed workflows."]
3646
+ message: `Run ${locator.runId} is ${index.status}; resume is only for blocked or failed runs${options.force ? " (or running with --force)" : ""}.`,
3647
+ suggestions: [hint]
3509
3648
  })]);
3510
3649
  process.exitCode = 2;
3511
3650
  return;
3512
3651
  }
3513
- if (workerIsActive(index.worker)) {
3652
+ if (workerIsActive(index.worker) && !options.force) {
3514
3653
  printResumeIssues(options.json, [issue({
3515
3654
  code: "RESUME_POLICY_INVALID",
3516
3655
  severity: "error",
3517
3656
  path: "/",
3518
3657
  message: `Run ${locator.runId} already has an active worker pid=${index.worker?.pid}; resume cannot take ownership.`,
3519
- suggestions: ["Use monitor to observe the active run, or recover after the worker becomes stale."]
3658
+ suggestions: ["Use monitor to observe the active run, recover after the worker becomes stale, or use --force to bypass."]
3659
+ })]);
3660
+ process.exitCode = 2;
3661
+ return;
3662
+ }
3663
+ if (workerIsActive(index.worker) && options.force) {
3664
+ printResumeIssues(options.json, [issue({
3665
+ code: "RESUME_POLICY_INVALID",
3666
+ severity: "error",
3667
+ path: "/",
3668
+ message: `Run ${locator.runId} has an active worker pid=${index.worker?.pid}; --force cannot take ownership of an active worker.`,
3669
+ suggestions: ["Use monitor to observe the active run, or wait for the worker to become stale."]
3520
3670
  })]);
3521
3671
  process.exitCode = 2;
3522
3672
  return;
@@ -3528,7 +3678,7 @@ function registerResume(program) {
3528
3678
  await writeRunIndex(locator.cwd, reset);
3529
3679
  let finalIndex;
3530
3680
  try {
3531
- finalIndex = options.wait ? await runWorkflowWorker(locator.cwd, locator.runId) : await readRunIndex(locator.cwd, locator.runId);
3681
+ finalIndex = options.wait ? await runWorkflowWorker(locator.cwd, locator.runId, { force: options.force }) : await readRunIndex(locator.cwd, locator.runId);
3532
3682
  const worker = options.wait ? finalIndex.worker : await spawnBackgroundWorker(locator.cwd, locator.runId);
3533
3683
  if (!options.wait) finalIndex = {
3534
3684
  ...await readRunIndex(locator.cwd, locator.runId),
@@ -3546,11 +3696,21 @@ function registerResume(program) {
3546
3696
  message: options.wait ? "Run resume reached a terminal state." : "Run resume started a background worker."
3547
3697
  };
3548
3698
  if (options.json) printJson(output);
3549
- else process.stdout.write(`${output.message}\n`);
3699
+ else {
3700
+ process.stdout.write(`${output.message}\n`);
3701
+ if (!options.wait) {
3702
+ process.stdout.write(`runId=${output.runId}\n`);
3703
+ process.stdout.write(`runDir=${runDir(locator.runId, locator.cwd)}\n`);
3704
+ if (output.worker) process.stdout.write(`worker=${output.worker.pid ?? "unknown"}\n`);
3705
+ }
3706
+ }
3550
3707
  });
3551
3708
  }
3552
3709
  async function readRunSpec(cwd, runId) {
3553
- return WorkflowSpecSchema.parse(JSON.parse(await fs.readFile(path.join(runDir(runId, cwd), "workflow.spec.json"), "utf8")));
3710
+ return WorkflowSpecSchema.parse(YAML.parse(await fs.readFile(path.join(runDir(runId, cwd), "workflow.spec.yaml"), "utf8")));
3711
+ }
3712
+ async function readRunPlan(cwd, runId) {
3713
+ return JSON.parse(await fs.readFile(path.join(runDir(runId, cwd), "execution-plan.json"), "utf8"));
3554
3714
  }
3555
3715
  function resetRecoverableStages(index, spec) {
3556
3716
  const kindByStage = new Map(spec.stages.map((stage) => [stage.id, stage.kind]));
@@ -3605,8 +3765,17 @@ async function markKnownRunFatal$1(cwd, runId, error) {
3605
3765
  //#endregion
3606
3766
  //#region src/commands/run.ts
3607
3767
  function registerRun(program) {
3608
- program.command("run").option("--spec <path>", "workflow spec path").option("--workflow <name>", "saved workflow name").option("--global", "resolve saved workflow from global directory").option("--input-json <path>", "raw workflow input JSON file").option("--wait", "run in the foreground until the workflow reaches a terminal state").option("--json", "print JSON").action(async (options) => {
3609
- const specPath = resolveSpecPath(options);
3768
+ program.command("run").argument("[spec]", "spec file path or workflow name").option("--global", "resolve saved workflow from global directory").option("--input <json-or-yaml-or-path>", "workflow input as an inline JSON object or JSON/YAML file path").option("--wait", "run in the foreground until the workflow reaches a terminal state").option("--json", "print JSON").action(async (specArg, options) => {
3769
+ if (!specArg) {
3770
+ if (options.json) printJson(resultFromIssues("run", []));
3771
+ else process.stderr.write("Error: provide a spec file path or workflow name.\n");
3772
+ process.exitCode = 1;
3773
+ return;
3774
+ }
3775
+ const specPath = await resolveSpecArg({
3776
+ spec: specArg,
3777
+ global: options.global
3778
+ });
3610
3779
  const { spec, result } = await loadAndLint(specPath);
3611
3780
  if (!spec || !result.ok) {
3612
3781
  if (options.json) printJson(result);
@@ -3614,7 +3783,7 @@ function registerRun(program) {
3614
3783
  process.exitCode = 1;
3615
3784
  return;
3616
3785
  }
3617
- const input = applyInputDefaults(spec, options.inputJson ? await readJsonFile(options.inputJson) : {});
3786
+ const input = applyInputDefaults(spec, options.input ? await readInputArg(options.input) : {});
3618
3787
  const inputResult = resultFromIssues("input", validateWorkflowInput(spec, input));
3619
3788
  if (!inputResult.ok) {
3620
3789
  if (options.json) printJson(inputResult);
@@ -3630,7 +3799,7 @@ function registerRun(program) {
3630
3799
  try {
3631
3800
  if (options.wait) {
3632
3801
  const finalIndex = await runWorkflowWorker(process.cwd(), prepared.logicalRunId, { reporter: waitReporter(options.json) });
3633
- emitTerminalSummary(options.json, prepared.logicalRunId, prepared.dir, finalIndex);
3802
+ await emitTerminalSummary(options.json, prepared.logicalRunId, prepared.dir, finalIndex, spec);
3634
3803
  return;
3635
3804
  }
3636
3805
  const worker = await spawnBackgroundWorker(process.cwd(), prepared.logicalRunId);
@@ -3667,7 +3836,7 @@ function waitReporter(json) {
3667
3836
  } else process.stdout.write(`worker exited pid=${event.pid} status=${event.status} exit=${event.exitCode ?? ""}\n`);
3668
3837
  };
3669
3838
  }
3670
- function emitTerminalSummary(json, runId, dir, index) {
3839
+ async function emitTerminalSummary(json, runId, dir, index, spec) {
3671
3840
  const summary = {
3672
3841
  type: "terminal_summary",
3673
3842
  ok: index.status !== "failed" && index.status !== "cancelled",
@@ -3675,7 +3844,8 @@ function emitTerminalSummary(json, runId, dir, index) {
3675
3844
  runDir: dir,
3676
3845
  status: index.status,
3677
3846
  blockedReason: index.blockedReason,
3678
- gateVerdict: index.gateVerdict
3847
+ gateVerdict: index.gateVerdict,
3848
+ finalOutput: json ? await readFinalOutput(dir, spec) : void 0
3679
3849
  };
3680
3850
  if (json) process.stdout.write(`${JSON.stringify(summary)}\n`);
3681
3851
  else process.stdout.write(`terminal status=${summary.status}${summary.gateVerdict ? ` verdict=${summary.gateVerdict}` : ""}\n`);
@@ -3707,9 +3877,12 @@ function registerRunWorker(program) {
3707
3877
  //#endregion
3708
3878
  //#region src/commands/save.ts
3709
3879
  function registerSave(program) {
3710
- program.command("save").argument("<name>", "workflow name").requiredOption("--spec <path>", "workflow spec path").option("--overwrite", "overwrite existing workflow").option("--global", "save to global workflow directory").option("--json", "print JSON").action(async (name, options) => {
3711
- const { spec, result } = await loadAndLint(options.spec);
3712
- if (!spec || !result.ok) {
3880
+ program.command("save").argument("<name>", "workflow name").argument("<spec>", "spec file path or workflow name").option("--overwrite", "overwrite existing workflow").option("--global", "save to global workflow directory").option("--json", "print JSON").action(async (name, spec, options) => {
3881
+ const { spec: workflowSpec, result } = await loadAndLint(await resolveSpecArg({
3882
+ spec,
3883
+ global: options.global
3884
+ }));
3885
+ if (!workflowSpec || !result.ok) {
3713
3886
  if (options.json) printJson(result);
3714
3887
  else printIssues(result);
3715
3888
  process.exitCode = 1;
@@ -3719,11 +3892,11 @@ function registerSave(program) {
3719
3892
  const target = path.join(root, name);
3720
3893
  await ensureEmptyOrOverwrite(target, options.overwrite);
3721
3894
  await fs.mkdir(target, { recursive: true });
3722
- const plan = compileExecutionPlan(spec);
3723
- await fs.writeFile(path.join(target, "workflow.spec.json"), `${JSON.stringify(spec, null, 2)}\n`, "utf8");
3895
+ const plan = compileExecutionPlan(workflowSpec, { input: applyInputDefaults(workflowSpec) });
3896
+ await fs.writeFile(path.join(target, "workflow.spec.yaml"), stringifyWorkflowSpec(workflowSpec), "utf8");
3724
3897
  await fs.writeFile(path.join(target, "execution-plan.json"), `${JSON.stringify(plan, null, 2)}\n`, "utf8");
3725
3898
  await writeHelperSnapshot(target);
3726
- await fs.writeFile(path.join(target, "README.md"), `# ${name}\n\n${spec.description || "Saved acpus workflow."}\n\nThis directory is a saved runtime-driven workflow snapshot. The stable authoring interface is workflow.spec.json; execution-plan.json is the derived runtime plan and should normally be regenerated from the spec.\n\nRun with acpus using workflow.spec.json.\n`, "utf8");
3899
+ await fs.writeFile(path.join(target, "README.md"), `# ${name}\n\n${workflowSpec.description || "Saved acpus workflow."}\n\nThis directory is a saved runtime-driven workflow snapshot. The stable authoring interface is workflow.spec.yaml; execution-plan.json is the derived runtime plan and should normally be regenerated from the spec.\n\nRun with acpus using workflow.spec.yaml.\n`, "utf8");
3727
3900
  const output = {
3728
3901
  ok: true,
3729
3902
  workflow: name,
@@ -3799,48 +3972,32 @@ async function ascendToPackageRoot(start) {
3799
3972
  //#endregion
3800
3973
  //#region src/commands/show.ts
3801
3974
  function registerShow(program) {
3802
- program.command("show").argument("<kind>", "workflow|run|draft").argument("<name>", "name or run id").option("--global", "show global workflow").option("--json", "print JSON").action(async (kind, name, options) => {
3803
- const file = await resolveShowFile(kind, name, options.global);
3804
- if (!file) throw new Error("kind must be workflow, run, or draft");
3975
+ const show = program.command("show");
3976
+ show.command("workflow").argument("<name>", "workflow name").option("--global", "show global workflow").option("--json", "print JSON").action(async (name, options) => {
3977
+ const file = path.join(options.global ? globalWorkflowsDir() : projectWorkflowsDir(), name, "workflow.spec.yaml");
3805
3978
  const text = await fs.readFile(file, "utf8");
3806
- if (options.json) printJson(JSON.parse(text));
3979
+ if (options.json) printJson(YAML.parse(text));
3807
3980
  else process.stdout.write(text);
3808
3981
  });
3809
- }
3810
- async function resolveShowFile(kind, name, global) {
3811
- if (kind === "workflow") return path.join(global ? globalWorkflowsDir() : projectWorkflowsDir(), name, "workflow.spec.json");
3812
- if (kind === "run") {
3813
- const locator = await resolveRunLocator(name);
3814
- return path.join(runDir(locator.runId, locator.cwd), "run.json");
3815
- }
3816
- if (kind === "draft") return path.join(draftsDir(), name);
3817
- return "";
3818
- }
3819
- //#endregion
3820
- //#region src/commands/validate.ts
3821
- function registerValidate(program) {
3822
- program.command("validate").option("--spec <path>", "workflow spec path").option("--workflow <name>", "saved workflow name").option("--global", "resolve saved workflow from global directory").option("--json", "print JSON").action(async (options) => {
3823
- const { result } = await loadAndLint(resolveSpecPath(options));
3824
- if (options.json) printJson(result);
3825
- else printIssues(result);
3826
- if (!result.ok) process.exitCode = 1;
3982
+ show.command("run").argument("<id>", "run id or directory").option("--json", "print JSON").action(async (id, options) => {
3983
+ const locator = await resolveRunLocator(id);
3984
+ const file = path.join(runDir(locator.runId, locator.cwd), "run.json");
3985
+ const text = await fs.readFile(file, "utf8");
3986
+ if (options.json) printJson(JSON.parse(text));
3987
+ else process.stdout.write(text);
3827
3988
  });
3828
3989
  }
3829
3990
  //#endregion
3830
3991
  //#region src/cli.ts
3831
3992
  const program = new Command();
3832
- program.name("acpus").description("Acpus — compose, conduct, recover, catalogue.").version("0.1.0");
3833
- registerValidate(program);
3834
- registerPreview(program);
3993
+ program.name("acpus").description("Acpus — compose, conduct, catalogue.").version("0.1.0");
3994
+ registerPlan(program);
3835
3995
  registerSave(program);
3836
- registerGenerate(program);
3837
3996
  registerRun(program);
3838
3997
  registerFollow(program);
3839
3998
  registerMonitor(program);
3840
3999
  registerResume(program);
3841
4000
  registerRunWorker(program);
3842
- registerRecover(program);
3843
- registerDiagnose(program);
3844
4001
  registerList(program);
3845
4002
  registerShow(program);
3846
4003
  program.parseAsync(process.argv).catch((error) => {
@@ -3857,4 +4014,4 @@ program.parseAsync(process.argv).catch((error) => {
3857
4014
  process.exitCode = 1;
3858
4015
  });
3859
4016
  //#endregion
3860
- export {};
4017
+ export { resolveRunLocator as n, listRunSummaries as t };