tickmarkr 2.5.6 → 2.5.7
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 +5 -5
- package/dist/cli/commands/status.js +95 -34
- package/dist/cli/commands/verify.js +6 -4
- 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 +4 -1
- package/dist/gates/run-gates.js +65 -9
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +20 -2
- package/dist/gates/test-reporter.js +6 -1
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +5 -0
- package/dist/run/activity.d.ts +28 -0
- package/dist/run/activity.js +194 -0
- package/dist/run/daemon.d.ts +9 -0
- package/dist/run/daemon.js +200 -46
- package/dist/run/git.d.ts +6 -1
- package/dist/run/git.js +63 -11
- package/dist/run/journal.d.ts +20 -4
- package/dist/run/journal.js +60 -9
- 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/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/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/skills/tickmarkr-overseer/SKILL.md +55 -3
package/dist/run/protocol.js
CHANGED
|
@@ -12,6 +12,38 @@ const TaskIdSchema = NonEmptyStringSchema;
|
|
|
12
12
|
const TaskIdsSchema = z.array(TaskIdSchema);
|
|
13
13
|
const AttemptSchema = z.number().int().nonnegative();
|
|
14
14
|
const EvidenceSchema = z.record(z.string(), z.unknown());
|
|
15
|
+
/** Shared lifecycle vocabulary for shell evidence and later build-result emitters. */
|
|
16
|
+
export const COMMAND_RECEIPT_OUTCOMES = [
|
|
17
|
+
"started", "completed", "spawn-failed", "timed-out", "cancelled", "reused-result", "skipped", "refused",
|
|
18
|
+
];
|
|
19
|
+
export const CommandReceiptAttributionSchema = z.object({
|
|
20
|
+
runId: NonEmptyStringSchema,
|
|
21
|
+
taskId: TaskIdSchema,
|
|
22
|
+
attempt: AttemptSchema,
|
|
23
|
+
gateRound: z.number().int().nonnegative(),
|
|
24
|
+
invocation: NonEmptyStringSchema,
|
|
25
|
+
}).strict();
|
|
26
|
+
export const ShellReceiptSchema = z.object({
|
|
27
|
+
outcome: z.enum(COMMAND_RECEIPT_OUTCOMES),
|
|
28
|
+
confirmedStart: z.boolean(),
|
|
29
|
+
attribution: CommandReceiptAttributionSchema.optional(),
|
|
30
|
+
pid: z.number().int().positive().optional(),
|
|
31
|
+
exitCode: z.number().int().nullable().optional(),
|
|
32
|
+
signal: NonEmptyStringSchema.nullable().optional(),
|
|
33
|
+
error: z.string().optional(),
|
|
34
|
+
durationMs: z.number().nonnegative().optional(),
|
|
35
|
+
}).strict();
|
|
36
|
+
/** A task-build row must carry the entire caller-owned correlation, even before spawn. */
|
|
37
|
+
export const CommandReceiptSchema = ShellReceiptSchema.extend({
|
|
38
|
+
attribution: CommandReceiptAttributionSchema,
|
|
39
|
+
});
|
|
40
|
+
export function readCommandReceipt(raw) {
|
|
41
|
+
const parsed = CommandReceiptSchema.safeParse(raw);
|
|
42
|
+
return parsed.success ? { kind: "receipt", receipt: parsed.data } : {
|
|
43
|
+
kind: "protocol-issue", raw,
|
|
44
|
+
issues: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
15
47
|
/** The T31 outcome vocabulary, made executable and strict at the persistence boundary. */
|
|
16
48
|
export const GateOutcomeSchema = z.discriminatedUnion("kind", [
|
|
17
49
|
z.object({ kind: z.literal("passed") }).strict(),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RunGraph } from "../../graph/schema.js";
|
|
2
2
|
import type { OperatorSnapshot } from "../../run/operator-state.js";
|
|
3
|
+
import type { AttemptHarvest, TaskRow } from "./derive.js";
|
|
3
4
|
/** The prototype's palette (tasks-redesign.mjs:24-30): xterm-256 indices, stated once. */
|
|
4
5
|
export declare const BOARD_PALETTE: {
|
|
5
6
|
readonly chrome: 238;
|
|
@@ -49,6 +50,8 @@ export interface BoardInput {
|
|
|
49
50
|
/** Footer key hints, joined the prototype's way; `false` leaves the footer to the caller (see boardFooter). */
|
|
50
51
|
readonly keys?: readonly string[] | false;
|
|
51
52
|
readonly colour?: boolean;
|
|
53
|
+
/** The cockpit's derived rows: the shared projection (blocker, next action, harvest) rides on each board row, unrendered. */
|
|
54
|
+
readonly rows?: readonly TaskRow[];
|
|
52
55
|
}
|
|
53
56
|
/** One task row, in the parts the Run view recomposes per width band. */
|
|
54
57
|
export interface BoardRow {
|
|
@@ -63,6 +66,12 @@ export interface BoardRow {
|
|
|
63
66
|
readonly note: string;
|
|
64
67
|
/** The note's segments, so a band can wrap it across continuation rows without cutting one. */
|
|
65
68
|
readonly noteParts: readonly string[];
|
|
69
|
+
/** From the shared projection (derive.ts TaskRow.summary), when the caller supplied rows. */
|
|
70
|
+
readonly blocker?: string;
|
|
71
|
+
readonly nextAction?: string;
|
|
72
|
+
readonly permittedActions?: readonly string[];
|
|
73
|
+
/** OBS-1048: the newest recorded attempt's harvest evidence, when the caller supplied rows. */
|
|
74
|
+
readonly harvest?: AttemptHarvest;
|
|
66
75
|
}
|
|
67
76
|
export interface BoardFrame {
|
|
68
77
|
/** Header through the gate header rule, in the prototype's order. */
|
|
@@ -89,6 +89,15 @@ export function areasFor(files) {
|
|
|
89
89
|
const hm = (ms) => `${Math.floor(ms / 3.6e6)}h${String(Math.floor((ms % 3.6e6) / 6e4)).padStart(2, "0")}m`;
|
|
90
90
|
const ago = (ms) => (ms < 6e4 ? "just now" : ms < 3.6e6 ? `${Math.round(ms / 6e4)}m ago` : `${Math.round(ms / 3.6e6)}h ago`);
|
|
91
91
|
const isDone = (t) => t !== undefined && (t.merged || t.state === "completed");
|
|
92
|
+
const projected = (row) => {
|
|
93
|
+
const blocker = row?.summary?.blocker;
|
|
94
|
+
const harvest = row?.harvests?.at(-1);
|
|
95
|
+
return {
|
|
96
|
+
...(blocker ? { blocker: blocker.kind, permittedActions: blocker.permittedActions } : {}),
|
|
97
|
+
...(blocker?.nextAction ? { nextAction: blocker.nextAction } : {}),
|
|
98
|
+
...(harvest ? { harvest } : {}),
|
|
99
|
+
};
|
|
100
|
+
};
|
|
92
101
|
/** The prototype's frame, sectioned. `renderBoard` joins it row for row. */
|
|
93
102
|
export function boardFrame(input, width) {
|
|
94
103
|
const W = Math.max(1, Math.floor(width));
|
|
@@ -218,6 +227,7 @@ export function boardFrame(input, width) {
|
|
|
218
227
|
tail: ` ${pad(mute(sliceCells(t?.channel ?? "—", CHAN - 2).head), CHAN)}${pad(mute(String(attempts)), 5)}`,
|
|
219
228
|
note: note.join(mute(" · ")),
|
|
220
229
|
noteParts: note,
|
|
230
|
+
...projected(input.rows?.find((row) => row.taskId === g.id)),
|
|
221
231
|
});
|
|
222
232
|
}
|
|
223
233
|
const bottom = [];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { readOperatorState as deriveOperatorState } from "../../run/operator-state.js";
|
|
2
|
+
import { type TaskActivityProjection } from "../../run/activity.js";
|
|
3
|
+
import { type OperatorTaskSummary } from "../../run/operator-summary.js";
|
|
2
4
|
import type { ComponentState, JournalRow, StatusStripItem } from "./components.js";
|
|
3
5
|
import type { RunViewId } from "./views.js";
|
|
4
6
|
export declare const SPARKLINE_BUCKET_WINDOW = 12;
|
|
@@ -60,8 +62,34 @@ export type CockpitGraph = {
|
|
|
60
62
|
readonly id: string;
|
|
61
63
|
readonly title?: string;
|
|
62
64
|
readonly status?: string;
|
|
65
|
+
/** Prerequisites and declared gates: read only by the shared projection (blocker, readiness). */
|
|
66
|
+
readonly deps?: readonly string[];
|
|
67
|
+
readonly gates?: readonly string[];
|
|
63
68
|
}[];
|
|
64
69
|
};
|
|
70
|
+
/** One recorded seat: a task attempt in a role. Keyed by all three, never by provider alone. */
|
|
71
|
+
export type RecordedIdentity = {
|
|
72
|
+
readonly key: string;
|
|
73
|
+
readonly role: string;
|
|
74
|
+
readonly attempt: number;
|
|
75
|
+
/** The slot name the driver recorded, verbatim; absent when the row named none. */
|
|
76
|
+
readonly name?: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* OBS-1048: what the journal recorded about one in-flight attempt's harvest. `trailerFirstSeenAt`
|
|
80
|
+
* is always absent — no producer journals it (daemon.ts samples the trailer without recording;
|
|
81
|
+
* OBS-1048 add.1, 2.5.8) — and is never synthesized from any other timestamp.
|
|
82
|
+
*/
|
|
83
|
+
export type AttemptHarvest = {
|
|
84
|
+
readonly attempt: number;
|
|
85
|
+
readonly lastWorkerStatus?: string;
|
|
86
|
+
readonly nudgeFailures: number;
|
|
87
|
+
readonly lastNudgeFailureAt?: string;
|
|
88
|
+
readonly pageCount: number;
|
|
89
|
+
readonly trailerFirstSeenAt?: undefined;
|
|
90
|
+
/** SUSPECTED: nudges failed and pages fired, and no worker-result of THIS attempt followed the launch. */
|
|
91
|
+
readonly suspectedStalledHarvest: boolean;
|
|
92
|
+
};
|
|
65
93
|
/** How an unrecorded field draws: the engagement said nothing, so the row says nothing. */
|
|
66
94
|
export declare const ABSENT_FIELD = "-";
|
|
67
95
|
/**
|
|
@@ -105,6 +133,13 @@ export type TaskRow = {
|
|
|
105
133
|
* that earns one rather than having to read `done` as if it were the same.
|
|
106
134
|
*/
|
|
107
135
|
readonly merged?: true;
|
|
136
|
+
/** Evidence-only activity projection (run/activity.ts), the one status renders. */
|
|
137
|
+
readonly activity?: TaskActivityProjection;
|
|
138
|
+
/** Operator summary (phase, blocker, next action) — the same projection status renders. */
|
|
139
|
+
readonly summary?: OperatorTaskSummary;
|
|
140
|
+
readonly identities?: readonly RecordedIdentity[];
|
|
141
|
+
/** Per recorded attempt, newest last (OBS-1048). */
|
|
142
|
+
readonly harvests?: readonly AttemptHarvest[];
|
|
108
143
|
};
|
|
109
144
|
/** One recorded gate result. `details` is the record's own text, verbatim. */
|
|
110
145
|
export type GateRow = {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// Consolidated C1/C6 readers use the incremental lifecycle contract; legacy captures keep their API.
|
|
2
2
|
export { readOperatorState as deriveOperatorState } from "../../run/operator-state.js";
|
|
3
3
|
import { channelKey } from "../../adapters/types.js";
|
|
4
|
+
import { newestPark, permittedDecisionVerbs } from "../../cli/commands/approve.js";
|
|
5
|
+
import { parseOwnedName } from "../../drivers/types.js";
|
|
6
|
+
import { projectActivity } from "../../run/activity.js";
|
|
4
7
|
import { isPidLive } from "../../run/lock.js";
|
|
8
|
+
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
9
|
+
import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
10
|
+
import { trackJournalRows } from "../../run/protocol.js";
|
|
5
11
|
export const SPARKLINE_BUCKET_WINDOW = 12;
|
|
6
12
|
const MINUTE_MS = 60_000;
|
|
7
13
|
const SPARKLINE_BUCKET_WIDTH_LADDER_MINUTES = [
|
|
@@ -470,7 +476,133 @@ function journalRows(events, tasks, defects, runStatus) {
|
|
|
470
476
|
* graph's own `status` is never consulted, which is what keeps a recompiled
|
|
471
477
|
* graph from repainting a parked task green.
|
|
472
478
|
*/
|
|
473
|
-
|
|
479
|
+
const stringField = (data, key) => typeof data[key] === "string" ? data[key] : undefined;
|
|
480
|
+
const attemptField = (data) => typeof data.attempt === "number" && Number.isInteger(data.attempt) && data.attempt >= 0 ? data.attempt : undefined;
|
|
481
|
+
/** Recorded seats for one task: role and attempt from the row, or from an owned slot name it carries. */
|
|
482
|
+
function recordedIdentities(events, taskId) {
|
|
483
|
+
const seen = new Map();
|
|
484
|
+
for (const event of events) {
|
|
485
|
+
if (event.taskId !== taskId)
|
|
486
|
+
continue;
|
|
487
|
+
const slot = recordValue(event.data.slot);
|
|
488
|
+
const name = stringField(event.data, "slotName") ?? (slot ? stringField(slot, "name") : stringField(event.data, "slot"));
|
|
489
|
+
const owned = name === undefined ? null : parseOwnedName(name);
|
|
490
|
+
const role = stringField(event.data, "role") ?? owned?.role ?? (event.event === "worker-launch" ? "worker" : undefined);
|
|
491
|
+
if (role === undefined)
|
|
492
|
+
continue;
|
|
493
|
+
const attempt = attemptField(event.data) ?? owned?.attempt;
|
|
494
|
+
if (attempt === undefined)
|
|
495
|
+
continue;
|
|
496
|
+
const key = `${taskId}:${attempt}:${role}`;
|
|
497
|
+
if (!seen.has(key))
|
|
498
|
+
seen.set(key, { key, role, attempt, ...(name === undefined ? {} : { name }) });
|
|
499
|
+
}
|
|
500
|
+
return [...seen.values()];
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* OBS-1048 fold, chronological: each worker-launch opens a fresh occurrence of its attempt label
|
|
504
|
+
* (labels reset on resume), and only a worker-result recorded AFTER that launch retires it — an
|
|
505
|
+
* unnumbered result binds to the newest launch so far, a numbered one to its own attempt's launch.
|
|
506
|
+
*/
|
|
507
|
+
function attemptHarvests(events, taskId) {
|
|
508
|
+
const byAttempt = new Map();
|
|
509
|
+
let newest;
|
|
510
|
+
for (const [order, event] of events.entries()) {
|
|
511
|
+
if (event.taskId !== taskId)
|
|
512
|
+
continue;
|
|
513
|
+
const attempt = attemptField(event.data) ?? (event.event === "worker-result" ? newest : undefined);
|
|
514
|
+
if (attempt === undefined)
|
|
515
|
+
continue;
|
|
516
|
+
const existing = byAttempt.get(attempt);
|
|
517
|
+
if (event.event === "worker-launch") {
|
|
518
|
+
// A launch resets its label's evidence: an earlier occurrence's status, nudges, pages and result are history.
|
|
519
|
+
byAttempt.set(attempt, { launched: true, returned: false, nudges: 0, pages: 0, order });
|
|
520
|
+
newest = attempt;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const h = existing ?? { launched: false, returned: false, nudges: 0, pages: 0, order };
|
|
524
|
+
if (!existing)
|
|
525
|
+
byAttempt.set(attempt, h);
|
|
526
|
+
switch (event.event) {
|
|
527
|
+
case "worker-status":
|
|
528
|
+
h.status = stringField(event.data, "status");
|
|
529
|
+
break;
|
|
530
|
+
case "worker-nudge-failed":
|
|
531
|
+
h.nudges += 1;
|
|
532
|
+
h.nudgeAt = event.ts;
|
|
533
|
+
break;
|
|
534
|
+
case "operator-page":
|
|
535
|
+
h.pages += 1;
|
|
536
|
+
break;
|
|
537
|
+
case "worker-result":
|
|
538
|
+
h.returned = true;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Attempt labels reset on resume, so numeric order is not chronology. The board reads the
|
|
543
|
+
// last harvest as newest; retain the order of each label's latest worker-launch occurrence.
|
|
544
|
+
return [...byAttempt].sort(([, a], [, b]) => a.order - b.order).map(([attempt, h]) => ({
|
|
545
|
+
attempt,
|
|
546
|
+
...(h.status === undefined ? {} : { lastWorkerStatus: h.status }),
|
|
547
|
+
nudgeFailures: h.nudges,
|
|
548
|
+
...(h.nudgeAt === undefined ? {} : { lastNudgeFailureAt: h.nudgeAt }),
|
|
549
|
+
pageCount: h.pages,
|
|
550
|
+
suspectedStalledHarvest: h.launched && !h.returned && h.nudges > 0 && h.pages > 0,
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The shared, evidence-only projection status renders (status.ts recordProjection), folded here
|
|
555
|
+
* so both surfaces read one fact. Phase/blocker/next-action come from run/activity.ts and
|
|
556
|
+
* run/operator-summary.ts; decisions from the production park oracle.
|
|
557
|
+
*/
|
|
558
|
+
function projectSummaries(events, runId, identities, graph, rowState) {
|
|
559
|
+
const journal = events.map((event) => ({
|
|
560
|
+
ts: event.ts, event: event.event, ...(event.taskId === undefined ? {} : { taskId: event.taskId }), data: event.data,
|
|
561
|
+
}));
|
|
562
|
+
const graphTask = (id) => graph?.tasks.find((task) => task.id === id);
|
|
563
|
+
const tasks = identities.map(({ id }) => ({
|
|
564
|
+
id, gates: graphTask(id)?.gates ?? [], deps: graphTask(id)?.deps ?? [], status: rowState(id) ?? graphTask(id)?.status ?? "unknown",
|
|
565
|
+
}));
|
|
566
|
+
const activity = projectActivity(runId, trackJournalRows(runId, journal.map((raw, sourceIndex) => ({ raw, sourceIndex }))), tasks);
|
|
567
|
+
const rehashAt = journal.reduce((at, event, index) => event.event === "graph-rehash" ? index : at, -1);
|
|
568
|
+
const summaryTasks = tasks.map((task) => {
|
|
569
|
+
const evidence = journal.filter((event) => event.taskId === task.id);
|
|
570
|
+
const last = evidence.at(-1);
|
|
571
|
+
const recorded = activity.get(task.id);
|
|
572
|
+
let phase = last ? recorded.state : undefined;
|
|
573
|
+
// A completed battery is evidence of readiness, never evidence that merge started (status.ts).
|
|
574
|
+
const boundary = journal.reduce((index, event, at) => ["run-start", "run-resume", "run-end"].includes(event.event)
|
|
575
|
+
|| (event.taskId === task.id && (event.event === "task-dispatch"
|
|
576
|
+
|| (event.event === "phase-start" && event.data.phase === "gates"))) ? at : index, -1);
|
|
577
|
+
const latest = new Map();
|
|
578
|
+
journal.forEach((event, index) => {
|
|
579
|
+
if (event.taskId === task.id && event.event === "gate-result" && typeof event.data.gate === "string")
|
|
580
|
+
latest.set(event.data.gate, index);
|
|
581
|
+
});
|
|
582
|
+
const priorGraph = rehashAt >= 0 && [...latest.values()].some((index) => index < rehashAt);
|
|
583
|
+
// Latest outcome including non-verdicts (status.ts): a held screen or skipped gate is not a pass.
|
|
584
|
+
const passedNow = (gate) => {
|
|
585
|
+
const latestResult = journal.slice(boundary + 1).filter((event) => event.taskId === task.id && event.event === "gate-result" && event.data.gate === gate).at(-1);
|
|
586
|
+
return latestResult !== undefined && normalizeGateOutcome(latestResult.data).kind === "passed";
|
|
587
|
+
};
|
|
588
|
+
if (recorded.state === "unconfirmed" && task.gates.length > 0 && !priorGraph && task.gates.every(passedNow)) {
|
|
589
|
+
phase = "awaiting merge phase";
|
|
590
|
+
}
|
|
591
|
+
const responsibility = [...evidence].reverse().find((event) => typeof event.data.role === "string" || typeof event.data.agent === "string");
|
|
592
|
+
return {
|
|
593
|
+
id: task.id, deps: task.deps, status: task.status, phase, lastEvidenceAt: last?.ts,
|
|
594
|
+
responsible: responsibility ? {
|
|
595
|
+
role: stringField(responsibility.data, "role"), agent: stringField(responsibility.data, "agent"),
|
|
596
|
+
} : undefined,
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
const decisions = summaryTasks.flatMap((task) => {
|
|
600
|
+
const park = task.status === "human" ? newestPark(journal, task.id) : undefined;
|
|
601
|
+
return park ? [{ taskId: task.id, park, verbs: permittedDecisionVerbs(park) }] : [];
|
|
602
|
+
});
|
|
603
|
+
return { activity, summaries: new Map(projectOperatorSummary(summaryTasks, decisions).map((task) => [task.taskId, task])) };
|
|
604
|
+
}
|
|
605
|
+
function taskRows(events, tasks, graph, runId) {
|
|
474
606
|
const lastSeen = new Map();
|
|
475
607
|
for (const event of events) {
|
|
476
608
|
if (event.taskId !== undefined)
|
|
@@ -483,6 +615,15 @@ function taskRows(events, tasks, graph) {
|
|
|
483
615
|
id,
|
|
484
616
|
title: undefined,
|
|
485
617
|
}));
|
|
618
|
+
// `task-done` records completion; only `merge` records landing. The fixed
|
|
619
|
+
// Tasks-view consumer maps `done` to a check, so the derived row must keep
|
|
620
|
+
// an unmerged completion in the neutral `completed` state and reserve
|
|
621
|
+
// `done` for a fact whose merge the journal actually carries.
|
|
622
|
+
const rowState = (id) => {
|
|
623
|
+
const fact = tasks.get(id);
|
|
624
|
+
return fact?.state === "done" && fact.merged !== true ? "completed" : fact?.state;
|
|
625
|
+
};
|
|
626
|
+
const projection = projectSummaries(events, runId, identities, graph, rowState);
|
|
486
627
|
return identities.map(({ id, title }) => {
|
|
487
628
|
const fact = tasks.get(id);
|
|
488
629
|
const assignment = fact?.dispatches.at(-1)?.assignment;
|
|
@@ -491,13 +632,9 @@ function taskRows(events, tasks, graph) {
|
|
|
491
632
|
// zero when a run resumes, so a task dispatched 0,1,0 was tried three times
|
|
492
633
|
// and reporting its last label would say one.
|
|
493
634
|
const attempts = fact?.dispatches.length ?? 0;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
// `done` for a fact whose merge the journal actually carries.
|
|
498
|
-
const state = fact?.state === "done" && fact.merged !== true
|
|
499
|
-
? "completed"
|
|
500
|
-
: fact?.state;
|
|
635
|
+
const state = rowState(id);
|
|
636
|
+
const recorded = recordedIdentities(events, id);
|
|
637
|
+
const harvests = attemptHarvests(events, id);
|
|
501
638
|
return {
|
|
502
639
|
id: `task:${id}`,
|
|
503
640
|
taskId: id,
|
|
@@ -513,6 +650,10 @@ function taskRows(events, tasks, graph) {
|
|
|
513
650
|
// Landing is its own fact. `done` is work finished; only a recorded merge
|
|
514
651
|
// is work landed, and only that earns a check-mark.
|
|
515
652
|
...(fact?.merged === true ? { merged: true } : {}),
|
|
653
|
+
activity: projection.activity.get(id),
|
|
654
|
+
summary: projection.summaries.get(id),
|
|
655
|
+
...(recorded.length === 0 ? {} : { identities: recorded }),
|
|
656
|
+
...(harvests.length === 0 ? {} : { harvests }),
|
|
516
657
|
};
|
|
517
658
|
});
|
|
518
659
|
}
|
|
@@ -737,9 +878,10 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
|
|
|
737
878
|
: interrupted
|
|
738
879
|
? "interrupted"
|
|
739
880
|
: "running";
|
|
881
|
+
const runId = source.fileName.replace(/(?:\.interrupted)?\.journal\.jsonl$/, "");
|
|
740
882
|
return {
|
|
741
883
|
binaryVersion,
|
|
742
|
-
runId
|
|
884
|
+
runId,
|
|
743
885
|
branch: typeof start.data.branch === "string" ? start.data.branch : "unknown",
|
|
744
886
|
status: runStatus,
|
|
745
887
|
elapsed: elapsedReading(events[0].ts, events.at(-1).ts),
|
|
@@ -762,7 +904,7 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
|
|
|
762
904
|
? `${spotlight.id} · ${spotlight.phase} · attempt ${taskAttempt(spotlight)} · ${taskActor(spotlight)}`
|
|
763
905
|
: "- · pending · attempt 1 · unassigned",
|
|
764
906
|
journalRows: journalRows(events, tasks, defects, runStatus),
|
|
765
|
-
taskRows: taskRows(events, tasks, options.graph),
|
|
907
|
+
taskRows: taskRows(events, tasks, options.graph, runId),
|
|
766
908
|
gateRows: gateRows(events),
|
|
767
909
|
fleetRows: fleetRows(events),
|
|
768
910
|
statusItems: [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import type { ChannelCost } from "../../report/cost.js";
|
|
3
3
|
import { type OperatorRecordRow } from "../../report/operator-record.js";
|
|
4
|
+
import { type OperatorPageGroup } from "../../run/operator-page-summary.js";
|
|
4
5
|
import type { EvidenceIdentity } from "../../run/operator-state.js";
|
|
5
6
|
import { type JournalEvent } from "../../run/journal.js";
|
|
6
7
|
import { type ComponentState } from "./components.js";
|
|
@@ -49,6 +50,7 @@ export interface EvidenceViewInput {
|
|
|
49
50
|
readonly learningPreview?: readonly string[];
|
|
50
51
|
}
|
|
51
52
|
export interface EvidenceViewModel {
|
|
53
|
+
readonly operatorPages: readonly OperatorPageGroup[];
|
|
52
54
|
readonly journal: readonly EvidenceRow[];
|
|
53
55
|
readonly channels: readonly OperatorRecordRow[];
|
|
54
56
|
readonly reportLines: readonly string[];
|
|
@@ -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 { 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
|