@h-rig/run-worker 0.0.6-alpha.154 → 0.0.6-alpha.156

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.
@@ -0,0 +1,356 @@
1
+ // @bun
2
+ // packages/run-worker/src/runs/projection.ts
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { isAbsolute, relative, resolve } from "path";
5
+ import { Duration, Effect, Option, Stream } from "effect";
6
+ import {
7
+ foldRunSessionEntries,
8
+ isTerminalRunStatus,
9
+ timelineEntriesFromCustomEntries
10
+ } from "@rig/contracts";
11
+ import { registrySnapshotStream } from "@rig/runtime/control-plane/discovery";
12
+
13
+ // packages/run-worker/src/runs/diagnostics.ts
14
+ function normalizeString(value) {
15
+ if (typeof value !== "string")
16
+ return null;
17
+ const normalized = value.replace(/\s+/g, " ").trim();
18
+ return normalized.length > 0 ? normalized : null;
19
+ }
20
+ function isGenericRunFailure(value) {
21
+ const text = normalizeString(value);
22
+ return Boolean(text && /^Task run failed \([^)]*\)$/i.test(text));
23
+ }
24
+ function appendCandidate(candidates, value) {
25
+ const text = normalizeString(value);
26
+ if (text && !candidates.includes(text))
27
+ candidates.push(text);
28
+ }
29
+ function categorizeUsefulRunError(lines) {
30
+ const taskSourceFailure = lines.find((line) => /failed to update task source/i.test(line));
31
+ if (taskSourceFailure)
32
+ return `Task source update failed: ${taskSourceFailure}`;
33
+ const moduleFailure = lines.find((line) => /cannot find module/i.test(line));
34
+ if (moduleFailure)
35
+ return `Runtime module resolution failed: ${moduleFailure}`;
36
+ const providerFailure = lines.find((line) => /no api key found|unauthorized|authentication failed|invalid api key/i.test(line));
37
+ if (providerFailure)
38
+ return `Provider authentication failed: ${providerFailure}`;
39
+ return null;
40
+ }
41
+ function summarizeRunError(projection) {
42
+ if (projection.status !== "failed")
43
+ return null;
44
+ const candidates = [];
45
+ for (const anomaly of projection.anomalies) {
46
+ appendCandidate(candidates, `Journal anomaly (${anomaly.kind}): ${anomaly.detail}`);
47
+ }
48
+ for (const phase of projection.closeoutPhases) {
49
+ if (phase.outcome === "failed")
50
+ appendCandidate(candidates, phase.detail);
51
+ }
52
+ for (const entry of projection.statusHistory) {
53
+ appendCandidate(candidates, entry.reason);
54
+ }
55
+ appendCandidate(candidates, projection.record.statusDetail);
56
+ appendCandidate(candidates, projection.record.errorText);
57
+ const nonGeneric = candidates.filter((candidate) => !isGenericRunFailure(candidate));
58
+ return categorizeUsefulRunError(nonGeneric) ?? nonGeneric.at(-1) ?? normalizeString(projection.record.errorText);
59
+ }
60
+
61
+ // packages/run-worker/src/runs/projection.ts
62
+ var EMPTY_PROJECTION = foldRunSessionEntries([], "");
63
+ var DISCOVERY_DIAGNOSTIC_RUN_ID = "__registry_discovery_error__";
64
+ function readSessionRunEntries(sessionPath) {
65
+ if (!sessionPath || !existsSync(sessionPath))
66
+ return [];
67
+ try {
68
+ return parseSessionRunEntries(readFileSync(sessionPath, "utf8"));
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+ function parseSessionRunEntries(raw) {
74
+ const entries = [];
75
+ for (const line of raw.split(`
76
+ `)) {
77
+ if (!line.trim())
78
+ continue;
79
+ try {
80
+ const parsed = JSON.parse(line);
81
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
82
+ continue;
83
+ entries.push({
84
+ type: "type" in parsed && typeof parsed.type === "string" ? parsed.type : "",
85
+ ..."customType" in parsed && typeof parsed.customType === "string" ? { customType: parsed.customType } : {},
86
+ ..."data" in parsed ? { data: parsed.data } : {}
87
+ });
88
+ } catch {
89
+ continue;
90
+ }
91
+ }
92
+ return entries;
93
+ }
94
+ function stringOrNull(value) {
95
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
96
+ }
97
+ function numberOrNull(value) {
98
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
99
+ }
100
+ function objectRecord(value) {
101
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
102
+ }
103
+ function registryStatusAsRunStatus(status) {
104
+ if (status === "waiting-input")
105
+ return "waiting-user-input";
106
+ if (status === "starting")
107
+ return "preparing";
108
+ return typeof status === "string" && [
109
+ "created",
110
+ "queued",
111
+ "preparing",
112
+ "running",
113
+ "waiting-approval",
114
+ "waiting-user-input",
115
+ "paused",
116
+ "validating",
117
+ "reviewing",
118
+ "closing-out",
119
+ "needs-attention",
120
+ "completed",
121
+ "failed",
122
+ "stopped"
123
+ ].includes(status) ? status : null;
124
+ }
125
+ function payloadString(payload, keys) {
126
+ if (!payload || typeof payload !== "object")
127
+ return null;
128
+ const record = payload;
129
+ for (const key of keys) {
130
+ const value = record[key];
131
+ if (typeof value === "string" && value.trim())
132
+ return value.trim();
133
+ }
134
+ return null;
135
+ }
136
+ function payloadOptions(payload) {
137
+ if (!payload || typeof payload !== "object")
138
+ return;
139
+ const record = payload;
140
+ const options = Array.isArray(record.options) ? record.options : Array.isArray(record.choices) ? record.choices : null;
141
+ const values = options?.filter((value) => typeof value === "string" && value.trim().length > 0) ?? [];
142
+ return values.length > 0 ? values : undefined;
143
+ }
144
+ function inboxRequest(request, kind) {
145
+ const fallback = kind === "approval" ? "Approval requested" : "Input requested";
146
+ return {
147
+ requestId: request.requestId,
148
+ kind,
149
+ title: payloadString(request.payload, ["title", "message", "reason", "prompt", "summary"]) ?? fallback,
150
+ body: payloadString(request.payload, ["body", "description", "detail", "details"]),
151
+ ...payloadOptions(request.payload) ? { options: payloadOptions(request.payload) } : {},
152
+ requestedAt: request.requestedAt ?? null,
153
+ source: "run"
154
+ };
155
+ }
156
+ function inboxFromProjection(projection) {
157
+ return [
158
+ ...projection.pendingApprovals.map((request) => inboxRequest(request, "approval")),
159
+ ...projection.pendingUserInputs.map((request) => inboxRequest(request, "input"))
160
+ ].sort((a, b) => (Date.parse(a.requestedAt ?? "") || 0) - (Date.parse(b.requestedAt ?? "") || 0));
161
+ }
162
+ function isProjectPath(projectRoot, cwd) {
163
+ const root = resolve(projectRoot);
164
+ const candidate = resolve(cwd);
165
+ const pathFromRoot = relative(root, candidate);
166
+ return pathFromRoot === "" || !pathFromRoot.startsWith("../") && pathFromRoot !== ".." && !isAbsolute(pathFromRoot);
167
+ }
168
+ function sourceFromEntry(projectRoot, projection, entry) {
169
+ const candidates = [
170
+ stringOrNull(projection.collabCwd),
171
+ stringOrNull(projection.cwd),
172
+ stringOrNull(entry.cwd),
173
+ stringOrNull(projection.worktreePath)
174
+ ].filter((cwd) => cwd !== null);
175
+ return candidates.some((cwd) => isProjectPath(projectRoot, cwd)) ? "local" : "remote";
176
+ }
177
+ function pendingRequests(value, fallbackKind) {
178
+ if (!Array.isArray(value))
179
+ return [];
180
+ return value.flatMap((item) => {
181
+ const record = objectRecord(item);
182
+ const requestId = stringOrNull(record?.requestId) ?? stringOrNull(record?.id);
183
+ if (!record || !requestId)
184
+ return [];
185
+ return [{
186
+ requestId,
187
+ requestKind: stringOrNull(record.requestKind) ?? stringOrNull(record.kind) ?? fallbackKind,
188
+ actionId: stringOrNull(record.actionId),
189
+ payload: "payload" in record ? record.payload : record,
190
+ requestedAt: stringOrNull(record.requestedAt) ?? stringOrNull(record.at) ?? ""
191
+ }];
192
+ });
193
+ }
194
+ function emptyFoldedProjection(runId, projection) {
195
+ const projectionRecord = projection;
196
+ const nestedProjection = objectRecord(projectionRecord.projection);
197
+ const pendingApprovals = pendingRequests(nestedProjection?.pendingApprovals ?? projectionRecord.pendingApprovals, "approval");
198
+ const pendingUserInputs = pendingRequests(nestedProjection?.pendingUserInputs ?? nestedProjection?.pendingInputs ?? projectionRecord.pendingUserInputs ?? (Array.isArray(projectionRecord.pendingInputs) ? projectionRecord.pendingInputs : undefined), "user-input");
199
+ const nestedRecord = objectRecord(nestedProjection?.record);
200
+ const nestedFolded = nestedProjection;
201
+ return {
202
+ ...EMPTY_PROJECTION,
203
+ ...nestedFolded ?? {},
204
+ record: {
205
+ ...nestedRecord ?? {},
206
+ runId,
207
+ ...projection.taskId ? { taskId: projection.taskId } : {},
208
+ ...projection.title ? { title: projection.title } : {},
209
+ ...projection.startedAt ? { startedAt: projection.startedAt } : {},
210
+ ...projection.updatedAt ? { updatedAt: projection.updatedAt } : {},
211
+ ...projection.completedAt ? { completedAt: projection.completedAt } : {},
212
+ ...projection.sessionPath ? { sessionPath: projection.sessionPath } : {},
213
+ ...projection.worktreePath ? { worktreePath: projection.worktreePath } : {},
214
+ ...projection.prUrl ? { prUrl: projection.prUrl } : {}
215
+ },
216
+ status: registryStatusAsRunStatus(projection.status) ?? registryStatusAsRunStatus(nestedProjection?.status),
217
+ pendingApprovals,
218
+ pendingUserInputs,
219
+ steeringCount: projection.steeringCount ?? numberOrNull(nestedProjection?.steeringCount) ?? 0,
220
+ stallCount: projection.stallCount ?? numberOrNull(nestedProjection?.stallCount) ?? 0,
221
+ lastEventAt: projection.updatedAt ?? stringOrNull(nestedProjection?.lastEventAt)
222
+ };
223
+ }
224
+ function runRecordFromRegistryEntry(projectRoot, entry) {
225
+ const projection = entry.projection && typeof entry.projection === "object" ? entry.projection : {};
226
+ const runId = stringOrNull(projection.runId) ?? entry.roomId;
227
+ const folded = emptyFoldedProjection(runId, projection);
228
+ const pushedStatus = registryStatusAsRunStatus(projection.status) ?? folded.status ?? registryStatusAsRunStatus(entry.status);
229
+ const status = entry.stale ? pushedStatus && isTerminalRunStatus(pushedStatus) ? pushedStatus : "stale" : pushedStatus ?? "running";
230
+ const sessionPath = stringOrNull(projection.sessionPath) ?? stringOrNull(entry.sessionPath) ?? null;
231
+ const collabCwd = stringOrNull(projection.collabCwd) ?? stringOrNull(projection.cwd) ?? stringOrNull(entry.cwd) ?? stringOrNull(projection.worktreePath);
232
+ return {
233
+ runId,
234
+ taskId: stringOrNull(projection.taskId),
235
+ title: stringOrNull(projection.title) ?? entry.title,
236
+ status,
237
+ source: sourceFromEntry(projectRoot, projection, entry),
238
+ live: !entry.stale,
239
+ stale: entry.stale,
240
+ startedAt: stringOrNull(projection.startedAt) ?? entry.startedAt ?? null,
241
+ updatedAt: stringOrNull(projection.updatedAt) ?? entry.heartbeatAt ?? null,
242
+ completedAt: stringOrNull(projection.completedAt),
243
+ joinLink: stringOrNull(projection.joinLink) ?? entry.joinLink ?? null,
244
+ webLink: stringOrNull(projection.webLink) ?? entry.webLink ?? null,
245
+ relayUrl: stringOrNull(projection.relayUrl) ?? entry.relayUrl ?? null,
246
+ sessionPath,
247
+ prUrl: stringOrNull(projection.prUrl),
248
+ worktreePath: stringOrNull(projection.worktreePath) ?? collabCwd,
249
+ pendingApprovals: numberOrNull(projection.pendingApprovals) ?? folded.pendingApprovals.length,
250
+ pendingInputs: numberOrNull(projection.pendingInputs) ?? folded.pendingUserInputs.length,
251
+ steeringCount: projection.steeringCount ?? 0,
252
+ stallCount: projection.stallCount ?? 0,
253
+ errorSummary: stringOrNull(projection.errorSummary) ?? summarizeRunError(folded),
254
+ timeline: Array.isArray(projection.timeline) ? [...projection.timeline] : [...timelineEntriesFromCustomEntries([])],
255
+ inbox: inboxFromProjection(folded),
256
+ collabCwd: collabCwd ?? null,
257
+ projection: folded
258
+ };
259
+ }
260
+ function runRecordsFromRegistrySnapshot(projectRoot, snapshot) {
261
+ return sortByRecency(snapshot.entries.map((entry) => runRecordFromRegistryEntry(projectRoot, entry)).filter((record) => record !== null));
262
+ }
263
+ function sortByRecency(records) {
264
+ return [...records].sort((a, b) => {
265
+ const at = Date.parse(b.updatedAt ?? b.startedAt ?? "") || 0;
266
+ const bt = Date.parse(a.updatedAt ?? a.startedAt ?? "") || 0;
267
+ return at - bt;
268
+ });
269
+ }
270
+ function discoveryDiagnosticRunRecord(projectRoot, error) {
271
+ const detail = error instanceof Error ? error.message : String(error);
272
+ const runId = DISCOVERY_DIAGNOSTIC_RUN_ID;
273
+ const projection = {
274
+ ...EMPTY_PROJECTION,
275
+ record: { runId, title: "Registry discovery unavailable" },
276
+ status: "needs-attention",
277
+ stallCount: 1
278
+ };
279
+ return {
280
+ runId,
281
+ taskId: null,
282
+ title: "Registry discovery unavailable",
283
+ status: "needs-attention",
284
+ source: "remote",
285
+ live: false,
286
+ stale: true,
287
+ startedAt: null,
288
+ updatedAt: null,
289
+ completedAt: null,
290
+ joinLink: null,
291
+ webLink: null,
292
+ relayUrl: null,
293
+ sessionPath: null,
294
+ prUrl: null,
295
+ worktreePath: null,
296
+ pendingApprovals: 0,
297
+ pendingInputs: 0,
298
+ steeringCount: 0,
299
+ stallCount: 1,
300
+ errorSummary: `registry discovery unavailable: ${detail}`,
301
+ timeline: [],
302
+ inbox: [],
303
+ collabCwd: null,
304
+ projection
305
+ };
306
+ }
307
+ async function listRunProjections(projectRoot, filter = {}) {
308
+ try {
309
+ const snapshot = await Effect.runPromise(registrySnapshotStream(projectRoot, filter).pipe(Stream.runHead, Effect.timeout(Duration.seconds(15)), Effect.map(Option.getOrNull)));
310
+ return snapshot ? runRecordsFromRegistrySnapshot(projectRoot, snapshot) : [discoveryDiagnosticRunRecord(projectRoot, "registry discovery stream ended without a snapshot")];
311
+ } catch (error) {
312
+ return [discoveryDiagnosticRunRecord(projectRoot, error)];
313
+ }
314
+ }
315
+ function selectRunProjection(runs, runId) {
316
+ const exactRun = runs.find((run) => run.runId === runId);
317
+ if (exactRun)
318
+ return exactRun;
319
+ const exactTask = runs.find((run) => run.taskId === runId);
320
+ if (exactTask)
321
+ return exactTask;
322
+ const prefixMatches = runs.filter((run) => run.runId.startsWith(runId));
323
+ if (prefixMatches.length === 1)
324
+ return prefixMatches[0] ?? null;
325
+ if (prefixMatches.length > 1) {
326
+ const matches = prefixMatches.map((run) => run.runId).join(", ");
327
+ throw new Error(`Ambiguous run id prefix "${runId}" matched ${prefixMatches.length} runs: ${matches}`);
328
+ }
329
+ return runs.find((run) => run.runId === DISCOVERY_DIAGNOSTIC_RUN_ID) ?? null;
330
+ }
331
+ async function getRunProjection(projectRoot, runId, filter = {}) {
332
+ const runs = await listRunProjections(projectRoot, filter);
333
+ return selectRunProjection(runs, runId);
334
+ }
335
+ async function resolveRunJoinTarget(projectRoot, runId, filter = {}) {
336
+ const run = await getRunProjection(projectRoot, runId, filter);
337
+ if (!run || !run.joinLink)
338
+ return null;
339
+ return { runId: run.runId, taskId: run.taskId, joinLink: run.joinLink, cwd: run.collabCwd ?? run.sessionPath, stale: run.stale };
340
+ }
341
+ var listRuns = listRunProjections;
342
+ var getRun = getRunProjection;
343
+ var resolveJoinTarget = resolveRunJoinTarget;
344
+ export {
345
+ selectRunProjection,
346
+ runRecordsFromRegistrySnapshot,
347
+ runRecordFromRegistryEntry,
348
+ resolveRunJoinTarget,
349
+ resolveJoinTarget,
350
+ readSessionRunEntries,
351
+ listRuns,
352
+ listRunProjections,
353
+ getRunProjection,
354
+ getRun,
355
+ discoveryDiagnosticRunRecord
356
+ };
@@ -0,0 +1,20 @@
1
+ import type { RunRecord } from "./projection";
2
+ export type RunClassificationPhase = "needs-attention" | "starting" | "active" | "waiting" | "paused" | "completed" | "failed" | "stopped" | "unknown";
3
+ export interface RunClassification {
4
+ readonly status: string;
5
+ readonly phase: RunClassificationPhase;
6
+ readonly isActive: boolean;
7
+ readonly isTerminal: boolean;
8
+ readonly isNeedsAttention: boolean;
9
+ }
10
+ export type RunStatusColorRole = "success" | "action-yellow" | "active-cyan" | "failure" | "muted" | "neutral";
11
+ export declare function statusColorRole(status: unknown): RunStatusColorRole;
12
+ export declare function runStatusColorRole(run: RunRecord): RunStatusColorRole;
13
+ export declare function isNeedsAttention(run: RunRecord): boolean;
14
+ export declare function classifyRun(run: RunRecord): RunClassification;
15
+ export declare function runStatusText(run: RunRecord): string;
16
+ export declare function statusRank(run: RunRecord): number;
17
+ export declare function canSteer(run: RunRecord): boolean;
18
+ export declare function canStop(run: RunRecord): boolean;
19
+ export declare function canPause(run: RunRecord): boolean;
20
+ export declare function canResume(run: RunRecord): boolean;
@@ -0,0 +1,181 @@
1
+ // @bun
2
+ // packages/run-worker/src/runs/run-status.ts
3
+ import {
4
+ ACTIVE_RUN_STATUSES,
5
+ TERMINAL_RUN_STATUSES,
6
+ isActiveRunStatus,
7
+ isTerminalRunStatus,
8
+ normalizeRunStatusToken
9
+ } from "@rig/contracts";
10
+ var KNOWN_RUN_STATUS = Object.fromEntries([...ACTIVE_RUN_STATUSES, ...TERMINAL_RUN_STATUSES].map((status) => [status, true]));
11
+ function canonicalStatusToken(status) {
12
+ const normalized = normalizeRunStatusToken(status);
13
+ if (normalized === "waiting-input")
14
+ return "waiting-user-input";
15
+ return normalized;
16
+ }
17
+ function asRunStatus(status) {
18
+ return KNOWN_RUN_STATUS[status] ? status : null;
19
+ }
20
+ function statusColorRole(status) {
21
+ switch (canonicalStatusToken(status)) {
22
+ case "done":
23
+ case "completed":
24
+ case "ready":
25
+ case "healthy":
26
+ case "approved":
27
+ case "merged":
28
+ return "success";
29
+ case "needs-attention":
30
+ case "needs_attention":
31
+ case "waiting-approval":
32
+ case "waiting-user-input":
33
+ case "waiting-input":
34
+ case "waiting_input":
35
+ case "blocked":
36
+ case "paused":
37
+ return "action-yellow";
38
+ case "running":
39
+ case "adopted":
40
+ case "preparing":
41
+ case "created":
42
+ case "queued":
43
+ case "starting":
44
+ case "pending":
45
+ case "in_progress":
46
+ case "in-progress":
47
+ case "active":
48
+ case "booting":
49
+ case "validating":
50
+ case "reviewing":
51
+ case "closing-out":
52
+ case "closing_out":
53
+ return "active-cyan";
54
+ case "failed":
55
+ case "error":
56
+ case "rejected":
57
+ return "failure";
58
+ case "stopped":
59
+ case "cancelled":
60
+ case "canceled":
61
+ case "stale":
62
+ return "muted";
63
+ default:
64
+ return "neutral";
65
+ }
66
+ }
67
+ function runStatusColorRole(run) {
68
+ const classification = classifyRun(run);
69
+ return classification.isNeedsAttention && !classification.isTerminal ? "action-yellow" : statusColorRole(classification.status);
70
+ }
71
+ function isSteerableStatus(status) {
72
+ switch (status) {
73
+ case "needs-attention":
74
+ case "waiting-approval":
75
+ case "waiting-user-input":
76
+ case "paused":
77
+ return false;
78
+ default: {
79
+ const runStatus = asRunStatus(status);
80
+ return runStatus ? isActiveRunStatus(runStatus) : false;
81
+ }
82
+ }
83
+ }
84
+ function isNeedsAttention(run) {
85
+ return canonicalStatusToken(run.status) === "needs-attention" || run.pendingApprovals + run.pendingInputs > 0 || run.stallCount > 0;
86
+ }
87
+ function phaseForStatus(status, runStatus, needsAttention) {
88
+ if (runStatus && isTerminalRunStatus(runStatus))
89
+ return runStatus === "completed" || runStatus === "failed" || runStatus === "stopped" ? runStatus : "stopped";
90
+ if (needsAttention)
91
+ return "needs-attention";
92
+ switch (status) {
93
+ case "created":
94
+ case "queued":
95
+ case "preparing":
96
+ case "starting":
97
+ case "booting":
98
+ return "starting";
99
+ case "waiting-approval":
100
+ case "waiting-user-input":
101
+ return "waiting";
102
+ case "paused":
103
+ return "paused";
104
+ case "running":
105
+ case "validating":
106
+ case "reviewing":
107
+ case "closing-out":
108
+ return "active";
109
+ default:
110
+ return runStatus && isActiveRunStatus(runStatus) ? "active" : "unknown";
111
+ }
112
+ }
113
+ function classifyRun(run) {
114
+ const status = canonicalStatusToken(run.status) || (run.live && !run.stale ? "starting" : "unknown");
115
+ const runStatus = asRunStatus(status);
116
+ const isTerminal = runStatus ? isTerminalRunStatus(runStatus) : false;
117
+ const isNeedsAttentionValue = isNeedsAttention(run);
118
+ const phase = phaseForStatus(status, runStatus, isNeedsAttentionValue);
119
+ return {
120
+ status,
121
+ phase,
122
+ isActive: runStatus ? isActiveRunStatus(runStatus) : !isTerminal && status !== "unknown",
123
+ isTerminal,
124
+ isNeedsAttention: isNeedsAttentionValue
125
+ };
126
+ }
127
+ function runStatusText(run) {
128
+ return classifyRun(run).status;
129
+ }
130
+ function statusRank(run) {
131
+ const classification = classifyRun(run);
132
+ if (classification.isNeedsAttention)
133
+ return 0;
134
+ switch (classification.phase) {
135
+ case "needs-attention":
136
+ return 0;
137
+ case "active":
138
+ case "waiting":
139
+ case "paused":
140
+ return 1;
141
+ case "starting":
142
+ return 2;
143
+ case "completed":
144
+ return 3;
145
+ case "failed":
146
+ return 4;
147
+ case "stopped":
148
+ return 5;
149
+ case "unknown":
150
+ return 6;
151
+ }
152
+ }
153
+ function canSteer(run) {
154
+ const classification = classifyRun(run);
155
+ if (classification.phase === "active" || classification.phase === "starting")
156
+ return true;
157
+ return classification.isNeedsAttention && isSteerableStatus(classification.status);
158
+ }
159
+ function canStop(run) {
160
+ const classification = classifyRun(run);
161
+ return classification.isActive && !classification.isTerminal;
162
+ }
163
+ function canPause(run) {
164
+ const phase = classifyRun(run).phase;
165
+ return phase === "active" || phase === "starting";
166
+ }
167
+ function canResume(run) {
168
+ return classifyRun(run).phase === "paused";
169
+ }
170
+ export {
171
+ statusRank,
172
+ statusColorRole,
173
+ runStatusText,
174
+ runStatusColorRole,
175
+ isNeedsAttention,
176
+ classifyRun,
177
+ canStop,
178
+ canSteer,
179
+ canResume,
180
+ canPause
181
+ };
@@ -0,0 +1,13 @@
1
+ import type { RigStatsData } from "@rig/contracts";
2
+ import type { RunRecord } from "./projection";
3
+ export { isNeedsAttention } from "./run-status";
4
+ type ListRunsFn = (projectRoot: string) => Promise<readonly RunRecord[]>;
5
+ export declare function parseTimestamp(value: string | null | undefined): number | null;
6
+ export declare function median(values: readonly number[]): number | null;
7
+ export declare function rate(part: number, total: number): number | null;
8
+ export declare function completedDuration(run: RunRecord): number | null;
9
+ export declare function computeStats(projectRootOrRuns: string | readonly RunRecord[], options?: {
10
+ since?: Date | null;
11
+ listRuns?: ListRunsFn;
12
+ }): Promise<RigStatsData>;
13
+ export declare const computeRigStats: typeof computeStats;