tickmarkr 2.4.2 → 2.5.0

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.
Files changed (70) hide show
  1. package/README.md +110 -24
  2. package/dist/cli/commands/approve.d.ts +42 -0
  3. package/dist/cli/commands/approve.js +80 -13
  4. package/dist/cli/commands/doctor.d.ts +234 -0
  5. package/dist/cli/commands/doctor.js +139 -5
  6. package/dist/cli/commands/report.js +15 -44
  7. package/dist/cli/commands/scope.js +36 -6
  8. package/dist/cli/commands/stats.d.ts +2 -0
  9. package/dist/cli/commands/stats.js +42 -23
  10. package/dist/cli/commands/status.js +20 -4
  11. package/dist/cli/commands/ui.js +42 -53
  12. package/dist/cli/commands/unlock.d.ts +1 -1
  13. package/dist/cli/commands/unlock.js +59 -9
  14. package/dist/cli/help.d.ts +219 -0
  15. package/dist/cli/help.js +212 -0
  16. package/dist/cli/index.d.ts +42 -2
  17. package/dist/cli/index.js +23 -8
  18. package/dist/drivers/herdr.d.ts +7 -2
  19. package/dist/drivers/herdr.js +78 -48
  20. package/dist/drivers/orca.d.ts +3 -2
  21. package/dist/drivers/orca.js +43 -4
  22. package/dist/drivers/subprocess.d.ts +1 -0
  23. package/dist/drivers/subprocess.js +3 -0
  24. package/dist/drivers/types.d.ts +14 -0
  25. package/dist/gates/artifact-manifest.d.ts +50 -0
  26. package/dist/gates/artifact-manifest.js +23 -0
  27. package/dist/plan/scope.d.ts +25 -0
  28. package/dist/plan/scope.js +92 -12
  29. package/dist/report/operator-record.d.ts +49 -0
  30. package/dist/report/operator-record.js +137 -0
  31. package/dist/run/daemon.d.ts +3 -4
  32. package/dist/run/daemon.js +18 -10
  33. package/dist/run/lock.d.ts +59 -2
  34. package/dist/run/lock.js +184 -26
  35. package/dist/run/operator-state.d.ts +86 -0
  36. package/dist/run/operator-state.js +165 -0
  37. package/dist/run/supervision.d.ts +32 -0
  38. package/dist/run/supervision.js +138 -17
  39. package/dist/tui/cockpit/capture.d.ts +19 -0
  40. package/dist/tui/cockpit/capture.js +89 -1
  41. package/dist/tui/cockpit/components.d.ts +15 -1
  42. package/dist/tui/cockpit/components.js +79 -9
  43. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  44. package/dist/tui/cockpit/decision-actions.js +315 -0
  45. package/dist/tui/cockpit/derive.d.ts +1 -1
  46. package/dist/tui/cockpit/derive.js +2 -0
  47. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  48. package/dist/tui/cockpit/evidence-view.js +210 -0
  49. package/dist/tui/cockpit/home-view.d.ts +88 -0
  50. package/dist/tui/cockpit/home-view.js +240 -0
  51. package/dist/tui/cockpit/keys.d.ts +125 -0
  52. package/dist/tui/cockpit/keys.js +31 -0
  53. package/dist/tui/cockpit/layout.d.ts +14 -0
  54. package/dist/tui/cockpit/layout.js +15 -0
  55. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  56. package/dist/tui/cockpit/live-runtime.js +683 -0
  57. package/dist/tui/cockpit/live-store.d.ts +289 -0
  58. package/dist/tui/cockpit/live-store.js +308 -0
  59. package/dist/tui/cockpit/live.d.ts +21 -1
  60. package/dist/tui/cockpit/live.js +12 -1
  61. package/dist/tui/cockpit/run-view.d.ts +100 -0
  62. package/dist/tui/cockpit/run-view.js +202 -0
  63. package/dist/tui/cockpit/shell.d.ts +50 -0
  64. package/dist/tui/cockpit/shell.js +74 -0
  65. package/dist/tui/cockpit/theme.d.ts +27 -0
  66. package/dist/tui/cockpit/theme.js +21 -0
  67. package/package.json +1 -1
  68. package/skills/tickmarkr-auto/SKILL.md +10 -1
  69. package/skills/tickmarkr-loop/SKILL.md +72 -1
  70. package/skills/tickmarkr-overseer/SKILL.md +12 -0
@@ -0,0 +1,210 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { existsSync, renameSync, statSync, unlinkSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { Box, useInput } from "ink";
5
+ import { useEffect, useState } from "react";
6
+ import { buildOperatorRecord, formatOperatorRecordRow } from "../../report/operator-record.js";
7
+ import { formatJournalNarration } from "../../run/journal.js";
8
+ import { BodyText, JournalRowPanel, Panel } from "./components.js";
9
+ // review.ts:485 emits `; raw saved: <path>) — failing closed`, the path immediately followed by
10
+ // the message's closing paren — so the capture must stop before a trailing `)`, never swallow it.
11
+ const ARTIFACT_PATTERN = /raw saved:\s*([^\s)]+)/gi;
12
+ /** Durable artifact locations named inside a gate/review's recorded detail text. */
13
+ export function parseArtifactLocations(details) {
14
+ return [...details.matchAll(ARTIFACT_PATTERN)].map((m) => m[1]);
15
+ }
16
+ function stateFor(e) {
17
+ if (e.event === "gate-result" || e.event === "review-leg2")
18
+ return e.data.pass === true ? "pass" : e.data.pass === false ? "fail" : "neutral";
19
+ if (e.event === "task-failed")
20
+ return "fail";
21
+ if (e.event === "task-human" || e.event === "task-blocked")
22
+ return "warn";
23
+ if (e.event === "merge" || e.event === "task-done" || e.event === "run-end")
24
+ return "pass";
25
+ if (e.event === "task-dispatch" || e.event === "run-start" || e.event === "run-resume")
26
+ return "active";
27
+ return "neutral";
28
+ }
29
+ /** Pure C2-journal-to-Evidence-rows mapping: the original journal position IS the evidence identity
30
+ * — assigned once here, from the full unfiltered event list, never recomputed from a display slice. */
31
+ export function deriveEvidenceJournal(eventsOrRows, source = "journal.jsonl") {
32
+ const result = [];
33
+ for (let i = 0; i < eventsOrRows.length; i++) {
34
+ const item = eventsOrRows[i];
35
+ let e;
36
+ let line;
37
+ let rowSource = source;
38
+ let id;
39
+ if ("line" in item && typeof item.line === "number") {
40
+ if (!item.event)
41
+ continue;
42
+ e = item.event;
43
+ line = item.line;
44
+ rowSource = item.source ?? source;
45
+ id = item.id ?? `${rowSource}#L${line}`;
46
+ }
47
+ else {
48
+ e = item;
49
+ line = i + 1;
50
+ id = `${source}#L${line}`;
51
+ }
52
+ if (!e)
53
+ continue;
54
+ const details = typeof e.data?.details === "string" ? e.data.details : "";
55
+ const artifacts = parseArtifactLocations(details);
56
+ if (typeof e.data?.artifactPath === "string" && e.data.artifactPath.trim()) {
57
+ const p = e.data.artifactPath.trim();
58
+ if (!artifacts.includes(p))
59
+ artifacts.push(p);
60
+ }
61
+ const gate = typeof e.data?.gate === "string" ? e.data.gate : (e.event === "review-leg2" ? "review" : undefined);
62
+ result.push({
63
+ evidence: { source: rowSource, line, id },
64
+ time: e.ts,
65
+ state: stateFor(e),
66
+ text: formatJournalNarration(e),
67
+ fullText: details,
68
+ artifacts,
69
+ ...(e.taskId ? { taskId: e.taskId } : {}),
70
+ ...(gate ? { gate } : {}),
71
+ });
72
+ }
73
+ return result;
74
+ }
75
+ /** The most recent review verdict recorded for a task — by original journal position, so a merged
76
+ * task's review opens the row it actually happened on rather than any on-screen index. */
77
+ export function selectTaskReview(journal, taskId) {
78
+ for (let i = journal.length - 1; i >= 0; i--) {
79
+ const row = journal[i];
80
+ if (row.taskId === taskId && (row.gate === "review" || row.gate === "review-leg2"))
81
+ return row;
82
+ }
83
+ return undefined;
84
+ }
85
+ export function deriveEvidenceView(input) {
86
+ const source = input.source ?? "journal.jsonl";
87
+ const items = input.rows ?? input.events ?? [];
88
+ const events = input.rows
89
+ ? input.rows.flatMap((r) => (r.event ? [r.event] : []))
90
+ : (input.events ?? []);
91
+ return {
92
+ journal: deriveEvidenceJournal(items, source),
93
+ channels: buildOperatorRecord(events, input.costs ?? []),
94
+ reportLines: input.reportLines ?? [],
95
+ statsLines: input.statsLines ?? [],
96
+ learningPreview: input.learningPreview?.length ? input.learningPreview : ["no telemetry yet"],
97
+ };
98
+ }
99
+ /** The default beside-spec destination, or undefined when the source spec is ambiguous or unknown —
100
+ * an ambiguous/missing source never guesses a path; the caller must ask for explicit choice. */
101
+ export function defaultExportPath(target) {
102
+ if (target.specPaths.length !== 1 || !target.specDir)
103
+ return undefined;
104
+ const specPath = target.specPaths[0];
105
+ const base = specPath.split("/").pop() ?? specPath;
106
+ const stem = base.includes(".") ? base.slice(0, base.lastIndexOf(".")) : base;
107
+ return `${target.specDir}/${stem}.${target.runId}.report.md`;
108
+ }
109
+ export function planEvidenceExport(path, io) {
110
+ return { path, exists: io.existsSync(path) };
111
+ }
112
+ export function writeEvidenceExport(path, record, io, confirmedOverwrite = false) {
113
+ if (io.existsSync(path) && !confirmedOverwrite) {
114
+ return { ok: false, reason: "exists — overwrite not confirmed" };
115
+ }
116
+ const dir = dirname(path);
117
+ const base = basename(path);
118
+ const tempPath = join(dir, `.${base}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`);
119
+ try {
120
+ io.writeFileSync(tempPath, record, { flag: "wx" });
121
+ if (io.existsSync(path) && !confirmedOverwrite) {
122
+ try {
123
+ (io.unlinkSync ?? unlinkSync)(tempPath);
124
+ }
125
+ catch { }
126
+ return { ok: false, reason: "exists — overwrite not confirmed" };
127
+ }
128
+ (io.renameSync ?? renameSync)(tempPath, path);
129
+ }
130
+ catch (error) {
131
+ const isEexist = (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST")
132
+ || (error instanceof Error && error.message.includes("EEXIST"));
133
+ if (!isEexist) {
134
+ try {
135
+ (io.unlinkSync ?? unlinkSync)(tempPath);
136
+ }
137
+ catch { }
138
+ }
139
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
140
+ }
141
+ const bytes = (io.statSync ? io.statSync(path).size : undefined) ?? (existsSync(path) ? statSync(path).size : Buffer.byteLength(record, "utf8"));
142
+ return { ok: true, path, bytes };
143
+ }
144
+ // ── View ─────────────────────────────────────────────────────────────────────────────────────────
145
+ const TABS = ["journal", "report", "stats", "channels", "learning"];
146
+ const TAB_TITLE = {
147
+ journal: "JOURNAL", report: "REPORT", stats: "STATS", channels: "CHANNELS", learning: "LEARNING PREVIEW",
148
+ };
149
+ /** The exported Evidence body: C1 mounts this against a live EvidenceViewModel. Selection is held by
150
+ * original journal line in local state — never recomputed from the array's current length — so a
151
+ * newly appended row can never steal it while Follow is off; toggling Follow on snaps back to the
152
+ * tail. */
153
+ export function EvidenceView({ model, width, focused = true, focusEvidence, onSelect, onExport }) {
154
+ const [tab, setTab] = useState("journal");
155
+ const [follow, setFollow] = useState(focusEvidence === undefined);
156
+ const [selectedLine, setSelectedLine] = useState(focusEvidence?.line ?? model.journal.at(-1)?.evidence.line);
157
+ // model.journal.length (not the array itself) drives re-selection on growth; `follow` drives the
158
+ // snap-back-to-tail transition when the operator turns it on.
159
+ useEffect(() => {
160
+ if (follow)
161
+ setSelectedLine(model.journal.at(-1)?.evidence.line);
162
+ }, [model.journal.length, follow]);
163
+ // A new inbound navigation target (C1 opening a different row) re-targets selection and turns
164
+ // Follow off, keyed on the target's identity — not the object — so an unchanged prop re-rendered
165
+ // as a fresh literal never re-fires and clobbers the operator's own manual navigation.
166
+ useEffect(() => {
167
+ if (focusEvidence) {
168
+ setFollow(false);
169
+ setSelectedLine(focusEvidence.line);
170
+ }
171
+ }, [focusEvidence?.source, focusEvidence?.line]);
172
+ const selectedIndex = model.journal.findIndex((row) => row.evidence.line === selectedLine);
173
+ const selectedRow = selectedIndex >= 0 ? model.journal[selectedIndex] : undefined;
174
+ useInput((input, key) => {
175
+ if (key.leftArrow)
176
+ setTab((t) => TABS[(TABS.indexOf(t) - 1 + TABS.length) % TABS.length]);
177
+ if (key.rightArrow)
178
+ setTab((t) => TABS[(TABS.indexOf(t) + 1) % TABS.length]);
179
+ if (input === "e" && onExport)
180
+ onExport();
181
+ if (tab !== "journal" || model.journal.length === 0)
182
+ return;
183
+ if (input === "f")
184
+ setFollow((f) => !f);
185
+ if (key.upArrow) {
186
+ setFollow(false);
187
+ const i = Math.max(0, (selectedIndex < 0 ? model.journal.length - 1 : selectedIndex) - 1);
188
+ setSelectedLine(model.journal[i].evidence.line);
189
+ }
190
+ if (key.downArrow) {
191
+ setFollow(false);
192
+ const i = Math.min(model.journal.length - 1, (selectedIndex < 0 ? 0 : selectedIndex) + 1);
193
+ setSelectedLine(model.journal[i].evidence.line);
194
+ }
195
+ if (key.return && selectedRow && onSelect)
196
+ onSelect(selectedRow.evidence);
197
+ }, { isActive: focused });
198
+ const journalRows = model.journal.map((row) => ({
199
+ id: row.evidence.id, time: row.time, state: row.state, text: row.text,
200
+ }));
201
+ 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, { rows: journalRows, selection: selectedIndex >= 0 ? selectedIndex : undefined }), _jsx(Panel, { title: "SELECTED EVIDENCE", children: selectedRow
202
+ ? (_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
+ : _jsx(BodyText, { emphasis: "dim", children: "no history" }) })] })), tab === "report" && (_jsx(Panel, { title: "REPORT", children: model.reportLines.length
204
+ ? model.reportLines.map((line, i) => _jsx(BodyText, { children: line }, i))
205
+ : _jsx(BodyText, { emphasis: "dim", children: "no history" }) })), tab === "stats" && (_jsx(Panel, { title: "STATS", children: model.statsLines.length
206
+ ? model.statsLines.map((line, i) => _jsx(BodyText, { children: line }, i))
207
+ : _jsx(BodyText, { emphasis: "dim", children: "no history" }) })), tab === "channels" && (_jsx(Panel, { title: "CHANNELS", children: model.channels.length
208
+ ? model.channels.map((row) => (_jsx(BodyText, { children: formatOperatorRecordRow(row) }, row.channel)))
209
+ : _jsx(BodyText, { emphasis: "dim", children: "no history" }) })), tab === "learning" && (_jsx(Panel, { title: "LEARNING PREVIEW", children: model.learningPreview.map((line, i) => _jsx(BodyText, { children: line }, i)) }))] }));
210
+ }
@@ -0,0 +1,88 @@
1
+ import { type ReactElement } from "react";
2
+ import type { EvidenceIdentity, OperatorSnapshot, OperatorTask } from "../../run/operator-state.js";
3
+ import { type ComponentState } from "./components.js";
4
+ /** Rows visible in the ACTIVITY panel at once; PageUp/PageDown move the window by this many. */
5
+ export declare const HOME_ACTIVITY_VISIBLE_ROWS = 3;
6
+ export interface HomeSeats {
7
+ /** Currently dispatched/running workers — a graph-backed fact, not a config count. */
8
+ readonly active: number;
9
+ /** Configured worker-eligible channels. Kept apart from `active`: neither is derivable from the other. */
10
+ readonly eligible: number;
11
+ }
12
+ export interface HomeSpend {
13
+ readonly measurable: boolean;
14
+ /** Ignored unless `measurable`: a caller must not smuggle a `$0` display past an unmeasurable flag. */
15
+ readonly display?: string;
16
+ }
17
+ export interface HomeActivityRow {
18
+ /** The original journal line this row draws — never a filtered/on-screen ordinal. */
19
+ readonly evidence: EvidenceIdentity;
20
+ readonly time: string;
21
+ readonly state: ComponentState;
22
+ readonly text: string;
23
+ }
24
+ export interface HomeNeedsYouTarget {
25
+ readonly kind: "park" | "diagnostic";
26
+ readonly id: string;
27
+ readonly label: string;
28
+ }
29
+ export interface HomeViewInput {
30
+ readonly operator: OperatorSnapshot;
31
+ readonly seats: HomeSeats;
32
+ readonly spend?: HomeSpend;
33
+ /** Sparkline samples for the MERGED tile; absent/all-null renders "no history", never a blank ramp. */
34
+ readonly trend?: readonly (number | null)[];
35
+ readonly activity?: readonly HomeActivityRow[];
36
+ /** Available diagnostic targets Needs-you may offer when no park is open (e.g. a lock/read issue). */
37
+ readonly diagnostics?: readonly HomeNeedsYouTarget[];
38
+ }
39
+ export interface HomeViewModel {
40
+ readonly hasRun: boolean;
41
+ readonly lifecycle: OperatorSnapshot["lifecycle"];
42
+ readonly label: string;
43
+ readonly green: boolean;
44
+ readonly comparable: boolean;
45
+ readonly merged: number;
46
+ readonly planned?: number;
47
+ /** A comparable graph with zero tasks: distinct from "not comparable" and from 100%. */
48
+ readonly noPlan: boolean;
49
+ readonly trend: readonly (number | null)[];
50
+ readonly gatesRan: {
51
+ readonly passed: number;
52
+ readonly total: number;
53
+ };
54
+ readonly gatePassRate?: number;
55
+ readonly currentTip: OperatorSnapshot["currentTip"];
56
+ readonly buckets: OperatorSnapshot["buckets"];
57
+ readonly seats: HomeSeats;
58
+ readonly spend: HomeSpend;
59
+ readonly humanCount: number;
60
+ readonly blockedCount: number;
61
+ readonly needsYou: readonly HomeNeedsYouTarget[];
62
+ readonly activity: readonly HomeActivityRow[];
63
+ readonly runningTask?: OperatorTask;
64
+ readonly progressPercent?: number;
65
+ readonly progressCaption: string;
66
+ }
67
+ /**
68
+ * Pure C2-to-Home mapping. Needs-you reads LIVE task state (not the closed
69
+ * run's buckets, which the fold clears on resume) so a park stays actionable
70
+ * for an active/resumed run, not only a finished one.
71
+ */
72
+ export declare function deriveHomeView(input: HomeViewInput): HomeViewModel;
73
+ /** The park/diagnostic Needs-you would open right now, or undefined when nothing needs the operator. */
74
+ export declare function selectNeedsYouTarget(model: HomeViewModel, index?: number): HomeNeedsYouTarget | undefined;
75
+ /** The original evidence a given activity row opens, however far outside the visible window it sits. */
76
+ export declare function selectActivityTarget(model: HomeViewModel, index: number): EvidenceIdentity | undefined;
77
+ export interface HomeViewProps {
78
+ readonly model: HomeViewModel;
79
+ readonly width?: number | string;
80
+ /** Whether Home currently owns local input; a shell showing another view passes false. */
81
+ readonly focused?: boolean;
82
+ readonly onOpenPark: (taskId: string) => void;
83
+ readonly onOpenDiagnostic: (id: string) => void;
84
+ readonly onOpenEvidence: (evidence: EvidenceIdentity) => void;
85
+ readonly onSelectNeedsYou?: (target: HomeNeedsYouTarget | undefined, index: number) => void;
86
+ }
87
+ /** The exported Home body: C1 mounts this against a live HomeViewModel and its own navigation callbacks. */
88
+ export declare function HomeView({ model, width, focused, onOpenPark, onOpenDiagnostic, onOpenEvidence, onSelectNeedsYou, }: HomeViewProps): ReactElement;
@@ -0,0 +1,240 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, useInput } from "ink";
3
+ import { useLayoutEffect, useState } from "react";
4
+ import { GLYPHS } from "../../brand.js";
5
+ import { BodyText, CockpitGrid, Panel, ProgressMeter, Sparkline, StateGlyph, } from "./components.js";
6
+ import { ABSENT_FIELD, fieldReading } from "./derive.js";
7
+ /** Rows visible in the ACTIVITY panel at once; PageUp/PageDown move the window by this many. */
8
+ export const HOME_ACTIVITY_VISIBLE_ROWS = 3;
9
+ const parkLabel = (task) => `${task.id} ${task.parkKind ?? task.state}`;
10
+ /**
11
+ * Pure C2-to-Home mapping. Needs-you reads LIVE task state (not the closed
12
+ * run's buckets, which the fold clears on resume) so a park stays actionable
13
+ * for an active/resumed run, not only a finished one.
14
+ */
15
+ export function deriveHomeView(input) {
16
+ const { operator } = input;
17
+ const hasRun = operator.lifecycle !== "UNKNOWN";
18
+ const humanTasks = operator.tasks.filter((t) => t.state === "human");
19
+ const blockedTasks = operator.tasks.filter((t) => t.state === "blocked");
20
+ const parks = [...humanTasks, ...blockedTasks].map((t) => ({
21
+ kind: "park",
22
+ id: t.id,
23
+ label: parkLabel(t),
24
+ }));
25
+ const diagnostics = (input.diagnostics ?? []).filter((d) => d.id.trim().length > 0);
26
+ const needsYou = [...parks, ...diagnostics];
27
+ const planned = operator.comparable ? operator.planned : undefined;
28
+ const noPlan = operator.comparable && planned === 0;
29
+ const gatePassRate = operator.gatesRan.total > 0
30
+ ? Math.round((operator.gatesRan.passed / operator.gatesRan.total) * 100)
31
+ : undefined;
32
+ const runningTask = operator.tasks.find((t) => t.state === "running");
33
+ const progressPercent = planned && planned > 0
34
+ ? Math.round((operator.merged / planned) * 100)
35
+ : undefined;
36
+ const progressCaption = !hasRun
37
+ ? ""
38
+ : noPlan
39
+ ? "no plan"
40
+ : !operator.comparable
41
+ ? "not comparable"
42
+ : `worker ${fieldReading(runningTask?.id)} | attempt ${fieldReading(runningTask?.attempt)}`;
43
+ return {
44
+ hasRun,
45
+ lifecycle: operator.lifecycle,
46
+ label: operator.label,
47
+ green: operator.green,
48
+ comparable: operator.comparable,
49
+ merged: operator.merged,
50
+ planned,
51
+ noPlan,
52
+ trend: input.trend ?? [],
53
+ gatesRan: operator.gatesRan,
54
+ gatePassRate,
55
+ currentTip: operator.currentTip,
56
+ buckets: operator.buckets,
57
+ seats: input.seats,
58
+ // A caller marking spend unmeasurable may still carry a stale `display`; discard it so an
59
+ // all-subscription/no-telemetry fixture can never surface as a measured $0.
60
+ spend: input.spend?.measurable ? input.spend : { measurable: false },
61
+ humanCount: humanTasks.length,
62
+ blockedCount: blockedTasks.length,
63
+ needsYou,
64
+ activity: input.activity ?? [],
65
+ runningTask,
66
+ progressPercent,
67
+ progressCaption,
68
+ };
69
+ }
70
+ /** The park/diagnostic Needs-you would open right now, or undefined when nothing needs the operator. */
71
+ export function selectNeedsYouTarget(model, index = 0) {
72
+ if (model.needsYou.length === 0)
73
+ return undefined;
74
+ const safeIndex = Math.min(Math.max(0, index), model.needsYou.length - 1);
75
+ return model.needsYou[safeIndex];
76
+ }
77
+ /** The original evidence a given activity row opens, however far outside the visible window it sits. */
78
+ export function selectActivityTarget(model, index) {
79
+ return model.activity[index]?.evidence;
80
+ }
81
+ const hasTrend = (trend) => trend.some((sample) => typeof sample === "number");
82
+ function Tile({ label, children }) {
83
+ return _jsx(Panel, { title: label, flexGrow: 1, children: children });
84
+ }
85
+ /** The exported Home body: C1 mounts this against a live HomeViewModel and its own navigation callbacks. */
86
+ export function HomeView({ model, width, focused = true, onOpenPark, onOpenDiagnostic, onOpenEvidence, onSelectNeedsYou, }) {
87
+ const [offset, setOffset] = useState(0);
88
+ const [selectedEvidenceId, setSelectedEvidenceId] = useState(() => model.activity[0]?.evidence.id);
89
+ const [activityIndex, setActivityIndex] = useState(0);
90
+ const [selectedNeedsYouId, setSelectedNeedsYouId] = useState(() => model.needsYou[0]?.id);
91
+ const [needsYouIndex, setNeedsYouIndex] = useState(0);
92
+ const [section, setSection] = useState(model.needsYou.length > 0 ? "needsYou" : "activity");
93
+ // Reconcile Needs-you selection against model updates by stable target id.
94
+ let effectiveNeedsYouIndex = 0;
95
+ if (model.needsYou.length > 0) {
96
+ const found = selectedNeedsYouId !== undefined
97
+ ? model.needsYou.findIndex((t) => t.id === selectedNeedsYouId)
98
+ : -1;
99
+ if (found >= 0) {
100
+ effectiveNeedsYouIndex = found;
101
+ }
102
+ else {
103
+ effectiveNeedsYouIndex = Math.min(Math.max(0, needsYouIndex), model.needsYou.length - 1);
104
+ }
105
+ }
106
+ const selectedNeedsYou = selectNeedsYouTarget(model, effectiveNeedsYouIndex);
107
+ // Reconcile Activity selection against model updates by stable evidence id.
108
+ let effectiveActivityIndex = 0;
109
+ if (model.activity.length > 0) {
110
+ const found = selectedEvidenceId !== undefined
111
+ ? model.activity.findIndex((r) => r.evidence.id === selectedEvidenceId)
112
+ : -1;
113
+ if (found >= 0) {
114
+ effectiveActivityIndex = found;
115
+ }
116
+ else {
117
+ effectiveActivityIndex = Math.min(Math.max(0, activityIndex), model.activity.length - 1);
118
+ }
119
+ }
120
+ useLayoutEffect(() => {
121
+ if (model.needsYou.length > 0) {
122
+ const currentTargetId = model.needsYou[effectiveNeedsYouIndex]?.id;
123
+ if (currentTargetId !== selectedNeedsYouId) {
124
+ setSelectedNeedsYouId(currentTargetId);
125
+ }
126
+ if (effectiveNeedsYouIndex !== needsYouIndex) {
127
+ setNeedsYouIndex(effectiveNeedsYouIndex);
128
+ }
129
+ }
130
+ onSelectNeedsYou?.(selectedNeedsYou, effectiveNeedsYouIndex);
131
+ }, [selectedNeedsYou?.id, effectiveNeedsYouIndex, onSelectNeedsYou]);
132
+ useLayoutEffect(() => {
133
+ if (model.activity.length > 0) {
134
+ const currentEvidenceId = model.activity[effectiveActivityIndex]?.evidence.id;
135
+ if (currentEvidenceId !== selectedEvidenceId) {
136
+ setSelectedEvidenceId(currentEvidenceId);
137
+ }
138
+ if (effectiveActivityIndex !== activityIndex) {
139
+ setActivityIndex(effectiveActivityIndex);
140
+ }
141
+ }
142
+ }, [model.activity, effectiveActivityIndex]);
143
+ const effectiveSection = model.needsYou.length > 0 ? section : "activity";
144
+ // The offset can reach the very last row (a short final page), not just the last full window —
145
+ // otherwise the oldest row could never become the one Enter opens.
146
+ const maxOffset = Math.max(0, model.activity.length - 1);
147
+ const maxSelection = Math.max(0, model.activity.length - 1);
148
+ const safeOffset = Math.min(offset, maxOffset);
149
+ useInput((_input, key) => {
150
+ if (key.leftArrow && model.needsYou.length > 0) {
151
+ if (effectiveSection === "needsYou") {
152
+ const nextIndex = (effectiveNeedsYouIndex + 1) % model.needsYou.length;
153
+ setNeedsYouIndex(nextIndex);
154
+ setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
155
+ }
156
+ else {
157
+ setSection("needsYou");
158
+ }
159
+ }
160
+ if (key.rightArrow)
161
+ setSection("activity");
162
+ if (key.upArrow) {
163
+ if (effectiveSection === "needsYou") {
164
+ if (model.needsYou.length > 0) {
165
+ const nextIndex = (effectiveNeedsYouIndex - 1 + model.needsYou.length) % model.needsYou.length;
166
+ setNeedsYouIndex(nextIndex);
167
+ setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
168
+ }
169
+ }
170
+ else {
171
+ if (model.activity.length > 0) {
172
+ const nextIndex = Math.max(0, effectiveActivityIndex - 1);
173
+ setActivityIndex(nextIndex);
174
+ setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
175
+ }
176
+ }
177
+ }
178
+ if (key.downArrow) {
179
+ if (effectiveSection === "needsYou") {
180
+ if (model.needsYou.length > 0) {
181
+ const nextIndex = (effectiveNeedsYouIndex + 1) % model.needsYou.length;
182
+ setNeedsYouIndex(nextIndex);
183
+ setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
184
+ }
185
+ }
186
+ else {
187
+ if (model.activity.length > 0) {
188
+ const nextIndex = Math.min(maxSelection, effectiveActivityIndex + 1);
189
+ setActivityIndex(nextIndex);
190
+ setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
191
+ }
192
+ }
193
+ }
194
+ if (key.pageDown) {
195
+ setOffset((o) => Math.max(0, o - HOME_ACTIVITY_VISIBLE_ROWS));
196
+ if (model.activity.length > 0) {
197
+ const nextIndex = Math.max(0, effectiveActivityIndex - HOME_ACTIVITY_VISIBLE_ROWS);
198
+ setActivityIndex(nextIndex);
199
+ setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
200
+ }
201
+ }
202
+ if (key.pageUp) {
203
+ setOffset((o) => Math.min(maxOffset, o + HOME_ACTIVITY_VISIBLE_ROWS));
204
+ if (model.activity.length > 0) {
205
+ const nextIndex = Math.min(maxSelection, effectiveActivityIndex + HOME_ACTIVITY_VISIBLE_ROWS);
206
+ setActivityIndex(nextIndex);
207
+ setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
208
+ }
209
+ }
210
+ if (key.return) {
211
+ if (effectiveSection === "needsYou") {
212
+ const target = selectedNeedsYou;
213
+ if (target)
214
+ (target.kind === "park" ? onOpenPark : onOpenDiagnostic)(target.id);
215
+ }
216
+ else {
217
+ const row = model.activity[effectiveActivityIndex];
218
+ if (row)
219
+ onOpenEvidence(row.evidence);
220
+ }
221
+ }
222
+ }, { isActive: focused });
223
+ if (!model.hasRun) {
224
+ return (_jsxs(Panel, { title: "HOME", width: width, children: [_jsx(BodyText, { children: "No run recorded yet." }), _jsx(BodyText, { children: "tickmarkr fleet \u2014 review installed models" }), _jsx(BodyText, { children: "tickmarkr plan \u2014 preview routing for a spec" })] }));
225
+ }
226
+ const mergedValue = model.noPlan ? "no plan" : `${model.merged} / ${fieldReading(model.planned)}`;
227
+ const gatePassRateValue = model.gatePassRate === undefined ? "no history" : `${model.gatePassRate}%`;
228
+ const spendValue = model.spend.measurable ? model.spend.display ?? ABSENT_FIELD : "not measurable";
229
+ const visibleActivity = model.activity.slice(safeOffset, safeOffset + HOME_ACTIVITY_VISIBLE_ROWS);
230
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(BodyText, { emphasis: "strong", children: ["HOME / ", model.lifecycle] }), _jsxs(CockpitGrid, { children: [_jsxs(Tile, { label: "MERGED", children: [_jsx(BodyText, { emphasis: "strong", children: mergedValue }), hasTrend(model.trend)
231
+ ? _jsx(Sparkline, { samples: model.trend })
232
+ : _jsx(BodyText, { emphasis: "dim", children: "no history" })] }), _jsx(Tile, { label: "GATES RAN (historical)", children: _jsxs(BodyText, { emphasis: "strong", children: [model.gatesRan.passed, " / ", model.gatesRan.total] }) }), _jsx(Tile, { label: "GATE PASS RATE (historical)", children: _jsx(BodyText, { emphasis: "strong", children: gatePassRateValue }) })] }), _jsxs(CockpitGrid, { children: [_jsxs(Tile, { label: "ACTIVE SEATS", children: [_jsxs(BodyText, { emphasis: "strong", children: [model.seats.active, " active"] }), _jsxs(BodyText, { emphasis: "dim", children: [model.seats.eligible, " eligible channels"] })] }), _jsxs(Tile, { label: "NEEDS YOU", children: [_jsxs(BodyText, { emphasis: effectiveSection === "needsYou" ? "strong" : "normal", children: [model.humanCount, " parks; ", model.blockedCount, " blocked"] }), selectedNeedsYou && _jsx(BodyText, { emphasis: "dim", children: selectedNeedsYou.label })] }), _jsx(Tile, { label: "SPEND", children: _jsx(BodyText, { emphasis: "strong", children: spendValue }) })] }), _jsxs(Panel, { title: "PROGRESS", children: [model.progressPercent === undefined
233
+ ? _jsx(BodyText, { children: model.progressCaption })
234
+ : (_jsxs(_Fragment, { children: [_jsx(ProgressMeter, { value: model.progressPercent }), _jsx(BodyText, { emphasis: "dim", children: model.progressCaption })] })), _jsxs(BodyText, { children: ["CURRENT TIP: ", model.currentTip.toUpperCase()] })] }), _jsxs(Panel, { title: "ACTIVITY", focused: effectiveSection === "activity", children: [visibleActivity.length === 0
235
+ ? _jsx(BodyText, { emphasis: "dim", children: "no history" })
236
+ : visibleActivity.map((row, index) => {
237
+ const isSelected = effectiveSection === "activity" && (safeOffset + index === effectiveActivityIndex);
238
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(BodyText, { emphasis: "dim", children: row.time }), _jsx(BodyText, { children: " " }), _jsx(StateGlyph, { state: row.state }), _jsxs(BodyText, { children: [" ", isSelected ? `${GLYPHS.pointer} ` : "", row.text] })] }, row.evidence.id));
239
+ }), _jsx(BodyText, { emphasis: "dim", children: "PageUp history | Enter opens original journal line" })] })] }));
240
+ }
@@ -19,6 +19,10 @@ export type RunKeyEvent = {
19
19
  readonly escape?: boolean;
20
20
  readonly backspace?: boolean;
21
21
  readonly delete?: boolean;
22
+ readonly pageUp?: boolean;
23
+ readonly pageDown?: boolean;
24
+ readonly shift?: boolean;
25
+ readonly meta?: boolean;
22
26
  readonly ctrl?: boolean;
23
27
  };
24
28
  };
@@ -242,3 +246,124 @@ export declare function dispatchRunSurfaceKey(event: RunKeyEvent, surface: RunSu
242
246
  export declare function resolveSetupKeyBinding(bindings: readonly SetupKeyBinding[], event: RunKeyEvent): SetupKeyBinding | undefined;
243
247
  /** Send a key event through the setup half of the shared surface registry. */
244
248
  export declare function dispatchSetupKey(event: RunKeyEvent, state: SetupInteractionState, bindings?: readonly SetupKeyBinding[]): SetupInteractionState | undefined;
249
+ /** Installed FINAL actions, shared by shell dispatch, help and pointer buttons. */
250
+ export declare const SHELL_BINDINGS: readonly [{
251
+ readonly key: "1";
252
+ readonly label: "Home";
253
+ readonly action: "home";
254
+ }, {
255
+ readonly key: "4";
256
+ readonly label: "Run";
257
+ readonly action: "run";
258
+ }, {
259
+ readonly key: "5";
260
+ readonly label: "Evidence";
261
+ readonly action: "evidence";
262
+ }, {
263
+ readonly key: "Tab";
264
+ readonly label: "Focus";
265
+ readonly action: "focus";
266
+ }, {
267
+ readonly key: "Enter";
268
+ readonly label: "Open";
269
+ readonly action: "open";
270
+ }, {
271
+ readonly key: "a";
272
+ readonly label: "Actions";
273
+ readonly action: "actions";
274
+ }, {
275
+ readonly key: "/";
276
+ readonly label: "Filter";
277
+ readonly action: "filter";
278
+ }, {
279
+ readonly key: "e";
280
+ readonly label: "Export";
281
+ readonly action: "export";
282
+ }, {
283
+ readonly key: "f";
284
+ readonly label: "Follow";
285
+ readonly action: "follow";
286
+ }, {
287
+ readonly key: "PageUp/PageDown";
288
+ readonly label: "Page";
289
+ readonly action: "page";
290
+ }, {
291
+ readonly key: "?";
292
+ readonly label: "Keys";
293
+ readonly action: "help";
294
+ }, {
295
+ readonly key: "q";
296
+ readonly label: "Quit";
297
+ readonly action: "quit";
298
+ }, {
299
+ readonly key: "+";
300
+ readonly label: "Widen shortcuts";
301
+ readonly action: "widen";
302
+ }, {
303
+ readonly key: "-";
304
+ readonly label: "Narrow shortcuts";
305
+ readonly action: "narrow";
306
+ }];
307
+ /** Advertise only installed operations with a target in this view. */
308
+ export declare function shellBindings(context: {
309
+ view: string;
310
+ evidenceSection?: number;
311
+ canOpen?: boolean;
312
+ canResize?: boolean;
313
+ }): ({
314
+ readonly key: "1";
315
+ readonly label: "Home";
316
+ readonly action: "home";
317
+ } | {
318
+ readonly key: "4";
319
+ readonly label: "Run";
320
+ readonly action: "run";
321
+ } | {
322
+ readonly key: "5";
323
+ readonly label: "Evidence";
324
+ readonly action: "evidence";
325
+ } | {
326
+ readonly key: "Tab";
327
+ readonly label: "Focus";
328
+ readonly action: "focus";
329
+ } | {
330
+ readonly key: "Enter";
331
+ readonly label: "Open";
332
+ readonly action: "open";
333
+ } | {
334
+ readonly key: "a";
335
+ readonly label: "Actions";
336
+ readonly action: "actions";
337
+ } | {
338
+ readonly key: "/";
339
+ readonly label: "Filter";
340
+ readonly action: "filter";
341
+ } | {
342
+ readonly key: "e";
343
+ readonly label: "Export";
344
+ readonly action: "export";
345
+ } | {
346
+ readonly key: "f";
347
+ readonly label: "Follow";
348
+ readonly action: "follow";
349
+ } | {
350
+ readonly key: "PageUp/PageDown";
351
+ readonly label: "Page";
352
+ readonly action: "page";
353
+ } | {
354
+ readonly key: "?";
355
+ readonly label: "Keys";
356
+ readonly action: "help";
357
+ } | {
358
+ readonly key: "q";
359
+ readonly label: "Quit";
360
+ readonly action: "quit";
361
+ } | {
362
+ readonly key: "+";
363
+ readonly label: "Widen shortcuts";
364
+ readonly action: "widen";
365
+ } | {
366
+ readonly key: "-";
367
+ readonly label: "Narrow shortcuts";
368
+ readonly action: "narrow";
369
+ })[];