@bastani/atomic 0.5.20 → 0.5.21-1
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/.agents/skills/workflow-creator/SKILL.md +78 -8
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +75 -0
- package/dist/sdk/components/orchestrator-panel.d.ts +23 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -1
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/errors.d.ts +12 -0
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/runtime/discovery.d.ts +55 -12
- package/dist/sdk/runtime/discovery.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/loader.d.ts.map +1 -1
- package/dist/sdk/runtime/status-writer.d.ts +101 -0
- package/dist/sdk/runtime/status-writer.d.ts.map +1 -0
- package/dist/sdk/runtime/version-compat.d.ts +28 -0
- package/dist/sdk/runtime/version-compat.d.ts.map +1 -0
- package/dist/sdk/types.d.ts +21 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/index.d.ts +1 -1
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +57 -3
- package/src/commands/cli/session.test.ts +43 -0
- package/src/commands/cli/session.ts +18 -8
- package/src/commands/cli/workflow-command.test.ts +10 -4
- package/src/commands/cli/workflow-inputs.test.ts +322 -0
- package/src/commands/cli/workflow-inputs.ts +219 -0
- package/src/commands/cli/workflow-status.test.ts +451 -0
- package/src/commands/cli/workflow-status.ts +330 -0
- package/src/commands/cli/workflow.test.ts +10 -3
- package/src/commands/cli/workflow.ts +57 -18
- package/src/sdk/components/orchestrator-panel.tsx +36 -1
- package/src/sdk/components/workflow-picker-panel.tsx +167 -18
- package/src/sdk/define-workflow.ts +1 -0
- package/src/sdk/errors.ts +20 -0
- package/src/sdk/runtime/discovery.ts +94 -20
- package/src/sdk/runtime/executor.ts +37 -0
- package/src/sdk/runtime/loader.ts +29 -2
- package/src/sdk/runtime/status-writer.test.ts +245 -0
- package/src/sdk/runtime/status-writer.ts +201 -0
- package/src/sdk/runtime/version-compat.ts +68 -0
- package/src/sdk/types.ts +21 -0
- package/src/sdk/workflows/index.ts +1 -0
|
@@ -42,7 +42,10 @@ import { useState, useEffect, useMemo, useRef, useCallback, useContext, createCo
|
|
|
42
42
|
import { useLatest } from "./hooks.ts";
|
|
43
43
|
import { resolveTheme, type TerminalTheme } from "../runtime/theme.ts";
|
|
44
44
|
import type { AgentType, WorkflowInput } from "../types.ts";
|
|
45
|
-
import type {
|
|
45
|
+
import type {
|
|
46
|
+
WorkflowWithMetadata,
|
|
47
|
+
WorkflowMetadataStatus,
|
|
48
|
+
} from "../runtime/discovery.ts";
|
|
46
49
|
import { ErrorBoundary } from "./error-boundary.tsx";
|
|
47
50
|
|
|
48
51
|
// ─── Theme ──────────────────────────────────────
|
|
@@ -258,6 +261,38 @@ export function buildRows(entries: ListEntry[], query: string): ListRow[] {
|
|
|
258
261
|
return rows;
|
|
259
262
|
}
|
|
260
263
|
|
|
264
|
+
// ─── Status helpers ─────────────────────────────
|
|
265
|
+
// Non-ok entries stay visible in the picker so the user sees that a
|
|
266
|
+
// workflow from an older SDK release (or one with a load error) still
|
|
267
|
+
// exists on disk — the previous behaviour silently dropped them, which
|
|
268
|
+
// made user/global workflows appear to vanish after an atomic upgrade.
|
|
269
|
+
|
|
270
|
+
/** Unicode glyph that prefixes a non-ok entry and heads its preview. */
|
|
271
|
+
const STATUS_ICON: Record<WorkflowMetadataStatus["kind"], string> = {
|
|
272
|
+
ok: " ",
|
|
273
|
+
incompatible: "⚠",
|
|
274
|
+
error: "✗",
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** Compact single-word label used in list rows and the bottom hint. */
|
|
278
|
+
const STATUS_LABEL: Record<WorkflowMetadataStatus["kind"], string> = {
|
|
279
|
+
ok: "",
|
|
280
|
+
incompatible: "update",
|
|
281
|
+
error: "broken",
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** Map each status kind to its semantic palette slot. */
|
|
285
|
+
const STATUS_COLOR: Record<WorkflowMetadataStatus["kind"], keyof PickerTheme> = {
|
|
286
|
+
ok: "success",
|
|
287
|
+
incompatible: "warning",
|
|
288
|
+
error: "error",
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/** Non-ok rows are inert — Enter / run commands must not transition the picker. */
|
|
292
|
+
function isRunnable(wf: WorkflowWithMetadata): boolean {
|
|
293
|
+
return wf.status.kind === "ok";
|
|
294
|
+
}
|
|
295
|
+
|
|
261
296
|
// ─── Validation ─────────────────────────────────
|
|
262
297
|
|
|
263
298
|
export function isFieldValid(field: WorkflowInput, value: string): boolean {
|
|
@@ -385,6 +420,24 @@ const WorkflowList = memo(function WorkflowList({
|
|
|
385
420
|
const entryIdx = entryIndexByRow.get(i) ?? -1;
|
|
386
421
|
const isFocused = entryIdx === focusedEntryIdx;
|
|
387
422
|
const wf = row.entry.workflow;
|
|
423
|
+
const statusKind = wf.status.kind;
|
|
424
|
+
const runnable = statusKind === "ok";
|
|
425
|
+
// Status indicator sits between the focus marker and the name so
|
|
426
|
+
// every row shares a 4-character gutter — ok rows render a blank
|
|
427
|
+
// gutter, so the list stays visually flush while non-ok rows
|
|
428
|
+
// always occupy a fixed slot (no layout jitter when filtering).
|
|
429
|
+
const statusIcon = STATUS_ICON[statusKind];
|
|
430
|
+
const statusCol = theme[STATUS_COLOR[statusKind]];
|
|
431
|
+
// Non-ok rows fade the name so the "diagnostic, not selectable"
|
|
432
|
+
// read is immediate even when the user hasn't reached the row
|
|
433
|
+
// yet — the eye catches the warning glyph + dim text together.
|
|
434
|
+
const nameCol = runnable
|
|
435
|
+
? isFocused
|
|
436
|
+
? theme.text
|
|
437
|
+
: theme.textMuted
|
|
438
|
+
: isFocused
|
|
439
|
+
? theme.textMuted
|
|
440
|
+
: theme.textDim;
|
|
388
441
|
|
|
389
442
|
return (
|
|
390
443
|
<box
|
|
@@ -399,7 +452,10 @@ const WorkflowList = memo(function WorkflowList({
|
|
|
399
452
|
<span fg={isFocused ? theme.primary : theme.textDim}>
|
|
400
453
|
{isFocused ? "▸ " : " "}
|
|
401
454
|
</span>
|
|
402
|
-
<span fg={
|
|
455
|
+
<span fg={statusCol}>
|
|
456
|
+
{statusIcon + " "}
|
|
457
|
+
</span>
|
|
458
|
+
<span fg={nameCol}>
|
|
403
459
|
{wf.name}
|
|
404
460
|
</span>
|
|
405
461
|
</text>
|
|
@@ -459,6 +515,59 @@ const ArgumentRow = memo(function ArgumentRow({
|
|
|
459
515
|
);
|
|
460
516
|
});
|
|
461
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Diagnostic block for non-ok workflows. Replaces the description +
|
|
520
|
+
* arguments sections in the preview pane so the user sees *why* a row
|
|
521
|
+
* is inert and what to do about it, instead of an empty-looking panel.
|
|
522
|
+
*/
|
|
523
|
+
const StatusDiagnostic = memo(function StatusDiagnostic({
|
|
524
|
+
status,
|
|
525
|
+
}: {
|
|
526
|
+
status: Exclude<WorkflowMetadataStatus, { kind: "ok" }>;
|
|
527
|
+
}) {
|
|
528
|
+
const theme = usePickerTheme();
|
|
529
|
+
const color = theme[STATUS_COLOR[status.kind]];
|
|
530
|
+
const icon = STATUS_ICON[status.kind];
|
|
531
|
+
|
|
532
|
+
// Headline + sub-copy split: the headline is terse (three words) so
|
|
533
|
+
// it reads at a glance even on a narrow right pane; the sub-copy
|
|
534
|
+
// explains *why* and the third paragraph tells the user what to do.
|
|
535
|
+
const headline =
|
|
536
|
+
status.kind === "incompatible"
|
|
537
|
+
? "update required"
|
|
538
|
+
: "failed to load";
|
|
539
|
+
const detail =
|
|
540
|
+
status.kind === "incompatible"
|
|
541
|
+
? `Needs Atomic v${status.requiredVersion}. Installed: v${status.currentVersion}.`
|
|
542
|
+
: status.message;
|
|
543
|
+
const remediation =
|
|
544
|
+
status.kind === "incompatible"
|
|
545
|
+
? "Update Atomic, or re-save the workflow against the current SDK."
|
|
546
|
+
: "Open the workflow file and fix the error above.";
|
|
547
|
+
|
|
548
|
+
return (
|
|
549
|
+
<box flexDirection="column">
|
|
550
|
+
<text>
|
|
551
|
+
<span fg={color}>
|
|
552
|
+
<strong>{icon + " " + headline}</strong>
|
|
553
|
+
</span>
|
|
554
|
+
</text>
|
|
555
|
+
|
|
556
|
+
<box height={1} />
|
|
557
|
+
|
|
558
|
+
<text>
|
|
559
|
+
<span fg={theme.textMuted}>{detail}</span>
|
|
560
|
+
</text>
|
|
561
|
+
|
|
562
|
+
<box height={1} />
|
|
563
|
+
|
|
564
|
+
<text>
|
|
565
|
+
<span fg={theme.textDim}>{remediation}</span>
|
|
566
|
+
</text>
|
|
567
|
+
</box>
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
|
|
462
571
|
const Preview = memo(function Preview({
|
|
463
572
|
wf,
|
|
464
573
|
}: {
|
|
@@ -466,6 +575,7 @@ const Preview = memo(function Preview({
|
|
|
466
575
|
}) {
|
|
467
576
|
const theme = usePickerTheme();
|
|
468
577
|
const args = wf.inputs;
|
|
578
|
+
const status = wf.status;
|
|
469
579
|
|
|
470
580
|
return (
|
|
471
581
|
<box
|
|
@@ -493,21 +603,27 @@ const Preview = memo(function Preview({
|
|
|
493
603
|
|
|
494
604
|
<box height={2} />
|
|
495
605
|
|
|
496
|
-
|
|
497
|
-
<span fg={theme.textMuted}>
|
|
498
|
-
{wf.description || "(no description)"}
|
|
499
|
-
</span>
|
|
500
|
-
</text>
|
|
501
|
-
|
|
502
|
-
{args.length > 0 && (
|
|
606
|
+
{status.kind === "ok" ? (
|
|
503
607
|
<>
|
|
504
|
-
<
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
608
|
+
<text>
|
|
609
|
+
<span fg={theme.textMuted}>
|
|
610
|
+
{wf.description || "(no description)"}
|
|
611
|
+
</span>
|
|
612
|
+
</text>
|
|
613
|
+
|
|
614
|
+
{args.length > 0 && (
|
|
615
|
+
<>
|
|
616
|
+
<box height={2} />
|
|
617
|
+
<SectionLabel label="ARGUMENTS" />
|
|
618
|
+
<box height={1} />
|
|
619
|
+
{args.map((f) => (
|
|
620
|
+
<ArgumentRow key={f.name} field={f} />
|
|
621
|
+
))}
|
|
622
|
+
</>
|
|
623
|
+
)}
|
|
510
624
|
</>
|
|
625
|
+
) : (
|
|
626
|
+
<StatusDiagnostic status={status} />
|
|
511
627
|
)}
|
|
512
628
|
</box>
|
|
513
629
|
);
|
|
@@ -1022,6 +1138,14 @@ const PICK_HINTS: Hint[] = [
|
|
|
1022
1138
|
{ key: "↵", label: "select" },
|
|
1023
1139
|
{ key: "esc", label: "quit" },
|
|
1024
1140
|
];
|
|
1141
|
+
// Shown instead of PICK_HINTS when the focused row is non-ok — the dim
|
|
1142
|
+
// `↵ unavailable` reads immediately as "this row is navigable but not
|
|
1143
|
+
// runnable", which matches the muted row colour and preview diagnostic.
|
|
1144
|
+
const PICK_HINTS_UNAVAILABLE: Hint[] = [
|
|
1145
|
+
{ key: "↑↓", label: "navigate" },
|
|
1146
|
+
{ key: "↵", label: "unavailable", dim: true },
|
|
1147
|
+
{ key: "esc", label: "quit" },
|
|
1148
|
+
];
|
|
1025
1149
|
const CONFIRM_HINTS: Hint[] = [
|
|
1026
1150
|
{ key: "y", label: "submit" },
|
|
1027
1151
|
{ key: "n", label: "cancel" },
|
|
@@ -1134,7 +1258,26 @@ const Statusline = memo(function Statusline({
|
|
|
1134
1258
|
{focusedWf ? (
|
|
1135
1259
|
<box paddingLeft={1} paddingRight={1} alignItems="center">
|
|
1136
1260
|
<text>
|
|
1137
|
-
|
|
1261
|
+
{focusedWf.status.kind !== "ok" ? (
|
|
1262
|
+
<span fg={theme[STATUS_COLOR[focusedWf.status.kind]]}>
|
|
1263
|
+
{STATUS_ICON[focusedWf.status.kind] + " "}
|
|
1264
|
+
</span>
|
|
1265
|
+
) : null}
|
|
1266
|
+
<span
|
|
1267
|
+
fg={
|
|
1268
|
+
focusedWf.status.kind === "ok" ? theme.text : theme.textMuted
|
|
1269
|
+
}
|
|
1270
|
+
>
|
|
1271
|
+
{focusedWf.name}
|
|
1272
|
+
</span>
|
|
1273
|
+
{focusedWf.status.kind !== "ok" ? (
|
|
1274
|
+
<>
|
|
1275
|
+
<span fg={theme.textDim}>{" · "}</span>
|
|
1276
|
+
<span fg={theme[STATUS_COLOR[focusedWf.status.kind]]}>
|
|
1277
|
+
{STATUS_LABEL[focusedWf.status.kind]}
|
|
1278
|
+
</span>
|
|
1279
|
+
</>
|
|
1280
|
+
) : null}
|
|
1138
1281
|
</text>
|
|
1139
1282
|
</box>
|
|
1140
1283
|
) : null}
|
|
@@ -1247,7 +1390,10 @@ function usePickerKeyboard(state: PickerKeyboardState): void {
|
|
|
1247
1390
|
if (key.name === "return") {
|
|
1248
1391
|
key.stopPropagation();
|
|
1249
1392
|
const wf = focusedWfRef.current;
|
|
1250
|
-
|
|
1393
|
+
// Silently swallow Enter on incompatible / broken entries — the
|
|
1394
|
+
// preview pane already explains the failure; advancing into the
|
|
1395
|
+
// prompt phase would be misleading since the workflow can't run.
|
|
1396
|
+
if (wf && isRunnable(wf)) {
|
|
1251
1397
|
const initial: Record<string, string> = {};
|
|
1252
1398
|
for (const f of wf.inputs) {
|
|
1253
1399
|
initial[f.name] =
|
|
@@ -1408,10 +1554,13 @@ export function WorkflowPicker({
|
|
|
1408
1554
|
setConfirmOpen,
|
|
1409
1555
|
});
|
|
1410
1556
|
|
|
1557
|
+
const focusedIsRunnable = focusedWf ? isRunnable(focusedWf) : true;
|
|
1411
1558
|
const hints = confirmOpen
|
|
1412
1559
|
? CONFIRM_HINTS
|
|
1413
1560
|
: phase === "pick"
|
|
1414
|
-
?
|
|
1561
|
+
? focusedIsRunnable
|
|
1562
|
+
? PICK_HINTS
|
|
1563
|
+
: PICK_HINTS_UNAVAILABLE
|
|
1415
1564
|
: isFormValid
|
|
1416
1565
|
? PROMPT_HINTS_VALID
|
|
1417
1566
|
: PROMPT_HINTS_INVALID;
|
|
@@ -152,6 +152,7 @@ export class WorkflowBuilder<A extends AgentType = AgentType, N extends string =
|
|
|
152
152
|
name: this.options.name,
|
|
153
153
|
description: this.options.description ?? "",
|
|
154
154
|
inputs,
|
|
155
|
+
minSDKVersion: this.options.minSDKVersion ?? null,
|
|
155
156
|
run: runFn,
|
|
156
157
|
};
|
|
157
158
|
}
|
package/src/sdk/errors.ts
CHANGED
|
@@ -38,6 +38,26 @@ export class InvalidWorkflowError extends Error {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Thrown when a workflow declares a `minSDKVersion` newer than the
|
|
43
|
+
* bundled CLI. Carries both versions so the CLI can render an
|
|
44
|
+
* actionable "update atomic or re-save the workflow" hint rather than
|
|
45
|
+
* a generic load error.
|
|
46
|
+
*/
|
|
47
|
+
export class IncompatibleSDKError extends Error {
|
|
48
|
+
constructor(
|
|
49
|
+
public readonly path: string,
|
|
50
|
+
public readonly requiredVersion: string,
|
|
51
|
+
public readonly currentVersion: string,
|
|
52
|
+
) {
|
|
53
|
+
super(
|
|
54
|
+
`${path} requires Atomic SDK v${requiredVersion}, but v${currentVersion} is installed.\n` +
|
|
55
|
+
` Update Atomic, or re-save the workflow against the current SDK.`,
|
|
56
|
+
);
|
|
57
|
+
this.name = "IncompatibleSDKError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
/** Extract a human-readable message from an unknown thrown value. */
|
|
42
62
|
export function errorMessage(error: unknown): string {
|
|
43
63
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -17,6 +17,7 @@ import { homedir } from "node:os";
|
|
|
17
17
|
import ignore from "ignore";
|
|
18
18
|
import type { AgentType, WorkflowInput } from "../types.ts";
|
|
19
19
|
import { WorkflowLoader } from "./loader.ts";
|
|
20
|
+
import { IncompatibleSDKError } from "../errors.ts";
|
|
20
21
|
|
|
21
22
|
export interface DiscoveredWorkflow {
|
|
22
23
|
name: string;
|
|
@@ -248,47 +249,120 @@ export async function findWorkflow(
|
|
|
248
249
|
return all.find((w) => w.name === name) ?? null;
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Load status for a {@link WorkflowWithMetadata} entry.
|
|
254
|
+
*
|
|
255
|
+
* - `ok` — the workflow compiled cleanly and is ready to run.
|
|
256
|
+
* - `incompatible` — the workflow declared a `minSDKVersion` newer
|
|
257
|
+
* than the bundled CLI. The CLI renders it with an
|
|
258
|
+
* "update required" badge so users see the mismatch
|
|
259
|
+
* rather than a silent disappearance.
|
|
260
|
+
* - `error` — any other load failure (syntax error, missing
|
|
261
|
+
* `.compile()`, invalid default export, etc.).
|
|
262
|
+
* Rendered with a "failed to load" badge plus the
|
|
263
|
+
* underlying message.
|
|
264
|
+
*/
|
|
265
|
+
export type WorkflowMetadataStatus =
|
|
266
|
+
| { kind: "ok" }
|
|
267
|
+
| {
|
|
268
|
+
kind: "incompatible";
|
|
269
|
+
requiredVersion: string;
|
|
270
|
+
currentVersion: string;
|
|
271
|
+
message: string;
|
|
272
|
+
}
|
|
273
|
+
| {
|
|
274
|
+
kind: "error";
|
|
275
|
+
stage: "resolve" | "validate" | "load";
|
|
276
|
+
message: string;
|
|
277
|
+
};
|
|
278
|
+
|
|
251
279
|
/**
|
|
252
280
|
* A discovered workflow enriched with the metadata the picker needs to
|
|
253
|
-
* render it: the human description
|
|
281
|
+
* render it: the human description, the declared input schema, and the
|
|
282
|
+
* load status.
|
|
254
283
|
*
|
|
255
284
|
* Populated by {@link loadWorkflowsMetadata}, which runs each discovered
|
|
256
|
-
* workflow through {@link WorkflowLoader.loadWorkflow} and extracts
|
|
257
|
-
*
|
|
258
|
-
*
|
|
285
|
+
* workflow through {@link WorkflowLoader.loadWorkflow} and extracts the
|
|
286
|
+
* display-relevant fields — the full compiled definition is discarded
|
|
287
|
+
* after extraction so re-imports during execution are cheap.
|
|
288
|
+
*
|
|
289
|
+
* Broken entries still materialise with empty `description` / `inputs`
|
|
290
|
+
* and a non-`ok` {@link status}, so the picker can render them as
|
|
291
|
+
* visible "update required" / "failed to load" rows instead of
|
|
292
|
+
* silently omitting them (the previous behaviour, which made user and
|
|
293
|
+
* global workflows vanish whenever the base SDK shape drifted between
|
|
294
|
+
* releases).
|
|
259
295
|
*/
|
|
260
296
|
export interface WorkflowWithMetadata extends DiscoveredWorkflow {
|
|
261
|
-
/** Workflow description, empty string when none was declared. */
|
|
297
|
+
/** Workflow description, empty string when none was declared or the workflow failed to load. */
|
|
262
298
|
description: string;
|
|
263
|
-
/** Picker-ready input schema; free-form
|
|
299
|
+
/** Picker-ready input schema; empty for free-form or failed-to-load workflows. */
|
|
264
300
|
inputs: readonly WorkflowInput[];
|
|
301
|
+
/** Load outcome — non-`ok` entries are rendered as visible diagnostics in the picker/list. */
|
|
302
|
+
status: WorkflowMetadataStatus;
|
|
265
303
|
}
|
|
266
304
|
|
|
267
305
|
/**
|
|
268
|
-
* Load metadata (description + picker-ready inputs) for a batch
|
|
306
|
+
* Load metadata (description + picker-ready inputs + status) for a batch
|
|
307
|
+
* of discovered workflows.
|
|
269
308
|
*
|
|
270
|
-
*
|
|
271
|
-
* entry
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
309
|
+
* **Failed workflows are kept in the returned list**, not dropped. Each
|
|
310
|
+
* broken entry carries a {@link WorkflowMetadataStatus} explaining the
|
|
311
|
+
* failure so the picker and `atomic workflow -l` can surface it as an
|
|
312
|
+
* actionable diagnostic. This is the only way end users discover that a
|
|
313
|
+
* workflow from an older SDK release has gone incompatible after an
|
|
314
|
+
* `atomic` upgrade — silent filtering would leave them with a missing
|
|
315
|
+
* entry and no breadcrumb.
|
|
316
|
+
*
|
|
317
|
+
* Callers that want to execute a workflow should still route through
|
|
318
|
+
* {@link WorkflowLoader.loadWorkflow} — this function throws away the
|
|
319
|
+
* compiled definition so re-running the loader on a confirmed pick is
|
|
320
|
+
* unavoidable.
|
|
275
321
|
*/
|
|
276
322
|
export async function loadWorkflowsMetadata(
|
|
277
323
|
discovered: DiscoveredWorkflow[],
|
|
278
324
|
): Promise<WorkflowWithMetadata[]> {
|
|
279
|
-
|
|
280
|
-
discovered.map(async (wf): Promise<WorkflowWithMetadata
|
|
325
|
+
return Promise.all(
|
|
326
|
+
discovered.map(async (wf): Promise<WorkflowWithMetadata> => {
|
|
281
327
|
const loaded = await WorkflowLoader.loadWorkflow(wf);
|
|
282
|
-
if (
|
|
328
|
+
if (loaded.ok) {
|
|
329
|
+
return {
|
|
330
|
+
...wf,
|
|
331
|
+
description: loaded.value.definition.description,
|
|
332
|
+
inputs: loaded.value.definition.inputs,
|
|
333
|
+
status: { kind: "ok" },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Incompatible SDK version is a first-class status so the UI can
|
|
338
|
+
// show a dedicated "update required" hint. Every other failure
|
|
339
|
+
// maps to a generic `error` variant — the picker renders the
|
|
340
|
+
// message but doesn't try to interpret it further.
|
|
341
|
+
if (loaded.error instanceof IncompatibleSDKError) {
|
|
342
|
+
return {
|
|
343
|
+
...wf,
|
|
344
|
+
description: "",
|
|
345
|
+
inputs: [],
|
|
346
|
+
status: {
|
|
347
|
+
kind: "incompatible",
|
|
348
|
+
requiredVersion: loaded.error.requiredVersion,
|
|
349
|
+
currentVersion: loaded.error.currentVersion,
|
|
350
|
+
message: loaded.message,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
283
355
|
return {
|
|
284
356
|
...wf,
|
|
285
|
-
description:
|
|
286
|
-
inputs:
|
|
357
|
+
description: "",
|
|
358
|
+
inputs: [],
|
|
359
|
+
status: {
|
|
360
|
+
kind: "error",
|
|
361
|
+
stage: loaded.stage,
|
|
362
|
+
message: loaded.message,
|
|
363
|
+
},
|
|
287
364
|
};
|
|
288
365
|
}),
|
|
289
366
|
);
|
|
290
|
-
return results.filter(
|
|
291
|
-
(r): r is WorkflowWithMetadata => r !== null,
|
|
292
|
-
);
|
|
293
367
|
}
|
|
294
368
|
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from "../providers/claude.ts";
|
|
56
56
|
import { OrchestratorPanel } from "./panel.tsx";
|
|
57
57
|
import { GraphFrontierTracker } from "./graph-inference.ts";
|
|
58
|
+
import { buildSnapshot, writeSnapshot } from "./status-writer.ts";
|
|
58
59
|
import { errorMessage } from "../errors.ts";
|
|
59
60
|
import { createPainter } from "../../theme/colors.ts";
|
|
60
61
|
|
|
@@ -1533,11 +1534,47 @@ export async function runOrchestrator(): Promise<void> {
|
|
|
1533
1534
|
tmuxSession: tmuxSessionName,
|
|
1534
1535
|
});
|
|
1535
1536
|
|
|
1537
|
+
// Mirror panel-store mutations to <sessionDir>/status.json so
|
|
1538
|
+
// out-of-process consumers (e.g. `atomic workflow status`) can read
|
|
1539
|
+
// the live workflow state without IPC into the orchestrator.
|
|
1540
|
+
// Writes are debounced via a "pending" flag so a burst of mutations
|
|
1541
|
+
// collapses into a single file write.
|
|
1542
|
+
let snapshotPending = false;
|
|
1543
|
+
const persistSnapshot = (): void => {
|
|
1544
|
+
if (snapshotPending) return;
|
|
1545
|
+
snapshotPending = true;
|
|
1546
|
+
queueMicrotask(() => {
|
|
1547
|
+
snapshotPending = false;
|
|
1548
|
+
const snap = panel.getSnapshot();
|
|
1549
|
+
void writeSnapshot(
|
|
1550
|
+
sessionsBaseDir,
|
|
1551
|
+
buildSnapshot({
|
|
1552
|
+
workflowRunId,
|
|
1553
|
+
tmuxSession: tmuxSessionName,
|
|
1554
|
+
...snap,
|
|
1555
|
+
}),
|
|
1556
|
+
);
|
|
1557
|
+
});
|
|
1558
|
+
};
|
|
1559
|
+
const unsubscribePanel = panel.subscribe(persistSnapshot);
|
|
1560
|
+
// Seed an initial snapshot so the file exists before any session starts.
|
|
1561
|
+
persistSnapshot();
|
|
1562
|
+
|
|
1536
1563
|
// Idempotent shutdown guard
|
|
1537
1564
|
let shutdownCalled = false;
|
|
1538
1565
|
const shutdown = (exitCode = 0) => {
|
|
1539
1566
|
if (shutdownCalled) return;
|
|
1540
1567
|
shutdownCalled = true;
|
|
1568
|
+
unsubscribePanel();
|
|
1569
|
+
// Final snapshot reflecting terminal state (completed/error/aborted).
|
|
1570
|
+
void writeSnapshot(
|
|
1571
|
+
sessionsBaseDir,
|
|
1572
|
+
buildSnapshot({
|
|
1573
|
+
workflowRunId,
|
|
1574
|
+
tmuxSession: tmuxSessionName,
|
|
1575
|
+
...panel.getSnapshot(),
|
|
1576
|
+
}),
|
|
1577
|
+
);
|
|
1541
1578
|
panel.destroy();
|
|
1542
1579
|
try {
|
|
1543
1580
|
tmux.killSession(tmuxSessionName);
|
|
@@ -12,10 +12,17 @@
|
|
|
12
12
|
|
|
13
13
|
import type { WorkflowDefinition, AgentType } from "../types.ts";
|
|
14
14
|
import type { DiscoveredWorkflow } from "./discovery.ts";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
errorMessage,
|
|
17
|
+
WorkflowNotCompiledError,
|
|
18
|
+
InvalidWorkflowError,
|
|
19
|
+
IncompatibleSDKError,
|
|
20
|
+
} from "../errors.ts";
|
|
16
21
|
import { validateCopilotWorkflow } from "../providers/copilot.ts";
|
|
17
22
|
import { validateOpenCodeWorkflow } from "../providers/opencode.ts";
|
|
18
23
|
import { validateClaudeWorkflow } from "../providers/claude.ts";
|
|
24
|
+
import { satisfiesMinVersion } from "./version-compat.ts";
|
|
25
|
+
import { VERSION } from "../../version.ts";
|
|
19
26
|
|
|
20
27
|
export namespace WorkflowLoader {
|
|
21
28
|
// ---------------------------------------------------------------------------
|
|
@@ -180,9 +187,29 @@ export namespace WorkflowLoader {
|
|
|
180
187
|
};
|
|
181
188
|
}
|
|
182
189
|
|
|
190
|
+
const def = definition as WorkflowDefinition;
|
|
191
|
+
|
|
192
|
+
// Refuse workflows whose declared minSDKVersion is newer than the
|
|
193
|
+
// bundled CLI — the workflow author opted in to a version gate
|
|
194
|
+
// exactly so the loader could surface a clear upgrade hint
|
|
195
|
+
// instead of letting a shape-drift error bubble up at run time.
|
|
196
|
+
if (!satisfiesMinVersion(VERSION, def.minSDKVersion)) {
|
|
197
|
+
const err = new IncompatibleSDKError(
|
|
198
|
+
validated.path,
|
|
199
|
+
def.minSDKVersion ?? "",
|
|
200
|
+
VERSION,
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
stage: "load",
|
|
205
|
+
error: err,
|
|
206
|
+
message: err.message,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
183
210
|
return {
|
|
184
211
|
ok: true,
|
|
185
|
-
value: { ...validated, definition:
|
|
212
|
+
value: { ...validated, definition: def },
|
|
186
213
|
};
|
|
187
214
|
} catch (error) {
|
|
188
215
|
return {
|