@bastani/atomic 0.5.20-0 → 0.5.21-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/workflow-creator/SKILL.md +56 -8
- package/dist/sdk/components/orchestrator-panel.d.ts +23 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.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/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-inputs.test.ts +321 -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/sdk/components/orchestrator-panel.tsx +36 -1
- package/src/sdk/runtime/executor.ts +37 -0
- package/src/sdk/runtime/status-writer.test.ts +245 -0
- package/src/sdk/runtime/status-writer.ts +201 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow status snapshot — bridges the in-process panel state with
|
|
3
|
+
* out-of-process consumers (e.g. `atomic workflow status`).
|
|
4
|
+
*
|
|
5
|
+
* The orchestrator subscribes to its `PanelStore` and writes a fresh
|
|
6
|
+
* snapshot to `~/.atomic/sessions/<workflowRunId>/status.json` every
|
|
7
|
+
* time the store mutates. Consumers read that file to derive the
|
|
8
|
+
* overall workflow state without needing IPC into the orchestrator.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { SessionData, SessionStatus } from "../components/orchestrator-panel-types.ts";
|
|
13
|
+
|
|
14
|
+
/** File name used for the status snapshot inside each workflow's session directory. */
|
|
15
|
+
export const STATUS_FILE_NAME = "status.json";
|
|
16
|
+
|
|
17
|
+
/** High-level workflow state surfaced to the agent / CLI consumer. */
|
|
18
|
+
export type WorkflowOverallStatus =
|
|
19
|
+
| "in_progress"
|
|
20
|
+
| "error"
|
|
21
|
+
| "completed"
|
|
22
|
+
| "needs_review";
|
|
23
|
+
|
|
24
|
+
/** Per-session entry mirrored from the orchestrator's panel store. */
|
|
25
|
+
export interface WorkflowStatusSession {
|
|
26
|
+
name: string;
|
|
27
|
+
status: SessionStatus;
|
|
28
|
+
parents: string[];
|
|
29
|
+
error?: string;
|
|
30
|
+
startedAt: number | null;
|
|
31
|
+
endedAt: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Snapshot persisted to disk for `atomic workflow status` to read.
|
|
36
|
+
* Schema is versioned so future readers can stay backwards-compatible.
|
|
37
|
+
*/
|
|
38
|
+
export interface WorkflowStatusSnapshot {
|
|
39
|
+
schemaVersion: 1;
|
|
40
|
+
workflowRunId: string;
|
|
41
|
+
tmuxSession: string;
|
|
42
|
+
workflowName: string;
|
|
43
|
+
agent: string;
|
|
44
|
+
prompt: string;
|
|
45
|
+
/** Overall state derived from per-session status + completion flags. */
|
|
46
|
+
overall: WorkflowOverallStatus;
|
|
47
|
+
/** True when the orchestrator has shown its completion banner. */
|
|
48
|
+
completionReached: boolean;
|
|
49
|
+
/** Fatal-error message set via `panel.showFatalError`, if any. */
|
|
50
|
+
fatalError: string | null;
|
|
51
|
+
/** Wall-clock time of the snapshot in ISO-8601 format. */
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
sessions: WorkflowStatusSession[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Inputs the writer needs to render a snapshot — a strict subset of
|
|
58
|
+
* `PanelStore` so the writer doesn't depend on the renderer module.
|
|
59
|
+
*/
|
|
60
|
+
export interface StatusWriterInputs {
|
|
61
|
+
workflowRunId: string;
|
|
62
|
+
tmuxSession: string;
|
|
63
|
+
workflowName: string;
|
|
64
|
+
agent: string;
|
|
65
|
+
prompt: string;
|
|
66
|
+
fatalError: string | null;
|
|
67
|
+
completionReached: boolean;
|
|
68
|
+
sessions: readonly SessionData[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Derive the overall workflow state from per-session statuses + the
|
|
73
|
+
* orchestrator-level completion / fatal-error flags.
|
|
74
|
+
*
|
|
75
|
+
* Precedence (highest first):
|
|
76
|
+
* 1. `error` — fatal error or any session ended in error
|
|
77
|
+
* 2. `needs_review` — at least one session is awaiting human input (HIL)
|
|
78
|
+
* 3. `completed` — completion banner reached and nothing errored
|
|
79
|
+
* 4. `in_progress` — default
|
|
80
|
+
*
|
|
81
|
+
* `needs_review` outranks `completed` so an agent that pauses for HIL
|
|
82
|
+
* right at the end is never reported as done while still waiting.
|
|
83
|
+
*/
|
|
84
|
+
export function deriveOverallStatus(input: {
|
|
85
|
+
sessions: readonly SessionData[];
|
|
86
|
+
completionReached: boolean;
|
|
87
|
+
fatalError: string | null;
|
|
88
|
+
}): WorkflowOverallStatus {
|
|
89
|
+
if (input.fatalError !== null) return "error";
|
|
90
|
+
if (input.sessions.some((s) => s.status === "error")) return "error";
|
|
91
|
+
if (input.sessions.some((s) => s.status === "awaiting_input")) {
|
|
92
|
+
return "needs_review";
|
|
93
|
+
}
|
|
94
|
+
if (input.completionReached) return "completed";
|
|
95
|
+
return "in_progress";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build a snapshot from the writer inputs (pure — exported for tests). */
|
|
99
|
+
export function buildSnapshot(
|
|
100
|
+
input: StatusWriterInputs,
|
|
101
|
+
now: () => Date = () => new Date(),
|
|
102
|
+
): WorkflowStatusSnapshot {
|
|
103
|
+
return {
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
workflowRunId: input.workflowRunId,
|
|
106
|
+
tmuxSession: input.tmuxSession,
|
|
107
|
+
workflowName: input.workflowName,
|
|
108
|
+
agent: input.agent,
|
|
109
|
+
prompt: input.prompt,
|
|
110
|
+
overall: deriveOverallStatus({
|
|
111
|
+
sessions: input.sessions,
|
|
112
|
+
completionReached: input.completionReached,
|
|
113
|
+
fatalError: input.fatalError,
|
|
114
|
+
}),
|
|
115
|
+
completionReached: input.completionReached,
|
|
116
|
+
fatalError: input.fatalError,
|
|
117
|
+
updatedAt: now().toISOString(),
|
|
118
|
+
sessions: input.sessions.map((s) => ({
|
|
119
|
+
name: s.name,
|
|
120
|
+
status: s.status,
|
|
121
|
+
parents: [...s.parents],
|
|
122
|
+
...(s.error !== undefined ? { error: s.error } : {}),
|
|
123
|
+
startedAt: s.startedAt,
|
|
124
|
+
endedAt: s.endedAt,
|
|
125
|
+
})),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Absolute path of the status file for a given workflow run directory. */
|
|
130
|
+
export function statusFilePath(sessionDir: string): string {
|
|
131
|
+
return join(sessionDir, STATUS_FILE_NAME);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write a snapshot to `<sessionDir>/status.json`. Uses an atomic
|
|
136
|
+
* write-then-rename so concurrent readers never see partial JSON.
|
|
137
|
+
* Errors are swallowed — the orchestrator must keep running even if
|
|
138
|
+
* the status file can't be persisted.
|
|
139
|
+
*/
|
|
140
|
+
export async function writeSnapshot(
|
|
141
|
+
sessionDir: string,
|
|
142
|
+
snapshot: WorkflowStatusSnapshot,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
const finalPath = statusFilePath(sessionDir);
|
|
145
|
+
const tmpPath = `${finalPath}.tmp-${process.pid}`;
|
|
146
|
+
try {
|
|
147
|
+
await Bun.write(tmpPath, JSON.stringify(snapshot, null, 2));
|
|
148
|
+
const { rename } = await import("node:fs/promises");
|
|
149
|
+
await rename(tmpPath, finalPath);
|
|
150
|
+
} catch {
|
|
151
|
+
// Best-effort — never fail the workflow because of a status write.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Read a snapshot from disk. Returns `null` when the file doesn't
|
|
157
|
+
* exist or fails to parse — callers fall back to deriving status from
|
|
158
|
+
* the live tmux session list.
|
|
159
|
+
*/
|
|
160
|
+
export async function readSnapshot(
|
|
161
|
+
sessionDir: string,
|
|
162
|
+
): Promise<WorkflowStatusSnapshot | null> {
|
|
163
|
+
try {
|
|
164
|
+
const file = Bun.file(statusFilePath(sessionDir));
|
|
165
|
+
if (!(await file.exists())) return null;
|
|
166
|
+
const parsed: unknown = JSON.parse(await file.text());
|
|
167
|
+
if (!isSnapshot(parsed)) return null;
|
|
168
|
+
return parsed;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Runtime guard for deserialised snapshots — keeps the reader type-safe. */
|
|
175
|
+
function isSnapshot(value: unknown): value is WorkflowStatusSnapshot {
|
|
176
|
+
if (!value || typeof value !== "object") return false;
|
|
177
|
+
const v = value as Record<string, unknown>;
|
|
178
|
+
return (
|
|
179
|
+
v.schemaVersion === 1 &&
|
|
180
|
+
typeof v.workflowRunId === "string" &&
|
|
181
|
+
typeof v.tmuxSession === "string" &&
|
|
182
|
+
typeof v.overall === "string" &&
|
|
183
|
+
Array.isArray(v.sessions)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extract the `workflowRunId` (the trailing 8-hex segment) from a
|
|
189
|
+
* tmux session name shaped `atomic-wf-<agent>-<name>-<id>`. Returns
|
|
190
|
+
* `null` for non-workflow sessions or names that don't end in a
|
|
191
|
+
* UUID-style suffix.
|
|
192
|
+
*/
|
|
193
|
+
export function workflowRunIdFromTmuxName(name: string): string | null {
|
|
194
|
+
if (!name.startsWith("atomic-wf-")) return null;
|
|
195
|
+
const lastDash = name.lastIndexOf("-");
|
|
196
|
+
if (lastDash < 0) return null;
|
|
197
|
+
const candidate = name.slice(lastDash + 1);
|
|
198
|
+
// generateId() produces an 8-char hex slice from crypto.randomUUID.
|
|
199
|
+
if (!/^[0-9a-f]{8}$/i.test(candidate)) return null;
|
|
200
|
+
return candidate;
|
|
201
|
+
}
|