tickmarkr 2.5.5 → 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/README.md +3 -1
- package/dist/adapters/prompt.js +21 -1
- package/dist/cli/commands/approve.js +69 -8
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/fleet.js +4 -0
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/status.js +95 -34
- package/dist/cli/commands/verify.js +108 -85
- package/dist/config/config.d.ts +11 -0
- package/dist/config/config.js +21 -12
- package/dist/config/fleet-overlay.js +55 -17
- package/dist/drivers/index.js +2 -1
- package/dist/drivers/orca.d.ts +21 -1
- package/dist/drivers/orca.js +209 -27
- package/dist/gates/baseline.d.ts +21 -5
- package/dist/gates/baseline.js +67 -17
- package/dist/gates/cache.d.ts +14 -13
- package/dist/gates/cache.js +17 -5
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +118 -15
- package/dist/gates/run-gates.d.ts +10 -2
- package/dist/gates/run-gates.js +362 -108
- package/dist/gates/test-manifest.d.ts +33 -1
- package/dist/gates/test-manifest.js +132 -40
- package/dist/gates/test-reporter.js +20 -7
- package/dist/graph/graph.d.ts +4 -0
- package/dist/graph/graph.js +50 -1
- package/dist/run/activity.d.ts +28 -0
- package/dist/run/activity.js +194 -0
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +11 -0
- package/dist/run/daemon.js +663 -128
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +46 -1
- package/dist/run/git.js +149 -12
- package/dist/run/journal.d.ts +23 -5
- package/dist/run/journal.js +98 -25
- package/dist/run/lease.d.ts +44 -0
- package/dist/run/lease.js +226 -3
- 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/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -3
- 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/dist/tui/ink/fleet-app.d.ts +10 -2
- package/dist/tui/ink/fleet-app.js +33 -15
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +55 -3
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
|
@@ -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
|
|
@@ -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 {
|