@h-rig/run-plugin 0.0.6-alpha.186
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 +1 -0
- package/dist/src/plugin.d.ts +4 -0
- package/dist/src/plugin.js +4319 -0
- package/dist/src/read-model/inspect-command.d.ts +19 -0
- package/dist/src/read-model/inspect-command.js +341 -0
- package/dist/src/read-model/plugin.d.ts +4 -0
- package/dist/src/read-model/plugin.js +2550 -0
- package/dist/src/read-model/read-model-backend/diagnostics.d.ts +9 -0
- package/dist/src/read-model/read-model-backend/diagnostics.js +51 -0
- package/dist/src/read-model/read-model-backend/guard.d.ts +4 -0
- package/dist/src/read-model/read-model-backend/guard.js +25 -0
- package/dist/src/read-model/read-model-backend/inbox.d.ts +23 -0
- package/dist/src/read-model/read-model-backend/inbox.js +669 -0
- package/dist/src/read-model/read-model-backend/index.d.ts +8 -0
- package/dist/src/read-model/read-model-backend/index.js +1163 -0
- package/dist/src/read-model/read-model-backend/inspect.d.ts +26 -0
- package/dist/src/read-model/read-model-backend/inspect.js +770 -0
- package/dist/src/read-model/read-model-backend/projection.d.ts +39 -0
- package/dist/src/read-model/read-model-backend/projection.js +669 -0
- package/dist/src/read-model/read-model-backend/run-status.d.ts +27 -0
- package/dist/src/read-model/read-model-backend/run-status.js +237 -0
- package/dist/src/read-model/read-model-backend/stats.d.ts +12 -0
- package/dist/src/read-model/read-model-backend/stats.js +800 -0
- package/dist/src/read-model/read-model-service.d.ts +2 -0
- package/dist/src/read-model/read-model-service.js +1725 -0
- package/dist/src/read-model/reconcile.d.ts +23 -0
- package/dist/src/read-model/reconcile.js +306 -0
- package/dist/src/read-model/run-format.d.ts +23 -0
- package/dist/src/read-model/run-format.js +202 -0
- package/dist/src/read-model/runs-screen.d.ts +14 -0
- package/dist/src/read-model/runs-screen.js +217 -0
- package/dist/src/read-model/session-journal.d.ts +26 -0
- package/dist/src/read-model/session-journal.js +355 -0
- package/dist/src/read-model/stats-command.d.ts +16 -0
- package/dist/src/read-model/stats-command.js +88 -0
- package/dist/src/read-model/stats-format.d.ts +1 -0
- package/dist/src/read-model/stats-format.js +8 -0
- package/dist/src/worker/autohost.d.ts +11 -0
- package/dist/src/worker/autohost.js +858 -0
- package/dist/src/worker/constants.d.ts +3 -0
- package/dist/src/worker/constants.js +10 -0
- package/dist/src/worker/extension.d.ts +14 -0
- package/dist/src/worker/extension.js +881 -0
- package/dist/src/worker/host.d.ts +1 -0
- package/dist/src/worker/host.js +1 -0
- package/dist/src/worker/inbox-command.d.ts +23 -0
- package/dist/src/worker/inbox-command.js +163 -0
- package/dist/src/worker/index.d.ts +2 -0
- package/dist/src/worker/index.js +1767 -0
- package/dist/src/worker/local-run-changes.d.ts +3 -0
- package/dist/src/worker/local-run-changes.js +65 -0
- package/dist/src/worker/notifications.d.ts +1 -0
- package/dist/src/worker/notifications.js +27 -0
- package/dist/src/worker/notify-cap.d.ts +11 -0
- package/dist/src/worker/notify-cap.js +13 -0
- package/dist/src/worker/panel-plugin.d.ts +11 -0
- package/dist/src/worker/panel-plugin.js +37 -0
- package/dist/src/worker/plugin.d.ts +3 -0
- package/dist/src/worker/plugin.js +1761 -0
- package/dist/src/worker/run-control-service.d.ts +2 -0
- package/dist/src/worker/run-control-service.js +210 -0
- package/dist/src/worker/session-journal-writer.d.ts +4 -0
- package/dist/src/worker/session-journal-writer.js +184 -0
- package/dist/src/worker/stall.d.ts +21 -0
- package/dist/src/worker/stall.js +55 -0
- package/dist/src/worker/utils.d.ts +21 -0
- package/dist/src/worker/utils.js +29 -0
- package/dist/src/worker/workflow-journal-writer.d.ts +7 -0
- package/dist/src/worker/workflow-journal-writer.js +76 -0
- package/package.json +47 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/run-plugin/src/read-model/runs-screen.ts
|
|
3
|
+
import {
|
|
4
|
+
DRONE_WIDTH,
|
|
5
|
+
RIG_SPINNER_FRAMES,
|
|
6
|
+
accent,
|
|
7
|
+
bold,
|
|
8
|
+
ink,
|
|
9
|
+
ink2,
|
|
10
|
+
ink3,
|
|
11
|
+
ink4,
|
|
12
|
+
phaseGlyph,
|
|
13
|
+
red,
|
|
14
|
+
renderDroneFrame,
|
|
15
|
+
statusRoleColor
|
|
16
|
+
} from "@rig/std-shared/board-theme";
|
|
17
|
+
var MAX_VISIBLE = 16;
|
|
18
|
+
function createRunsBoardView(host, deps) {
|
|
19
|
+
const { truncateToWidth, matchesKey } = deps;
|
|
20
|
+
let runs = [];
|
|
21
|
+
let selected = 0;
|
|
22
|
+
let loading = true;
|
|
23
|
+
let error = null;
|
|
24
|
+
let tick = 0;
|
|
25
|
+
let filter = "";
|
|
26
|
+
let sortMode = "active";
|
|
27
|
+
function visibleRuns() {
|
|
28
|
+
const query = filter.trim().toLowerCase();
|
|
29
|
+
const filtered = query ? runs.filter((run) => run.runId.toLowerCase().includes(query) || run.status.toLowerCase().includes(query) || run.title.toLowerCase().includes(query)) : [...runs];
|
|
30
|
+
if (sortMode === "active") {
|
|
31
|
+
return filtered.map((run, index) => ({ run, index })).sort((a, b) => a.run.rank - b.run.rank || a.index - b.index).map((entry) => entry.run);
|
|
32
|
+
}
|
|
33
|
+
if (sortMode === "status") {
|
|
34
|
+
return filtered.map((run, index) => ({ run, index })).sort((a, b) => a.run.status.localeCompare(b.run.status) || a.index - b.index).map((entry) => entry.run);
|
|
35
|
+
}
|
|
36
|
+
return filtered;
|
|
37
|
+
}
|
|
38
|
+
function clampSelection() {
|
|
39
|
+
selected = Math.max(0, Math.min(selected, Math.max(0, visibleRuns().length - 1)));
|
|
40
|
+
}
|
|
41
|
+
function moveSelection(delta) {
|
|
42
|
+
const visible = visibleRuns();
|
|
43
|
+
if (visible.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
selected = Math.max(0, Math.min(visible.length - 1, selected + delta));
|
|
46
|
+
}
|
|
47
|
+
function selectedRun() {
|
|
48
|
+
return visibleRuns()[selected] ?? null;
|
|
49
|
+
}
|
|
50
|
+
function setFilter(next) {
|
|
51
|
+
filter = next;
|
|
52
|
+
clampSelection();
|
|
53
|
+
}
|
|
54
|
+
function cycleSort() {
|
|
55
|
+
sortMode = sortMode === "active" ? "recent" : sortMode === "recent" ? "status" : "active";
|
|
56
|
+
clampSelection();
|
|
57
|
+
return sortMode;
|
|
58
|
+
}
|
|
59
|
+
function droneBoardLines(width, message, messageWidth) {
|
|
60
|
+
const spinner = RIG_SPINNER_FRAMES[tick % RIG_SPINNER_FRAMES.length];
|
|
61
|
+
if (width < DRONE_WIDTH + 4) {
|
|
62
|
+
return ["", ` ${accent(spinner)} ${ink3(message)}`, ""];
|
|
63
|
+
}
|
|
64
|
+
const pad = " ".repeat(Math.max(0, Math.floor((width - DRONE_WIDTH) / 2)));
|
|
65
|
+
const msgPad = " ".repeat(Math.max(0, Math.floor((width - messageWidth) / 2)));
|
|
66
|
+
return [
|
|
67
|
+
"",
|
|
68
|
+
...renderDroneFrame(tick).map((line) => pad + line),
|
|
69
|
+
"",
|
|
70
|
+
`${msgPad}${accent(spinner)} ${ink3(message)}`,
|
|
71
|
+
""
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
function renderLines(width) {
|
|
75
|
+
if (loading) {
|
|
76
|
+
return droneBoardLines(width, "contacting the fleet\u2026", 24);
|
|
77
|
+
}
|
|
78
|
+
if (error) {
|
|
79
|
+
return ["", ` ${red("runs unavailable:")} ${ink2(error)}`, ""];
|
|
80
|
+
}
|
|
81
|
+
const visible = visibleRuns();
|
|
82
|
+
if (runs.length === 0) {
|
|
83
|
+
return droneBoardLines(width, "no runs yet \u2014 n picks a task and launches one", 44);
|
|
84
|
+
}
|
|
85
|
+
if (visible.length === 0) {
|
|
86
|
+
return ["", ` ${ink2("no runs match")} ${accent(`/${filter}`)} ${ink3("\u2014 esc clears the filter")}`, ""];
|
|
87
|
+
}
|
|
88
|
+
const start = Math.max(0, Math.min(selected - Math.floor(MAX_VISIBLE / 2), visible.length - MAX_VISIBLE));
|
|
89
|
+
const window = visible.slice(start, start + MAX_VISIBLE);
|
|
90
|
+
const lines = window.map((run, index) => {
|
|
91
|
+
const isSelected = start + index === selected;
|
|
92
|
+
const color = statusRoleColor(run.role);
|
|
93
|
+
const dot = color(phaseGlyph(run.phase));
|
|
94
|
+
const id = run.runId.slice(0, 8);
|
|
95
|
+
const status = color(run.status.padEnd(16));
|
|
96
|
+
const title = truncateToWidth(run.title, Math.max(8, width - 36));
|
|
97
|
+
const row = ` ${dot} ${isSelected ? bold(ink(id)) : ink3(id)} ${status} ${isSelected ? ink(title) : ink2(title)}`;
|
|
98
|
+
return isSelected ? accent("\u258C") + row : " " + row;
|
|
99
|
+
});
|
|
100
|
+
const meta = [];
|
|
101
|
+
if (visible.length > MAX_VISIBLE)
|
|
102
|
+
meta.push(`${selected + 1}/${visible.length}`);
|
|
103
|
+
if (filter)
|
|
104
|
+
meta.push(`filter: ${filter}`);
|
|
105
|
+
meta.push(`sort: ${sortMode}`);
|
|
106
|
+
lines.push(ink4(` ${meta.join(" \xB7 ")}`));
|
|
107
|
+
return ["", ...lines, ""];
|
|
108
|
+
}
|
|
109
|
+
function handleInput(data) {
|
|
110
|
+
const enc = (value) => encodeURIComponent(value);
|
|
111
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
112
|
+
moveSelection(-1);
|
|
113
|
+
host.requestRender();
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
117
|
+
moveSelection(1);
|
|
118
|
+
host.requestRender();
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
122
|
+
const run = selectedRun();
|
|
123
|
+
if (run)
|
|
124
|
+
host.act(`run-detail:${enc(run.runId)}`);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (data === "a") {
|
|
128
|
+
const run = selectedRun();
|
|
129
|
+
if (run)
|
|
130
|
+
host.attach(run.runId);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (data === "s") {
|
|
134
|
+
const run = selectedRun();
|
|
135
|
+
if (run)
|
|
136
|
+
host.openSteer(run.runId);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (data === "p") {
|
|
140
|
+
const run = selectedRun();
|
|
141
|
+
if (run) {
|
|
142
|
+
host.setNotice(`pause requested for ${run.runId.slice(0, 8)}\u2026`);
|
|
143
|
+
host.act(`run-pause:${enc(run.runId)}`);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (data === "u") {
|
|
148
|
+
const run = selectedRun();
|
|
149
|
+
if (run) {
|
|
150
|
+
host.setNotice(`resume requested for ${run.runId.slice(0, 8)}\u2026`);
|
|
151
|
+
host.act(`run-resume:${enc(run.runId)}`);
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (data === "x") {
|
|
156
|
+
const run = selectedRun();
|
|
157
|
+
if (run) {
|
|
158
|
+
host.setNotice(`stop requested for ${run.runId.slice(0, 8)}\u2026`);
|
|
159
|
+
host.act(`run-stop:${enc(run.runId)}`);
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (data === "n") {
|
|
164
|
+
host.navigate("tasks");
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (data === "/") {
|
|
168
|
+
host.openSearch(filter, setFilter);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (data === "o") {
|
|
172
|
+
const mode = cycleSort();
|
|
173
|
+
host.setNotice(`sorted: ${mode}`);
|
|
174
|
+
host.requestRender();
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (matchesKey(data, "escape")) {
|
|
178
|
+
if (filter) {
|
|
179
|
+
setFilter("");
|
|
180
|
+
host.requestRender();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
render(width) {
|
|
189
|
+
return renderLines(width).map((line) => truncateToWidth(line, Math.max(10, width - 1)));
|
|
190
|
+
},
|
|
191
|
+
handleInput,
|
|
192
|
+
setBoard(board) {
|
|
193
|
+
if (board.kind !== "runs")
|
|
194
|
+
return;
|
|
195
|
+
const previous = selectedRun()?.runId;
|
|
196
|
+
runs = [...board.runs];
|
|
197
|
+
if (previous) {
|
|
198
|
+
const index = visibleRuns().findIndex((run) => run.runId === previous);
|
|
199
|
+
if (index >= 0)
|
|
200
|
+
selected = index;
|
|
201
|
+
}
|
|
202
|
+
clampSelection();
|
|
203
|
+
loading = false;
|
|
204
|
+
error = null;
|
|
205
|
+
},
|
|
206
|
+
setError(next) {
|
|
207
|
+
error = next;
|
|
208
|
+
},
|
|
209
|
+
setTick(next) {
|
|
210
|
+
tick = next;
|
|
211
|
+
},
|
|
212
|
+
invalidate() {}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export {
|
|
216
|
+
createRunsBoardView
|
|
217
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type RigRunTimelineEntry, type RunInboxResolutionSentinel, type RunJournalProjection, type RunSessionCustomEntry, type RunSessionJournalCodec, type RunStatus } from "@rig/contracts";
|
|
2
|
+
export declare function isTerminalRunStatus(status: RunStatus): boolean;
|
|
3
|
+
export declare function sessionIdFromSessionFile(sessionPath: string | null | undefined): string | null;
|
|
4
|
+
export declare function foldRunSessionEntries(entries: ReadonlyArray<RunSessionCustomEntry>, runId: string): RunJournalProjection;
|
|
5
|
+
export declare function projectRunFromSession(source: {
|
|
6
|
+
getEntries(): ReadonlyArray<RunSessionCustomEntry>;
|
|
7
|
+
} | ReadonlyArray<RunSessionCustomEntry>, runId: string): RunJournalProjection;
|
|
8
|
+
export declare function timelineEntriesFromCustomEntries(entries: readonly RunSessionCustomEntry[]): readonly RigRunTimelineEntry[];
|
|
9
|
+
export declare function latestTimelineEntriesFromCustomEntries(entries: readonly RunSessionCustomEntry[], limit: number): readonly RigRunTimelineEntry[];
|
|
10
|
+
export declare function parseStopSentinel(text: string, expectedRunId: string): {
|
|
11
|
+
reason: string | null;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function parsePauseSentinel(text: string, expectedRunId: string): {
|
|
14
|
+
requestedBy: string | null;
|
|
15
|
+
} | null;
|
|
16
|
+
export declare function parseResumeSentinel(text: string, expectedRunId: string): {
|
|
17
|
+
requestedBy: string | null;
|
|
18
|
+
} | null;
|
|
19
|
+
export declare function parseInboxResolutionSentinel(text: string, expectedRunId: string): RunInboxResolutionSentinel | null;
|
|
20
|
+
/**
|
|
21
|
+
* The read-only run-session journal codec, packaged as the `RUN_SESSION_JOURNAL`
|
|
22
|
+
* capability value. Workflow fact APPENDS (rig.workflow.*) are NOT here: a
|
|
23
|
+
* read-only projection plugin must not write a status journal (arch Law 1), so
|
|
24
|
+
* the workflow journal WRITER capability is owned by the run-worker writer owner.
|
|
25
|
+
*/
|
|
26
|
+
export declare const runSessionJournalCodec: RunSessionJournalCodec;
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/run-plugin/src/read-model/session-journal.ts
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
import {
|
|
5
|
+
CUSTOM_TYPE_FOR,
|
|
6
|
+
RIG_CONTROL_SENTINEL_END,
|
|
7
|
+
RIG_INBOX_RESOLUTION_SENTINEL,
|
|
8
|
+
RIG_PAUSE_SENTINEL,
|
|
9
|
+
RIG_RESUME_SENTINEL,
|
|
10
|
+
RIG_STOP_SENTINEL,
|
|
11
|
+
RIG_STOP_SENTINEL_END,
|
|
12
|
+
RunJournalEvent,
|
|
13
|
+
TYPE_FOR_CUSTOM
|
|
14
|
+
} from "@rig/contracts";
|
|
15
|
+
var decodeRunJournalEvent = (value) => Schema.decodeUnknownSync(RunJournalEvent)(value);
|
|
16
|
+
var RUN_STATUS_TRANSITIONS = {
|
|
17
|
+
created: ["queued", "preparing", "running", "failed", "stopped"],
|
|
18
|
+
queued: ["preparing", "running", "failed", "stopped"],
|
|
19
|
+
preparing: ["queued", "running", "needs-attention", "failed", "stopped"],
|
|
20
|
+
running: [
|
|
21
|
+
"queued",
|
|
22
|
+
"waiting-approval",
|
|
23
|
+
"waiting-user-input",
|
|
24
|
+
"paused",
|
|
25
|
+
"validating",
|
|
26
|
+
"reviewing",
|
|
27
|
+
"closing-out",
|
|
28
|
+
"needs-attention",
|
|
29
|
+
"completed",
|
|
30
|
+
"failed",
|
|
31
|
+
"stopped"
|
|
32
|
+
],
|
|
33
|
+
"waiting-approval": ["running", "needs-attention", "failed", "stopped"],
|
|
34
|
+
"waiting-user-input": ["running", "needs-attention", "failed", "stopped"],
|
|
35
|
+
paused: ["running", "failed", "stopped"],
|
|
36
|
+
validating: ["running", "reviewing", "closing-out", "needs-attention", "completed", "failed", "stopped"],
|
|
37
|
+
reviewing: ["running", "validating", "closing-out", "needs-attention", "completed", "failed", "stopped"],
|
|
38
|
+
"closing-out": ["running", "needs-attention", "completed", "failed", "stopped"],
|
|
39
|
+
"needs-attention": ["queued", "preparing", "running", "closing-out", "completed", "failed", "stopped"],
|
|
40
|
+
completed: [],
|
|
41
|
+
failed: ["queued", "preparing", "running", "closing-out"],
|
|
42
|
+
stopped: ["queued", "preparing", "running", "closing-out"]
|
|
43
|
+
};
|
|
44
|
+
var TERMINAL_RUN_STATUSES = ["completed", "failed", "stopped"];
|
|
45
|
+
function isTerminalRunStatus(status) {
|
|
46
|
+
return TERMINAL_RUN_STATUSES.includes(status);
|
|
47
|
+
}
|
|
48
|
+
function canTransitionRunStatus(from, to) {
|
|
49
|
+
if (from === null)
|
|
50
|
+
return true;
|
|
51
|
+
if (from === to)
|
|
52
|
+
return true;
|
|
53
|
+
return RUN_STATUS_TRANSITIONS[from].includes(to);
|
|
54
|
+
}
|
|
55
|
+
function assertRunStatusTransition(from, to) {
|
|
56
|
+
if (!canTransitionRunStatus(from, to)) {
|
|
57
|
+
throw new Error(`Illegal run status transition: ${from ?? "(none)"} -> ${to}. ` + `Allowed from ${from ?? "(none)"}: ${from ? RUN_STATUS_TRANSITIONS[from].join(", ") : "(any)"}.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function reduceRunJournal(events, runId = null) {
|
|
61
|
+
let record = {};
|
|
62
|
+
let status = null;
|
|
63
|
+
const statusHistory = [];
|
|
64
|
+
const pendingApprovals = new Map;
|
|
65
|
+
const resolvedApprovals = [];
|
|
66
|
+
const pendingUserInputs = new Map;
|
|
67
|
+
const resolvedUserInputs = [];
|
|
68
|
+
const closeoutPhases = [];
|
|
69
|
+
let resolvedPipeline = null;
|
|
70
|
+
const stageOutcomes = [];
|
|
71
|
+
const anomalies = [];
|
|
72
|
+
let steeringCount = 0;
|
|
73
|
+
let stallCount = 0;
|
|
74
|
+
let lastSeq = 0;
|
|
75
|
+
let lastEventAt = null;
|
|
76
|
+
const projectedRunId = runId ?? events[0]?.runId ?? null;
|
|
77
|
+
for (const event of events) {
|
|
78
|
+
lastSeq = event.seq;
|
|
79
|
+
lastEventAt = event.at;
|
|
80
|
+
switch (event.type) {
|
|
81
|
+
case "status-changed": {
|
|
82
|
+
if (!canTransitionRunStatus(status, event.to)) {
|
|
83
|
+
anomalies.push({ seq: event.seq, kind: "illegal-transition", detail: `${status ?? "(none)"} -> ${event.to}` });
|
|
84
|
+
}
|
|
85
|
+
statusHistory.push({ seq: event.seq, at: event.at, from: status, to: event.to, reason: event.reason ?? null });
|
|
86
|
+
const wasTerminal = status !== null && isTerminalRunStatus(status);
|
|
87
|
+
status = event.to;
|
|
88
|
+
record = { ...record, updatedAt: event.at };
|
|
89
|
+
if (isTerminalRunStatus(event.to) && !record.completedAt)
|
|
90
|
+
record = { ...record, completedAt: event.at };
|
|
91
|
+
if (!isTerminalRunStatus(event.to) && wasTerminal)
|
|
92
|
+
record = { ...record, completedAt: null };
|
|
93
|
+
if (event.to === "running" && !record.startedAt)
|
|
94
|
+
record = { ...record, startedAt: event.at };
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "record-patch": {
|
|
98
|
+
const next = { ...record };
|
|
99
|
+
for (const [key, value] of Object.entries(event.patch)) {
|
|
100
|
+
if (value !== undefined)
|
|
101
|
+
next[key] = value;
|
|
102
|
+
}
|
|
103
|
+
next.updatedAt = event.patch.updatedAt ?? event.at;
|
|
104
|
+
record = next;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "approval-requested": {
|
|
108
|
+
pendingApprovals.set(event.requestId, {
|
|
109
|
+
requestId: event.requestId,
|
|
110
|
+
requestKind: event.requestKind,
|
|
111
|
+
actionId: event.actionId ?? null,
|
|
112
|
+
payload: event.payload,
|
|
113
|
+
requestedAt: event.at
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "approval-resolved": {
|
|
118
|
+
const pending = pendingApprovals.get(event.requestId);
|
|
119
|
+
if (!pending) {
|
|
120
|
+
const alreadyResolved = resolvedApprovals.some((entry) => entry.requestId === event.requestId);
|
|
121
|
+
anomalies.push({ seq: event.seq, kind: alreadyResolved ? "duplicate-resolution" : "unknown-request", detail: `approval ${event.requestId}` });
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
pendingApprovals.delete(event.requestId);
|
|
125
|
+
resolvedApprovals.push({ ...pending, decision: event.decision, note: event.note ?? null, actor: event.actor, resolvedAt: event.at });
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "input-requested": {
|
|
129
|
+
pendingUserInputs.set(event.requestId, {
|
|
130
|
+
requestId: event.requestId,
|
|
131
|
+
requestKind: "user-input",
|
|
132
|
+
actionId: null,
|
|
133
|
+
payload: event.payload,
|
|
134
|
+
requestedAt: event.at
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "input-resolved": {
|
|
139
|
+
const pending = pendingUserInputs.get(event.requestId);
|
|
140
|
+
if (!pending) {
|
|
141
|
+
const alreadyResolved = resolvedUserInputs.some((entry) => entry.requestId === event.requestId);
|
|
142
|
+
anomalies.push({ seq: event.seq, kind: alreadyResolved ? "duplicate-resolution" : "unknown-request", detail: `user-input ${event.requestId}` });
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
pendingUserInputs.delete(event.requestId);
|
|
146
|
+
resolvedUserInputs.push({ ...pending, answers: event.answers, actor: event.actor, resolvedAt: event.at });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "steering":
|
|
150
|
+
steeringCount += 1;
|
|
151
|
+
break;
|
|
152
|
+
case "adopted":
|
|
153
|
+
record = { ...record, pid: event.pid, updatedAt: event.at };
|
|
154
|
+
break;
|
|
155
|
+
case "stall-detected":
|
|
156
|
+
stallCount += 1;
|
|
157
|
+
break;
|
|
158
|
+
case "closeout-phase":
|
|
159
|
+
closeoutPhases.push({ seq: event.seq, at: event.at, phase: event.phase, outcome: event.outcome, detail: event.detail ?? null });
|
|
160
|
+
break;
|
|
161
|
+
case "pipeline-resolved":
|
|
162
|
+
resolvedPipeline = event.pipeline;
|
|
163
|
+
break;
|
|
164
|
+
case "stage-outcome":
|
|
165
|
+
stageOutcomes.push({ seq: event.seq, at: event.at, outcome: event.outcome });
|
|
166
|
+
break;
|
|
167
|
+
case "timeline-entry":
|
|
168
|
+
case "log-entry":
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
runId: projectedRunId,
|
|
174
|
+
record,
|
|
175
|
+
status,
|
|
176
|
+
statusHistory,
|
|
177
|
+
pendingApprovals: [...pendingApprovals.values()],
|
|
178
|
+
resolvedApprovals,
|
|
179
|
+
pendingUserInputs: [...pendingUserInputs.values()],
|
|
180
|
+
resolvedUserInputs,
|
|
181
|
+
steeringCount,
|
|
182
|
+
stallCount,
|
|
183
|
+
closeoutPhases,
|
|
184
|
+
resolvedPipeline,
|
|
185
|
+
stageOutcomes,
|
|
186
|
+
lastSeq,
|
|
187
|
+
lastEventAt,
|
|
188
|
+
anomalies
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function sessionIdFromSessionFile(sessionPath) {
|
|
192
|
+
if (!sessionPath)
|
|
193
|
+
return null;
|
|
194
|
+
const file = sessionPath.split(/[\\/]/).pop() ?? "";
|
|
195
|
+
const match = file.match(/_([0-9a-fA-F][0-9a-fA-F-]{7,})\.jsonl$/);
|
|
196
|
+
return match?.[1] ?? null;
|
|
197
|
+
}
|
|
198
|
+
function isRunSessionCustomType(customType) {
|
|
199
|
+
return customType !== undefined && Object.hasOwn(TYPE_FOR_CUSTOM, customType);
|
|
200
|
+
}
|
|
201
|
+
function foldRunSessionEntries(entries, runId) {
|
|
202
|
+
const events = [];
|
|
203
|
+
entries.forEach((entry, index) => {
|
|
204
|
+
if (entry.type !== "custom" || !isRunSessionCustomType(entry.customType))
|
|
205
|
+
return;
|
|
206
|
+
const data = entry.data !== null && typeof entry.data === "object" ? entry.data : {};
|
|
207
|
+
const stamped = {
|
|
208
|
+
v: 1,
|
|
209
|
+
seq: index + 1,
|
|
210
|
+
at: typeof data.at === "string" ? data.at : new Date(0).toISOString(),
|
|
211
|
+
runId,
|
|
212
|
+
...data,
|
|
213
|
+
type: TYPE_FOR_CUSTOM[entry.customType]
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
events.push(decodeRunJournalEvent(stamped));
|
|
217
|
+
} catch {}
|
|
218
|
+
});
|
|
219
|
+
return reduceRunJournal(events, runId);
|
|
220
|
+
}
|
|
221
|
+
function projectRunFromSession(source, runId) {
|
|
222
|
+
const entries = "getEntries" in source ? source.getEntries() : source;
|
|
223
|
+
return foldRunSessionEntries(entries, runId);
|
|
224
|
+
}
|
|
225
|
+
function isRecord(value) {
|
|
226
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
227
|
+
}
|
|
228
|
+
function timelineEntryFromCustomEntry(entry) {
|
|
229
|
+
if (entry.customType !== CUSTOM_TYPE_FOR["timeline-entry"] || !isRecord(entry.data))
|
|
230
|
+
return null;
|
|
231
|
+
const payload = isRecord(entry.data.payload) ? entry.data.payload : entry.data;
|
|
232
|
+
const type = typeof payload.type === "string" ? payload.type : "timeline";
|
|
233
|
+
const stage = typeof payload.stage === "string" ? payload.stage : typeof payload.name === "string" ? payload.name : null;
|
|
234
|
+
const status = typeof payload.status === "string" ? payload.status : typeof payload.outcome === "string" ? payload.outcome : null;
|
|
235
|
+
const detail = typeof payload.detail === "string" ? payload.detail : typeof payload.message === "string" ? payload.message : null;
|
|
236
|
+
const at = typeof entry.data.at === "string" ? entry.data.at : typeof payload.at === "string" ? payload.at : null;
|
|
237
|
+
return { at, type, stage, status, detail };
|
|
238
|
+
}
|
|
239
|
+
function timelineEntriesFromCustomEntries(entries) {
|
|
240
|
+
return entries.flatMap((entry) => {
|
|
241
|
+
const timelineEntry = timelineEntryFromCustomEntry(entry);
|
|
242
|
+
return timelineEntry ? [timelineEntry] : [];
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function latestTimelineEntriesFromCustomEntries(entries, limit) {
|
|
246
|
+
if (limit <= 0)
|
|
247
|
+
return [];
|
|
248
|
+
const timeline = [];
|
|
249
|
+
for (let index = entries.length - 1;index >= 0 && timeline.length < limit; index -= 1) {
|
|
250
|
+
const timelineEntry = timelineEntryFromCustomEntry(entries[index]);
|
|
251
|
+
if (timelineEntry)
|
|
252
|
+
timeline.push(timelineEntry);
|
|
253
|
+
}
|
|
254
|
+
timeline.reverse();
|
|
255
|
+
return timeline;
|
|
256
|
+
}
|
|
257
|
+
function parseStopSentinel(text, expectedRunId) {
|
|
258
|
+
const start = text.indexOf(RIG_STOP_SENTINEL);
|
|
259
|
+
if (start < 0)
|
|
260
|
+
return null;
|
|
261
|
+
const end = text.indexOf(RIG_STOP_SENTINEL_END, start);
|
|
262
|
+
const body = text.slice(start + RIG_STOP_SENTINEL.length, end >= 0 ? end : undefined).trim();
|
|
263
|
+
const runId = body.match(/(?:^|\s)runId=([^\s>]+)/)?.[1] ?? "";
|
|
264
|
+
if (runId && runId !== expectedRunId)
|
|
265
|
+
return null;
|
|
266
|
+
const reason = body.match(/(?:^|\s)reason=(.+)$/)?.[1]?.trim() ?? null;
|
|
267
|
+
return { reason };
|
|
268
|
+
}
|
|
269
|
+
function parseControlSentinel(marker, text, expectedRunId) {
|
|
270
|
+
const start = text.indexOf(marker);
|
|
271
|
+
if (start < 0)
|
|
272
|
+
return null;
|
|
273
|
+
const end = text.indexOf(RIG_CONTROL_SENTINEL_END, start);
|
|
274
|
+
const body = text.slice(start + marker.length, end >= 0 ? end : undefined).trim();
|
|
275
|
+
const runId = body.match(/(?:^|\s)runId=([^\s>]+)/)?.[1] ?? "";
|
|
276
|
+
if (runId && runId !== expectedRunId)
|
|
277
|
+
return null;
|
|
278
|
+
const requestedBy = body.match(/(?:^|\s)requestedBy=([^\s>]+)/)?.[1] ?? null;
|
|
279
|
+
return { requestedBy };
|
|
280
|
+
}
|
|
281
|
+
function parsePauseSentinel(text, expectedRunId) {
|
|
282
|
+
return parseControlSentinel(RIG_PAUSE_SENTINEL, text, expectedRunId);
|
|
283
|
+
}
|
|
284
|
+
function parseResumeSentinel(text, expectedRunId) {
|
|
285
|
+
return parseControlSentinel(RIG_RESUME_SENTINEL, text, expectedRunId);
|
|
286
|
+
}
|
|
287
|
+
function decodeInboxResolutionPayload(payload) {
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse(decodeURIComponent(payload));
|
|
290
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
291
|
+
return null;
|
|
292
|
+
const record = parsed;
|
|
293
|
+
if (record.kind === "approval") {
|
|
294
|
+
const requestId = typeof record.requestId === "string" ? record.requestId.trim() : "";
|
|
295
|
+
const decision = record.decision === "approve" || record.decision === "reject" ? record.decision : null;
|
|
296
|
+
const note = typeof record.note === "string" ? record.note : record.note === null ? null : undefined;
|
|
297
|
+
if (!requestId || !decision)
|
|
298
|
+
return null;
|
|
299
|
+
return { kind: "approval", requestId, decision, ...note !== undefined ? { note } : {} };
|
|
300
|
+
}
|
|
301
|
+
if (record.kind === "input") {
|
|
302
|
+
const requestId = typeof record.requestId === "string" ? record.requestId.trim() : "";
|
|
303
|
+
const answers = record.answers && typeof record.answers === "object" && !Array.isArray(record.answers) ? Object.fromEntries(Object.entries(record.answers).filter((entry) => typeof entry[1] === "string")) : null;
|
|
304
|
+
if (!requestId || !answers)
|
|
305
|
+
return null;
|
|
306
|
+
return { kind: "input", requestId, answers };
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
function parseInboxResolutionSentinel(text, expectedRunId) {
|
|
314
|
+
const start = text.indexOf(RIG_INBOX_RESOLUTION_SENTINEL);
|
|
315
|
+
if (start < 0)
|
|
316
|
+
return null;
|
|
317
|
+
const end = text.indexOf(RIG_CONTROL_SENTINEL_END, start);
|
|
318
|
+
const body = text.slice(start + RIG_INBOX_RESOLUTION_SENTINEL.length, end >= 0 ? end : undefined).trim();
|
|
319
|
+
const runId = body.match(/(?:^|\s)runId=([^\s>]+)/)?.[1] ?? "";
|
|
320
|
+
if (runId && runId !== expectedRunId)
|
|
321
|
+
return null;
|
|
322
|
+
const requestedBy = body.match(/(?:^|\s)requestedBy=([^\s>]+)/)?.[1] ?? null;
|
|
323
|
+
const payload = body.match(/(?:^|\s)data=([^\s>]+)/)?.[1] ?? "";
|
|
324
|
+
const decoded = decodeInboxResolutionPayload(payload);
|
|
325
|
+
if (!decoded)
|
|
326
|
+
return null;
|
|
327
|
+
return { ...decoded, requestedBy };
|
|
328
|
+
}
|
|
329
|
+
var runSessionJournalCodec = {
|
|
330
|
+
foldRunSessionEntries,
|
|
331
|
+
projectRunFromSession,
|
|
332
|
+
isTerminalRunStatus,
|
|
333
|
+
canTransitionRunStatus,
|
|
334
|
+
assertRunStatusTransition,
|
|
335
|
+
sessionIdFromSessionFile,
|
|
336
|
+
timelineEntriesFromCustomEntries,
|
|
337
|
+
latestTimelineEntriesFromCustomEntries,
|
|
338
|
+
parseStopSentinel,
|
|
339
|
+
parsePauseSentinel,
|
|
340
|
+
parseResumeSentinel,
|
|
341
|
+
parseInboxResolutionSentinel
|
|
342
|
+
};
|
|
343
|
+
export {
|
|
344
|
+
timelineEntriesFromCustomEntries,
|
|
345
|
+
sessionIdFromSessionFile,
|
|
346
|
+
runSessionJournalCodec,
|
|
347
|
+
projectRunFromSession,
|
|
348
|
+
parseStopSentinel,
|
|
349
|
+
parseResumeSentinel,
|
|
350
|
+
parsePauseSentinel,
|
|
351
|
+
parseInboxResolutionSentinel,
|
|
352
|
+
latestTimelineEntriesFromCustomEntries,
|
|
353
|
+
isTerminalRunStatus,
|
|
354
|
+
foldRunSessionEntries
|
|
355
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `rig stats` CLI command — owned by run-plugin (it owns the
|
|
3
|
+
* RUN_STATS / RUN_READ_MODEL domain). Lazy-imported by plugin.ts so config-time
|
|
4
|
+
* stays light. Mechanical arg-parsing helpers live on the floor
|
|
5
|
+
* (@rig/std-shared/cli-args); table formatting is this plugin's own
|
|
6
|
+
* (./cli-format); the metric aggregation is the read-model capability's.
|
|
7
|
+
*/
|
|
8
|
+
import type { CommandOutcome, RigStatsData, RunRecord } from "@rig/contracts";
|
|
9
|
+
import type { RuntimeCliContext } from "@rig/core/config";
|
|
10
|
+
type StatsCommandDeps = {
|
|
11
|
+
listRuns?: (projectRoot: string) => Promise<readonly RunRecord[]>;
|
|
12
|
+
};
|
|
13
|
+
export declare function parseSinceOption(value: string | undefined, now?: Date): Date | null;
|
|
14
|
+
export declare function formatStatsRows(stats: RigStatsData): Array<[string, unknown]>;
|
|
15
|
+
export declare function executeStats(context: RuntimeCliContext, args: readonly string[], deps?: StatsCommandDeps): Promise<CommandOutcome>;
|
|
16
|
+
export {};
|