tickmarkr 2.5.6 → 2.5.8
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/dist/adapters/prompt.js +21 -1
- package/dist/cli/commands/approve.js +13 -6
- package/dist/cli/commands/compile.js +7 -0
- package/dist/cli/commands/plan.d.ts +5 -0
- package/dist/cli/commands/plan.js +28 -23
- package/dist/cli/commands/resume.js +1 -1
- package/dist/cli/commands/run.js +1 -1
- package/dist/cli/commands/status.js +95 -34
- package/dist/cli/commands/verify.js +6 -4
- package/dist/compile/native.js +68 -7
- package/dist/compile/retired-literals.d.ts +22 -0
- package/dist/compile/retired-literals.js +271 -0
- package/dist/drivers/index.d.ts +4 -2
- package/dist/drivers/index.js +54 -6
- package/dist/gates/baseline.d.ts +8 -3
- package/dist/gates/baseline.js +6 -3
- package/dist/gates/review.js +12 -1
- package/dist/gates/run-gates.d.ts +6 -1
- package/dist/gates/run-gates.js +67 -9
- package/dist/gates/test-manifest.d.ts +12 -0
- package/dist/gates/test-manifest.js +29 -3
- package/dist/gates/test-reporter.js +28 -2
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +5 -0
- package/dist/graph/schema.d.ts +28 -0
- package/dist/graph/schema.js +13 -1
- package/dist/run/activity.d.ts +28 -0
- package/dist/run/activity.js +194 -0
- package/dist/run/daemon.d.ts +28 -0
- package/dist/run/daemon.js +466 -110
- package/dist/run/git.d.ts +6 -1
- package/dist/run/git.js +63 -11
- package/dist/run/journal.d.ts +57 -5
- package/dist/run/journal.js +103 -9
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +1 -0
- package/dist/run/operator-page-summary.d.ts +56 -0
- package/dist/run/operator-page-summary.js +68 -0
- package/dist/run/operator-summary.d.ts +69 -0
- package/dist/run/operator-summary.js +77 -0
- package/dist/run/protocol.d.ts +71 -0
- package/dist/run/protocol.js +32 -0
- package/dist/run/repair-disposition.d.ts +41 -0
- package/dist/run/repair-disposition.js +77 -0
- package/dist/tui/cockpit/board.d.ts +9 -0
- package/dist/tui/cockpit/board.js +10 -0
- package/dist/tui/cockpit/derive.d.ts +35 -0
- package/dist/tui/cockpit/derive.js +152 -10
- package/dist/tui/cockpit/evidence-view.d.ts +2 -0
- package/dist/tui/cockpit/evidence-view.js +42 -12
- package/dist/tui/cockpit/home-view.js +45 -30
- package/dist/tui/cockpit/live-store.d.ts +18 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +8 -1
- package/dist/tui/cockpit/run-cockpit.js +95 -1
- package/dist/tui/cockpit/run-view.d.ts +37 -0
- package/dist/tui/cockpit/run-view.js +189 -2
- package/package.json +1 -1
- package/schema/rungraph.schema.json +54 -0
- package/skills/tickmarkr-overseer/SKILL.md +162 -3
|
@@ -2,10 +2,12 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { existsSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { Box, useInput } from "ink";
|
|
5
|
-
import { useEffect, useState } from "react";
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
6
|
import { buildOperatorRecord, formatOperatorRecordRow } from "../../report/operator-record.js";
|
|
7
|
+
import { EMPTY_OPERATOR_PAGE_SUMMARY, foldOperatorPages, operatorPageRow } from "../../run/operator-page-summary.js";
|
|
7
8
|
import { formatJournalNarration } from "../../run/journal.js";
|
|
8
9
|
import { BodyText, JournalRowPanel, Panel } from "./components.js";
|
|
10
|
+
import { cellWidth, sliceCells } from "./width.js";
|
|
9
11
|
// review.ts:485 emits `; raw saved: <path>) — failing closed`, the path immediately followed by
|
|
10
12
|
// the message's closing paren — so the capture must stop before a trailing `)`, never swallow it.
|
|
11
13
|
const ARTIFACT_PATTERN = /raw saved:\s*([^\s)]+)/gi;
|
|
@@ -64,7 +66,7 @@ export function deriveEvidenceJournal(eventsOrRows, source = "journal.jsonl") {
|
|
|
64
66
|
time: e.ts,
|
|
65
67
|
state: stateFor(e),
|
|
66
68
|
text: formatJournalNarration(e),
|
|
67
|
-
fullText: details,
|
|
69
|
+
fullText: details || JSON.stringify(e.data, null, 2),
|
|
68
70
|
artifacts,
|
|
69
71
|
...(e.taskId ? { taskId: e.taskId } : {}),
|
|
70
72
|
...(gate ? { gate } : {}),
|
|
@@ -89,6 +91,15 @@ export function deriveEvidenceView(input) {
|
|
|
89
91
|
? input.rows.flatMap((r) => (r.event ? [r.event] : []))
|
|
90
92
|
: (input.events ?? []);
|
|
91
93
|
return {
|
|
94
|
+
operatorPages: foldOperatorPages(EMPTY_OPERATOR_PAGE_SUMMARY, items.flatMap((item, index) => {
|
|
95
|
+
const tracked = "line" in item;
|
|
96
|
+
const event = tracked ? item.event : item;
|
|
97
|
+
if (!event)
|
|
98
|
+
return [];
|
|
99
|
+
// The source identifies the owning run; preserve physical lines across malformed rows.
|
|
100
|
+
const row = operatorPageRow(event, tracked ? item.line : index + 1, tracked ? item.source ?? source : source);
|
|
101
|
+
return row ? [row] : [];
|
|
102
|
+
})).groups,
|
|
92
103
|
journal: deriveEvidenceJournal(items, source),
|
|
93
104
|
channels: buildOperatorRecord(events, input.costs ?? []),
|
|
94
105
|
reportLines: input.reportLines ?? [],
|
|
@@ -171,6 +182,18 @@ export function EvidenceView({ model, width, focused = true, focusEvidence, onSe
|
|
|
171
182
|
}, [focusEvidence?.source, focusEvidence?.line]);
|
|
172
183
|
const selectedIndex = model.journal.findIndex((row) => row.evidence.line === selectedLine);
|
|
173
184
|
const selectedRow = selectedIndex >= 0 ? model.journal[selectedIndex] : undefined;
|
|
185
|
+
// The key handler reads the selection from this ref, never from the render it closed over: ink
|
|
186
|
+
// re-subscribes the handler in an effect AFTER the frame is drawn, so a key arriving in that
|
|
187
|
+
// window would act on the previous row while the screen shows the new one (Enter opened the row
|
|
188
|
+
// above the highlighted one; a second fast arrow lost its step). Render resyncs it; a move
|
|
189
|
+
// writes it before the state update, so the next key sees the move even with no render between.
|
|
190
|
+
const lineRef = useRef(selectedLine);
|
|
191
|
+
lineRef.current = selectedLine;
|
|
192
|
+
const moveTo = (index) => {
|
|
193
|
+
const line = model.journal[index].evidence.line;
|
|
194
|
+
lineRef.current = line;
|
|
195
|
+
setSelectedLine(line);
|
|
196
|
+
};
|
|
174
197
|
useInput((input, key) => {
|
|
175
198
|
if (key.leftArrow)
|
|
176
199
|
setTab((t) => TABS[(TABS.indexOf(t) - 1 + TABS.length) % TABS.length]);
|
|
@@ -182,23 +205,30 @@ export function EvidenceView({ model, width, focused = true, focusEvidence, onSe
|
|
|
182
205
|
return;
|
|
183
206
|
if (input === "f")
|
|
184
207
|
setFollow((f) => !f);
|
|
208
|
+
const current = model.journal.findIndex((row) => row.evidence.line === lineRef.current);
|
|
185
209
|
if (key.upArrow) {
|
|
186
210
|
setFollow(false);
|
|
187
|
-
|
|
188
|
-
setSelectedLine(model.journal[i].evidence.line);
|
|
211
|
+
moveTo(Math.max(0, (current < 0 ? model.journal.length - 1 : current) - 1));
|
|
189
212
|
}
|
|
190
213
|
if (key.downArrow) {
|
|
191
214
|
setFollow(false);
|
|
192
|
-
|
|
193
|
-
setSelectedLine(model.journal[i].evidence.line);
|
|
215
|
+
moveTo(Math.min(model.journal.length - 1, (current < 0 ? 0 : current) + 1));
|
|
194
216
|
}
|
|
195
|
-
if (key.return &&
|
|
196
|
-
onSelect(
|
|
217
|
+
if (key.return && current >= 0 && onSelect)
|
|
218
|
+
onSelect(model.journal[current].evidence);
|
|
197
219
|
}, { isActive: focused });
|
|
198
|
-
const journalRows = model.journal.map((row) =>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
const journalRows = model.journal.map((row) => {
|
|
221
|
+
// Leave room for physical source references and complete state labels in the narrow body.
|
|
222
|
+
// The raw row opened with Enter retains the full recorded timestamp.
|
|
223
|
+
const time = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(row.time) ? row.time.slice(11, 19) : row.time;
|
|
224
|
+
const text = `#L${row.evidence.line} ${row.text}`;
|
|
225
|
+
// Reserve the widest state label (18 cells), two separators and the selection pointer.
|
|
226
|
+
// Bound narration before Yoga lays out the row so long task IDs cannot squeeze state words.
|
|
227
|
+
const budget = typeof width === "number" ? Math.max(1, width - cellWidth(time) - 22) : undefined;
|
|
228
|
+
const clipped = budget === undefined || cellWidth(text) <= budget ? text : `${sliceCells(text, budget - 1).head}…`;
|
|
229
|
+
return { id: row.evidence.id, time, state: row.state, text: clipped };
|
|
230
|
+
});
|
|
231
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(BodyText, { emphasis: "strong", children: ["EVIDENCE ", TABS.map((t) => (t === tab ? `[${TAB_TITLE[t]}]` : TAB_TITLE[t])).join(" "), tab === "journal" ? ` | Follow ${follow ? "on" : "off"}` : "", selectedRow ? ` | selected ${selectedRow.evidence.id}` : ""] }), tab === "journal" && (_jsxs(_Fragment, { children: [_jsx(JournalRowPanel, { title: "RAW JOURNAL", rows: journalRows, selection: selectedIndex >= 0 ? selectedIndex : undefined }), _jsx(Panel, { title: "OPERATOR PAGE GROUPS", children: model.operatorPages.length ? model.operatorPages.map((group, index) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(BodyText, { emphasis: "strong", children: [group.taskId, " \u00B7 ", group.park, " \u00B7 ", group.status] }), _jsxs(BodyText, { children: ["first ", group.firstEvidenceAt] }), _jsxs(BodyText, { children: ["last ", group.lastEvidenceAt] }), _jsxs(BodyText, { children: ["visible ", group.observedCount, " \u00B7 suppressed ", group.suppressedCount, group.rawOnly ? " · raw-only (suppression unrecorded)" : ""] }), _jsx(BodyText, { children: "Raw source lines (\u2191\u2193 select, Enter open):" }), group.lines.map(line => _jsxs(BodyText, { children: ["#L", line] }, line))] }, `${group.key}:${index}`))) : _jsx(BodyText, { emphasis: "dim", children: "no operator pages" }) }), _jsx(Panel, { title: "SELECTED EVIDENCE", children: selectedRow
|
|
202
232
|
? (_jsxs(_Fragment, { children: [_jsxs(BodyText, { emphasis: "dim", children: ["Original journal ", selectedRow.evidence.id] }), selectedRow.fullText.split("\n").map((line, i) => _jsx(BodyText, { children: line }, i)), _jsxs(BodyText, { emphasis: "dim", children: ["Artifacts: ", selectedRow.artifacts.length ? selectedRow.artifacts.join(", ") : "missing"] })] }))
|
|
203
233
|
: _jsx(BodyText, { emphasis: "dim", children: "no history" }) })] })), tab === "report" && (_jsx(Panel, { title: "REPORT", children: model.reportLines.length
|
|
204
234
|
? model.reportLines.map((line, i) => _jsx(BodyText, { children: line }, i))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, useInput } from "ink";
|
|
3
|
-
import { useLayoutEffect, useState } from "react";
|
|
3
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { GLYPHS } from "../../brand.js";
|
|
5
5
|
import { BodyText, CockpitGrid, Panel, ProgressMeter, Sparkline, StateGlyph, } from "./components.js";
|
|
6
6
|
import { ABSENT_FIELD, fieldReading } from "./derive.js";
|
|
@@ -146,75 +146,90 @@ export function HomeView({ model, width, focused = true, onOpenPark, onOpenDiagn
|
|
|
146
146
|
const maxOffset = Math.max(0, model.activity.length - 1);
|
|
147
147
|
const maxSelection = Math.max(0, model.activity.length - 1);
|
|
148
148
|
const safeOffset = Math.min(offset, maxOffset);
|
|
149
|
+
// Keep input ahead of the next render/effect subscription, as in EvidenceView.
|
|
150
|
+
// Render reconciles the refs with model updates; each move writes before setting state.
|
|
151
|
+
const sectionRef = useRef(effectiveSection);
|
|
152
|
+
const needsYouIndexRef = useRef(effectiveNeedsYouIndex);
|
|
153
|
+
const activityIndexRef = useRef(effectiveActivityIndex);
|
|
154
|
+
sectionRef.current = effectiveSection;
|
|
155
|
+
needsYouIndexRef.current = effectiveNeedsYouIndex;
|
|
156
|
+
activityIndexRef.current = effectiveActivityIndex;
|
|
157
|
+
const moveSection = (next) => {
|
|
158
|
+
sectionRef.current = next;
|
|
159
|
+
setSection(next);
|
|
160
|
+
};
|
|
161
|
+
const moveNeedsYou = (index) => {
|
|
162
|
+
needsYouIndexRef.current = index;
|
|
163
|
+
setNeedsYouIndex(index);
|
|
164
|
+
setSelectedNeedsYouId(model.needsYou[index]?.id);
|
|
165
|
+
};
|
|
166
|
+
const moveActivity = (index) => {
|
|
167
|
+
activityIndexRef.current = index;
|
|
168
|
+
setActivityIndex(index);
|
|
169
|
+
setSelectedEvidenceId(model.activity[index]?.evidence.id);
|
|
170
|
+
};
|
|
149
171
|
useInput((_input, key) => {
|
|
150
172
|
if (key.leftArrow && model.needsYou.length > 0) {
|
|
151
|
-
if (
|
|
152
|
-
const nextIndex = (
|
|
153
|
-
|
|
154
|
-
setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
|
|
173
|
+
if (sectionRef.current === "needsYou") {
|
|
174
|
+
const nextIndex = (needsYouIndexRef.current + 1) % model.needsYou.length;
|
|
175
|
+
moveNeedsYou(nextIndex);
|
|
155
176
|
}
|
|
156
177
|
else {
|
|
157
|
-
|
|
178
|
+
moveSection("needsYou");
|
|
158
179
|
}
|
|
159
180
|
}
|
|
160
181
|
if (key.rightArrow)
|
|
161
|
-
|
|
182
|
+
moveSection("activity");
|
|
162
183
|
if (key.upArrow) {
|
|
163
|
-
if (
|
|
184
|
+
if (sectionRef.current === "needsYou") {
|
|
164
185
|
if (model.needsYou.length > 0) {
|
|
165
|
-
const nextIndex = (
|
|
166
|
-
|
|
167
|
-
setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
|
|
186
|
+
const nextIndex = (needsYouIndexRef.current - 1 + model.needsYou.length) % model.needsYou.length;
|
|
187
|
+
moveNeedsYou(nextIndex);
|
|
168
188
|
}
|
|
169
189
|
}
|
|
170
190
|
else {
|
|
171
191
|
if (model.activity.length > 0) {
|
|
172
|
-
const nextIndex = Math.max(0,
|
|
173
|
-
|
|
174
|
-
setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
|
|
192
|
+
const nextIndex = Math.max(0, activityIndexRef.current - 1);
|
|
193
|
+
moveActivity(nextIndex);
|
|
175
194
|
}
|
|
176
195
|
}
|
|
177
196
|
}
|
|
178
197
|
if (key.downArrow) {
|
|
179
|
-
if (
|
|
198
|
+
if (sectionRef.current === "needsYou") {
|
|
180
199
|
if (model.needsYou.length > 0) {
|
|
181
|
-
const nextIndex = (
|
|
182
|
-
|
|
183
|
-
setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
|
|
200
|
+
const nextIndex = (needsYouIndexRef.current + 1) % model.needsYou.length;
|
|
201
|
+
moveNeedsYou(nextIndex);
|
|
184
202
|
}
|
|
185
203
|
}
|
|
186
204
|
else {
|
|
187
205
|
if (model.activity.length > 0) {
|
|
188
|
-
const nextIndex = Math.min(maxSelection,
|
|
189
|
-
|
|
190
|
-
setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
|
|
206
|
+
const nextIndex = Math.min(maxSelection, activityIndexRef.current + 1);
|
|
207
|
+
moveActivity(nextIndex);
|
|
191
208
|
}
|
|
192
209
|
}
|
|
193
210
|
}
|
|
194
211
|
if (key.pageDown) {
|
|
195
212
|
setOffset((o) => Math.max(0, o - HOME_ACTIVITY_VISIBLE_ROWS));
|
|
196
213
|
if (model.activity.length > 0) {
|
|
197
|
-
const nextIndex = Math.max(0,
|
|
198
|
-
|
|
199
|
-
setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
|
|
214
|
+
const nextIndex = Math.max(0, activityIndexRef.current - HOME_ACTIVITY_VISIBLE_ROWS);
|
|
215
|
+
moveActivity(nextIndex);
|
|
200
216
|
}
|
|
201
217
|
}
|
|
202
218
|
if (key.pageUp) {
|
|
203
219
|
setOffset((o) => Math.min(maxOffset, o + HOME_ACTIVITY_VISIBLE_ROWS));
|
|
204
220
|
if (model.activity.length > 0) {
|
|
205
|
-
const nextIndex = Math.min(maxSelection,
|
|
206
|
-
|
|
207
|
-
setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
|
|
221
|
+
const nextIndex = Math.min(maxSelection, activityIndexRef.current + HOME_ACTIVITY_VISIBLE_ROWS);
|
|
222
|
+
moveActivity(nextIndex);
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
if (key.return) {
|
|
211
|
-
if (
|
|
212
|
-
const target =
|
|
226
|
+
if (sectionRef.current === "needsYou") {
|
|
227
|
+
const target = selectNeedsYouTarget(model, needsYouIndexRef.current);
|
|
213
228
|
if (target)
|
|
214
229
|
(target.kind === "park" ? onOpenPark : onOpenDiagnostic)(target.id);
|
|
215
230
|
}
|
|
216
231
|
else {
|
|
217
|
-
const row = model.activity[
|
|
232
|
+
const row = model.activity[activityIndexRef.current];
|
|
218
233
|
if (row)
|
|
219
234
|
onOpenEvidence(row.evidence);
|
|
220
235
|
}
|
|
@@ -121,6 +121,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
121
121
|
oracle: "test";
|
|
122
122
|
test: string;
|
|
123
123
|
text?: string | undefined;
|
|
124
|
+
landing?: string | undefined;
|
|
124
125
|
} | {
|
|
125
126
|
oracle: "judge";
|
|
126
127
|
text: string;
|
|
@@ -138,6 +139,14 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
138
139
|
confidence?: number | undefined;
|
|
139
140
|
reason?: string | undefined;
|
|
140
141
|
}[] | undefined;
|
|
142
|
+
pins?: ({
|
|
143
|
+
kind: "literal";
|
|
144
|
+
text: string;
|
|
145
|
+
glob: string;
|
|
146
|
+
} | {
|
|
147
|
+
kind: "fixture";
|
|
148
|
+
paths: string[];
|
|
149
|
+
})[] | undefined;
|
|
141
150
|
routingHints?: {
|
|
142
151
|
pin?: {
|
|
143
152
|
via: string;
|
|
@@ -213,6 +222,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
213
222
|
oracle: "test";
|
|
214
223
|
test: string;
|
|
215
224
|
text?: string | undefined;
|
|
225
|
+
landing?: string | undefined;
|
|
216
226
|
} | {
|
|
217
227
|
oracle: "judge";
|
|
218
228
|
text: string;
|
|
@@ -230,6 +240,14 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
230
240
|
confidence?: number | undefined;
|
|
231
241
|
reason?: string | undefined;
|
|
232
242
|
}[] | undefined;
|
|
243
|
+
pins?: ({
|
|
244
|
+
kind: "literal";
|
|
245
|
+
text: string;
|
|
246
|
+
glob: string;
|
|
247
|
+
} | {
|
|
248
|
+
kind: "fixture";
|
|
249
|
+
paths: string[];
|
|
250
|
+
})[] | undefined;
|
|
233
251
|
routingHints?: {
|
|
234
252
|
pin?: {
|
|
235
253
|
via: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import { type JournalRow } from "./components.js";
|
|
3
|
-
import { type RunCockpitData } from "./derive.js";
|
|
3
|
+
import { type RunCockpitData, type TaskRow } from "./derive.js";
|
|
4
4
|
import { type KeybarEntry, type RunInteractionState, type RunKeyProjection } from "./keys.js";
|
|
5
5
|
import { type RunViewId } from "./views.js";
|
|
6
6
|
import { type FramePlan, type FrameRegion, type FrameState, type FrameCockpitLayout, type FrameView, type MeasuredSize, type PlannedFrame } from "./layout.js";
|
|
@@ -29,6 +29,13 @@ export declare function panelRows(lines: readonly string[], columns: number): nu
|
|
|
29
29
|
* identities assigned by the production journal derivation.
|
|
30
30
|
*/
|
|
31
31
|
export declare function deriveRunViewRows(data: RunCockpitData, viewId: RunViewId, filterQuery?: string): readonly JournalRow[];
|
|
32
|
+
/**
|
|
33
|
+
* T9: the shared projection (derive.ts TaskRow.summary/activity/identities/harvests) read onto the
|
|
34
|
+
* task row — identity, phase, build evidence, blocker, next action — each unrecorded field saying so,
|
|
35
|
+
* each recorded one citing the journal line it was read from, and the OBS-1048 stall marker when the
|
|
36
|
+
* newest attempt's harvest is suspect. No attempt label is printed here: the ruler is the row's own.
|
|
37
|
+
*/
|
|
38
|
+
export declare function taskProjectionText(row: TaskRow, journal?: readonly JournalRow[]): string;
|
|
32
39
|
/**
|
|
33
40
|
* The identities the body's rows carry in the order the body draws them — the
|
|
34
41
|
* same window `SizedJournalPanel` paints, so a hit on a body row resolves to
|
|
@@ -4,6 +4,7 @@ import { cloneElement, createContext, useContext, useEffect, useInsertionEffect,
|
|
|
4
4
|
import { GLYPHS, PLAIN_COMPACT_LOCKUP, } from "../../brand.js";
|
|
5
5
|
import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, keyRosterLines, Panel, PANEL_CHROME_ROWS, ProgressMeter, Sparkline, StatTile, StatusStrip, } from "./components.js";
|
|
6
6
|
import { fieldReading } from "./derive.js";
|
|
7
|
+
import { STALL_MARKER } from "./run-view.js";
|
|
7
8
|
import { initialRunInteractionState, projectRunKeyEntries, reconcileRunInteraction, RUN_SIDE_RAIL_COLUMN_FLOOR, runPanelFocusOrder, runSideRailVisible, selectableRunViewRowIds, } from "./keys.js";
|
|
8
9
|
import { DECISIONS_TAB_TITLE, RUN_VIEWS, runViewById, setupSectionAt, SETUP_SECTIONS, } from "./views.js";
|
|
9
10
|
import { FRAME_HEADER_GAP, FRAME_NESTED_ROW_BANDS, FULL_JOURNAL_ROWS, planFrame, RUN_PANEL_HORIZONTAL_CHROME, SIDEBAR_COLUMN_FLOOR, } from "./layout.js";
|
|
@@ -279,7 +280,7 @@ function promotedViewRows(data, viewId) {
|
|
|
279
280
|
time: fieldReading(row.lastEventTime),
|
|
280
281
|
...(row.lastEventTimestamp === undefined ? {} : { timestamp: row.lastEventTimestamp }),
|
|
281
282
|
state: taskRowState(row),
|
|
282
|
-
text: `${row.taskId} · ${fieldReading(row.state)} · ${runAttemptLabel(row.attempts)} · ${fieldReading(row.actor)}${row.title === undefined ? "" : ` · ${row.title}`}`,
|
|
283
|
+
text: `${row.taskId} · ${fieldReading(row.state)} · ${runAttemptLabel(row.attempts)} · ${fieldReading(row.actor)}${row.title === undefined ? "" : ` · ${row.title}`} · ${taskProjectionText(row, data.journalRows)}`,
|
|
283
284
|
}));
|
|
284
285
|
}
|
|
285
286
|
if (viewId === "gates") {
|
|
@@ -302,6 +303,99 @@ function promotedViewRows(data, viewId) {
|
|
|
302
303
|
}
|
|
303
304
|
return [];
|
|
304
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* T9: the shared projection (derive.ts TaskRow.summary/activity/identities/harvests) read onto the
|
|
308
|
+
* task row — identity, phase, build evidence, blocker, next action — each unrecorded field saying so,
|
|
309
|
+
* each recorded one citing the journal line it was read from, and the OBS-1048 stall marker when the
|
|
310
|
+
* newest attempt's harvest is suspect. No attempt label is printed here: the ruler is the row's own.
|
|
311
|
+
*/
|
|
312
|
+
export function taskProjectionText(row, journal = []) {
|
|
313
|
+
const identity = row.identities?.at(-1);
|
|
314
|
+
const blocker = row.summary?.blocker;
|
|
315
|
+
const harvest = row.harvests?.at(-1);
|
|
316
|
+
const evidence = projectionEvidence(row, journal);
|
|
317
|
+
const at = (line) => line === undefined ? "(no journal row)" : `#L${line}`;
|
|
318
|
+
const responsible = row.summary?.responsible;
|
|
319
|
+
const identityLabel = identity
|
|
320
|
+
? `${identity.role} ${fieldReading(identity.name ?? responsible?.agent)}`
|
|
321
|
+
: responsible?.role !== undefined || responsible?.agent !== undefined
|
|
322
|
+
? `${fieldReading(responsible.role)} ${fieldReading(responsible.agent)}`
|
|
323
|
+
: fieldReading(undefined);
|
|
324
|
+
return [
|
|
325
|
+
`identity ${identityLabel}${evidence.identity === undefined ? identityLabel === fieldReading(undefined) ? "" : ` ${at(undefined)}` : ` ${at(evidence.identity)}`}`,
|
|
326
|
+
`phase ${fieldReading(row.summary?.phase ?? row.activity?.state)}${row.summary?.phase === undefined && row.activity === undefined ? "" : ` ${at(evidence.phase)}`}`,
|
|
327
|
+
`build ${fieldReading(row.activity?.build.state)}${row.activity !== undefined && "receipt" in row.activity.build ? ` ${at(evidence.build)}` : ""}`,
|
|
328
|
+
`blocker ${blocker ? `${blocker.kind} ${at(evidence.blocker)}` : "none"}`,
|
|
329
|
+
`next ${blocker?.nextAction == null ? fieldReading(undefined) : `${blocker.nextAction} ${at(evidence.nextAction)}`}`,
|
|
330
|
+
...(harvest?.suspectedStalledHarvest ? [`${STALL_MARKER} · launch ${at(evidence.launch)} · ${harvest.nudgeFailures} nudge failed ${at(evidence.nudge)} · ${harvest.pageCount} paged ${at(evidence.page)} · no worker-result`] : []),
|
|
331
|
+
].reduce((text, part) => `${text} · ${part}`);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Correlate each already-derived field with the row that can have produced that value. This is
|
|
335
|
+
* intentionally not a "newest row with a plausible name" lookup: launch identity is paired by
|
|
336
|
+
* recorded seat, phase starts at the current launch boundary, a receipt is taken from the current
|
|
337
|
+
* verification battery, and harvest counts select only the number of rows retained by that fold.
|
|
338
|
+
* The newest presentation row omits its event name; it is used only for a terminal field whose
|
|
339
|
+
* derived last-evidence timestamp proves that exact row is the transition.
|
|
340
|
+
*/
|
|
341
|
+
function projectionEvidence(row, journal) {
|
|
342
|
+
const facts = journal.flatMap((entry) => {
|
|
343
|
+
const match = /^event:(\d+)$/u.exec(entry.id);
|
|
344
|
+
if (!match || !entry.text.startsWith(`${row.taskId} `))
|
|
345
|
+
return [];
|
|
346
|
+
const separator = entry.text.lastIndexOf(" · ");
|
|
347
|
+
return [{ line: Number(match[1]), timestamp: entry.timestamp, event: separator < 0 ? undefined : entry.text.slice(separator + 3) }];
|
|
348
|
+
}).sort((a, b) => a.line - b.line);
|
|
349
|
+
const lines = (event) => facts.filter((fact) => fact.event === event).map((fact) => fact.line);
|
|
350
|
+
// JournalRow is deliberately presentation-only: it carries neither the raw payload nor its
|
|
351
|
+
// attempt. Consequently two plausible rows cannot be correlated to the accepted projection
|
|
352
|
+
// without guessing. Fail closed in that case; the evidence remains reachable in the journal.
|
|
353
|
+
const only = (candidates) => candidates.length === 1 ? candidates[0] : undefined;
|
|
354
|
+
const unknownAfter = (line) => facts
|
|
355
|
+
.filter((fact) => fact.event === undefined && (line === undefined || fact.line > line))
|
|
356
|
+
.map((fact) => fact.line);
|
|
357
|
+
const newestTaskLine = [...facts].reverse().find((fact) => fact.timestamp === row.lastEventTimestamp)?.line;
|
|
358
|
+
const namedIdentities = row.identities?.filter((seat) => seat.name !== undefined) ?? [];
|
|
359
|
+
const currentIdentity = row.identities?.at(-1);
|
|
360
|
+
const launches = lines("worker-launch");
|
|
361
|
+
const identityIndex = currentIdentity === undefined ? -1 : namedIdentities.findIndex((seat) => seat.key === currentIdentity.key);
|
|
362
|
+
// A newest worker-launch has no printed event suffix. It is nevertheless the only missing launch
|
|
363
|
+
// when the identity fold gained one more named seat on the row carrying lastEventTimestamp.
|
|
364
|
+
if (launches.length < namedIdentities.length && newestTaskLine !== undefined && !launches.includes(newestTaskLine))
|
|
365
|
+
launches.push(newestTaskLine);
|
|
366
|
+
const identity = identityIndex >= 0 && launches.length === namedIdentities.length
|
|
367
|
+
? launches[identityIndex]
|
|
368
|
+
: currentIdentity === undefined ? only(lines("worker-result")) : undefined;
|
|
369
|
+
const launch = identityIndex >= 0 && launches.length === namedIdentities.length ? launches[identityIndex] : undefined;
|
|
370
|
+
const afterLaunch = (candidates) => candidates.filter((line) => launch === undefined || line > launch);
|
|
371
|
+
const state = row.summary?.phase ?? row.activity?.state;
|
|
372
|
+
let phase;
|
|
373
|
+
if (state === "terminal") {
|
|
374
|
+
phase = only([...lines("task-human"), ...lines("task-failed"), ...lines("task-done"), ...lines("task-approved"), ...lines("merge")].sort((a, b) => a - b));
|
|
375
|
+
}
|
|
376
|
+
else if (state === "implementing")
|
|
377
|
+
phase = launch;
|
|
378
|
+
else if (state === "preparing")
|
|
379
|
+
phase = only([...lines("task-dispatch"), ...unknownAfter(undefined)]);
|
|
380
|
+
else if (state === "returned-for-verification")
|
|
381
|
+
phase = only([...afterLaunch(lines("worker-result")), ...unknownAfter(launch)]);
|
|
382
|
+
else if (state === "validating" || state === "reviewing")
|
|
383
|
+
phase = only([...afterLaunch([...lines("phase-start"), ...lines("gate-phase-start")].sort((a, b) => a - b)), ...unknownAfter(launch)]);
|
|
384
|
+
else
|
|
385
|
+
phase = only([...afterLaunch([...lines("gate-result"), ...lines("phase-start"), ...lines("gate-phase-start")].sort((a, b) => a - b)), ...unknownAfter(launch)]);
|
|
386
|
+
const battery = lines("phase-start").at(-1) ?? launch;
|
|
387
|
+
const receiptLines = [...lines("build-receipt"), ...lines("build-result")].sort((a, b) => a - b).filter((line) => battery === undefined || line > battery);
|
|
388
|
+
const build = row.activity !== undefined && "receipt" in row.activity.build ? only([...receiptLines, ...unknownAfter(battery)]) : undefined;
|
|
389
|
+
const parkCandidates = [...lines("task-human"), ...lines("task-failed")].sort((a, b) => a - b);
|
|
390
|
+
const park = only(parkCandidates) ?? (parkCandidates.length === 0 && row.state === "human" ? newestTaskLine : undefined);
|
|
391
|
+
const nudgeCandidates = afterLaunch(lines("worker-nudge-failed"));
|
|
392
|
+
const pageCandidates = afterLaunch(lines("operator-page"));
|
|
393
|
+
const nudgeCount = row.harvests?.at(-1)?.nudgeFailures ?? 0;
|
|
394
|
+
const pageCount = row.harvests?.at(-1)?.pageCount ?? 0;
|
|
395
|
+
const nudge = nudgeCount > 0 && nudgeCandidates.length === nudgeCount ? nudgeCandidates.at(-1) : undefined;
|
|
396
|
+
const page = pageCount > 0 && pageCandidates.length === pageCount ? pageCandidates.at(-1) : undefined;
|
|
397
|
+
return { identity, phase, build, ...(row.summary?.blocker ? { blocker: park, nextAction: park } : {}), launch, nudge, page };
|
|
398
|
+
}
|
|
305
399
|
/** A task row exists on an operator surface only when the journal fold recorded a task fact. */
|
|
306
400
|
function recordedTaskRow(row) {
|
|
307
401
|
return row.state !== undefined
|
|
@@ -83,6 +83,43 @@ export declare function applyRunViewKey(session: RunViewSession, event: RunViewK
|
|
|
83
83
|
* a closed run whose record lacks a bucket says so rather than reading absence as empty.
|
|
84
84
|
*/
|
|
85
85
|
export declare function runSummaryLine(s: OperatorSnapshot): string;
|
|
86
|
+
/** One projected reading and the physical journal line it came from; no line means no row says it. */
|
|
87
|
+
export interface ProjectionField {
|
|
88
|
+
readonly label: string;
|
|
89
|
+
readonly line?: number;
|
|
90
|
+
}
|
|
91
|
+
export interface TaskProjection {
|
|
92
|
+
readonly taskId: string;
|
|
93
|
+
readonly identity: ProjectionField;
|
|
94
|
+
readonly phase: ProjectionField;
|
|
95
|
+
readonly build: ProjectionField;
|
|
96
|
+
readonly blocker: ProjectionField;
|
|
97
|
+
readonly nextAction: ProjectionField;
|
|
98
|
+
/** OBS-1048: present only when the newest launched attempt has failed nudges, pages and no worker-result. */
|
|
99
|
+
readonly stalled?: ProjectionField;
|
|
100
|
+
}
|
|
101
|
+
export declare const STALL_MARKER = "\u26A0 stalled harvest suspected";
|
|
102
|
+
/**
|
|
103
|
+
* The evidence-only projection status renders (run/activity.ts, run/operator-summary.ts), folded
|
|
104
|
+
* over the journal rows this view was handed, with the physical line each reading was taken from.
|
|
105
|
+
* Nothing is inferred from a title, a provider or a task id: an unrecorded field says so.
|
|
106
|
+
*/
|
|
107
|
+
export declare function projectRunTasks(snapshot: OperatorSnapshot, rows: readonly RunEvidenceRow[], graph: RunGraph | undefined, decisions: readonly RunDecision[], runId: string): readonly TaskProjection[];
|
|
108
|
+
/** One wrapped line per task: every field with its line, and the stall marker when the harvest is suspect. */
|
|
109
|
+
export declare const projectionLine: (p: TaskProjection) => string;
|
|
110
|
+
export interface PaneLocator {
|
|
111
|
+
readonly status: "recorded" | "unavailable";
|
|
112
|
+
readonly reason: string;
|
|
113
|
+
/** The worker-launch row the reading stands on, when one exists. */
|
|
114
|
+
readonly line?: number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Where the current attempt's pane is recorded — and only that. The reading is exact-name: the
|
|
118
|
+
* launch row's slot name must be the owned name of THIS task, attempt and run. No terminal is ever
|
|
119
|
+
* chosen by title, provider or task id; a missing, foreign, stale or ambiguous record reads
|
|
120
|
+
* unavailable with the rows it was read from, and reading never touches a host.
|
|
121
|
+
*/
|
|
122
|
+
export declare function paneLocator(task: OperatorTask, rows: readonly RunEvidenceRow[], runId: string): PaneLocator;
|
|
86
123
|
/** The board's footer keys merged with the cockpit's own — every one of them handled by this view or the shell. */
|
|
87
124
|
export declare const RUN_VIEW_KEYS: readonly string[];
|
|
88
125
|
export interface RunViewProps {
|