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/README.md +45 -32
- package/dist/cli.mjs +427 -270
- package/dist/index.d.mts +980 -1528
- package/dist/index.mjs +243 -2
- package/dist/{monitor-app-CSjUPe9j.mjs → monitor-app-CPlEcyHR.mjs} +8 -73
- package/dist/monitor-rendering-LGr9Ebd_.mjs +78 -0
- package/dist/run-picker-app-utJ2f5CU.mjs +104 -0
- package/dist/{run-workflow-CbxKhAqF.mjs → run-workflow-DdIAC8Zu.mjs} +3844 -4508
- package/package.json +2 -2
- package/schemas/workflow-spec.schema.json +1915 -1308
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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/
|
|
3020
|
-
async function
|
|
3021
|
-
const
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
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
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
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
|
|
3074
|
-
const
|
|
3075
|
-
const
|
|
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
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
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
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
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("
|
|
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("
|
|
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
|
|
3149
|
-
|
|
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
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
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/
|
|
3159
|
-
function
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
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
|
|
3178
|
-
|
|
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("
|
|
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
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
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
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
const
|
|
3202
|
-
|
|
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
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
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
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
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("
|
|
3276
|
-
|
|
3277
|
-
|
|
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
|
|
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(
|
|
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/
|
|
3308
|
-
function
|
|
3309
|
-
program.command("
|
|
3310
|
-
const specPath =
|
|
3311
|
-
|
|
3312
|
-
|
|
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
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
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.
|
|
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: ["
|
|
3575
|
+
suggestions: ["Inspect monitor details, then start a new run if edit recovery is needed."]
|
|
3446
3576
|
}));
|
|
3447
|
-
const compiledMaxItems = stage.limits?.maxFanoutItems
|
|
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
|
-
|
|
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
|
|
3508
|
-
suggestions: [
|
|
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,
|
|
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
|
|
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(
|
|
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").
|
|
3609
|
-
|
|
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.
|
|
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").
|
|
3711
|
-
const { spec, result } = await loadAndLint(
|
|
3712
|
-
|
|
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(
|
|
3723
|
-
await fs.writeFile(path.join(target, "workflow.spec.
|
|
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${
|
|
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
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
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(
|
|
3979
|
+
if (options.json) printJson(YAML.parse(text));
|
|
3807
3980
|
else process.stdout.write(text);
|
|
3808
3981
|
});
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
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,
|
|
3833
|
-
|
|
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 };
|