tickmarkr 2.4.1 → 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 (75) hide show
  1. package/README.md +110 -24
  2. package/dist/adapters/claude-code.js +7 -2
  3. package/dist/adapters/codex.d.ts +1 -0
  4. package/dist/adapters/codex.js +66 -5
  5. package/dist/adapters/types.d.ts +4 -0
  6. package/dist/adapters/types.js +33 -0
  7. package/dist/cli/commands/approve.d.ts +42 -0
  8. package/dist/cli/commands/approve.js +80 -13
  9. package/dist/cli/commands/doctor.d.ts +234 -0
  10. package/dist/cli/commands/doctor.js +139 -5
  11. package/dist/cli/commands/report.js +15 -44
  12. package/dist/cli/commands/scope.js +36 -6
  13. package/dist/cli/commands/stats.d.ts +2 -0
  14. package/dist/cli/commands/stats.js +42 -23
  15. package/dist/cli/commands/status.js +20 -4
  16. package/dist/cli/commands/ui.js +42 -53
  17. package/dist/cli/commands/unlock.d.ts +1 -1
  18. package/dist/cli/commands/unlock.js +59 -9
  19. package/dist/cli/help.d.ts +219 -0
  20. package/dist/cli/help.js +212 -0
  21. package/dist/cli/index.d.ts +42 -2
  22. package/dist/cli/index.js +23 -8
  23. package/dist/drivers/herdr.d.ts +7 -2
  24. package/dist/drivers/herdr.js +78 -48
  25. package/dist/drivers/orca.d.ts +3 -2
  26. package/dist/drivers/orca.js +43 -4
  27. package/dist/drivers/subprocess.d.ts +1 -0
  28. package/dist/drivers/subprocess.js +3 -0
  29. package/dist/drivers/types.d.ts +14 -0
  30. package/dist/gates/artifact-manifest.d.ts +50 -0
  31. package/dist/gates/artifact-manifest.js +23 -0
  32. package/dist/plan/scope.d.ts +25 -0
  33. package/dist/plan/scope.js +92 -12
  34. package/dist/report/operator-record.d.ts +49 -0
  35. package/dist/report/operator-record.js +137 -0
  36. package/dist/run/daemon.d.ts +3 -4
  37. package/dist/run/daemon.js +18 -10
  38. package/dist/run/lock.d.ts +59 -2
  39. package/dist/run/lock.js +184 -26
  40. package/dist/run/operator-state.d.ts +86 -0
  41. package/dist/run/operator-state.js +165 -0
  42. package/dist/run/supervision.d.ts +32 -0
  43. package/dist/run/supervision.js +138 -17
  44. package/dist/tui/cockpit/capture.d.ts +19 -0
  45. package/dist/tui/cockpit/capture.js +89 -1
  46. package/dist/tui/cockpit/components.d.ts +15 -1
  47. package/dist/tui/cockpit/components.js +79 -9
  48. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  49. package/dist/tui/cockpit/decision-actions.js +315 -0
  50. package/dist/tui/cockpit/derive.d.ts +1 -1
  51. package/dist/tui/cockpit/derive.js +2 -0
  52. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  53. package/dist/tui/cockpit/evidence-view.js +210 -0
  54. package/dist/tui/cockpit/home-view.d.ts +88 -0
  55. package/dist/tui/cockpit/home-view.js +240 -0
  56. package/dist/tui/cockpit/keys.d.ts +125 -0
  57. package/dist/tui/cockpit/keys.js +31 -0
  58. package/dist/tui/cockpit/layout.d.ts +14 -0
  59. package/dist/tui/cockpit/layout.js +15 -0
  60. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  61. package/dist/tui/cockpit/live-runtime.js +683 -0
  62. package/dist/tui/cockpit/live-store.d.ts +289 -0
  63. package/dist/tui/cockpit/live-store.js +308 -0
  64. package/dist/tui/cockpit/live.d.ts +21 -1
  65. package/dist/tui/cockpit/live.js +12 -1
  66. package/dist/tui/cockpit/run-view.d.ts +100 -0
  67. package/dist/tui/cockpit/run-view.js +202 -0
  68. package/dist/tui/cockpit/shell.d.ts +50 -0
  69. package/dist/tui/cockpit/shell.js +74 -0
  70. package/dist/tui/cockpit/theme.d.ts +27 -0
  71. package/dist/tui/cockpit/theme.js +21 -0
  72. package/package.json +1 -1
  73. package/skills/tickmarkr-auto/SKILL.md +10 -1
  74. package/skills/tickmarkr-loop/SKILL.md +72 -1
  75. package/skills/tickmarkr-overseer/SKILL.md +12 -0
@@ -0,0 +1,683 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { existsSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { renderMarkdownRecord } from "../../cli/commands/report.js";
5
+ import { loadConfig } from "../../config/config.js";
6
+ import { estimateCosts } from "../../report/cost.js";
7
+ import { PerformanceObserver, performance } from "node:perf_hooks";
8
+ import { render, useInput, useStdin } from "ink";
9
+ import { useLayoutEffect, useSyncExternalStore } from "react";
10
+ import { createLiveStore } from "./live-store.js";
11
+ import { HomeView, deriveHomeView, selectNeedsYouTarget } from "./home-view.js";
12
+ import { RunView, applyRunViewKey, initialRunViewSession, runGateCells, evidenceLookup, runTaskRow, GATE_CELL_LETTERS } from "./run-view.js";
13
+ import { EvidenceView, deriveEvidenceView, defaultExportPath, planEvidenceExport, writeEvidenceExport } from "./evidence-view.js";
14
+ import { ShellGridFlow, ShellJournalSelection } from "./components.js";
15
+ import { CockpitShell, initialShellState } from "./shell.js";
16
+ import { planShell } from "./layout.js";
17
+ import { shellBindings } from "./keys.js";
18
+ import { createPointerReportReader, POINTER_TRACKING_ON, POINTER_TRACKING_OFF } from "./pointer.js";
19
+ import { deriveRunDecisions, previewDecision, executeDecision, withDecisionPreview, withDecisionReceipt, decisionConfirmLines } from "./decision-actions.js";
20
+ import { approvalRunOwner } from "../../cli/commands/approve.js";
21
+ import { GATE_NAMES } from "../../graph/schema.js";
22
+ import { GLYPHS } from "../../brand.js";
23
+ import { Journal } from "../../run/journal.js";
24
+ import { observeNamedRun } from "../../run/supervision.js";
25
+ import { formatOwnedName } from "../../drivers/types.js";
26
+ import { cellWidth, fitCells, wrapCells } from "./width.js";
27
+ import { resolveShellColourMode } from "./theme.js";
28
+ let timelineUsers = 0;
29
+ let timelineObserver;
30
+ /** React 19 development measures retain fiber details in Node's global timeline.
31
+ * Drain only React DevTools entries, sharing one observer across independent mounts.
32
+ * No environment mutation, global performance monkey patch, or retained frame archive. */
33
+ export function borrowRuntimeTimeline() {
34
+ if (timelineUsers++ === 0) {
35
+ timelineObserver = new PerformanceObserver(list => {
36
+ for (const entry of list.getEntries()) {
37
+ const detail = entry.detail;
38
+ if (detail?.devtools)
39
+ performance.clearMeasures(entry.name);
40
+ }
41
+ });
42
+ timelineObserver.observe({ entryTypes: ["measure"] });
43
+ }
44
+ let released = false;
45
+ return () => {
46
+ if (released)
47
+ return;
48
+ released = true;
49
+ if (--timelineUsers === 0) {
50
+ for (const entry of performance.getEntriesByType("measure")) {
51
+ if (entry.detail?.devtools)
52
+ performance.clearMeasures(entry.name);
53
+ }
54
+ timelineObserver?.disconnect();
55
+ timelineObserver = undefined;
56
+ }
57
+ };
58
+ }
59
+ export const SHELL_TERMINAL_ENTER = "\x1b[?1049h\x1b[22;0t\x1b]0;tickmarkr cockpit\x07" + POINTER_TRACKING_ON;
60
+ export const SHELL_TERMINAL_RESTORE = POINTER_TRACKING_OFF + "\x1b[23;0t\x1b[?25h\x1b[?1049l";
61
+ /** Preserve a policy-disabled gate from C2 even when C5's generic event
62
+ * mapper calls an unpassed result unknown. Lead with the gate name so a long
63
+ * task identity cannot clip the label that explains the disabled glyph. */
64
+ function presentEvidenceRow(row, event) {
65
+ if (event?.event !== "gate-result" || event.data.disabled !== true)
66
+ return row;
67
+ return { ...row, state: "inactive", text: `${event.data.gate ?? "gate"} — ${row.text}` };
68
+ }
69
+ /** The live entry mounts the dependency-owned leaves, using only C2 snapshots/history/page. */
70
+ export async function runConsolidatedCockpit(options) {
71
+ const { input, output, cwd, runId } = options;
72
+ const colour = resolveShellColourMode(options.environment);
73
+ const releaseTimeline = borrowRuntimeTimeline();
74
+ const wasRaw = input.isRaw ?? false;
75
+ const wasPaused = input.isPaused();
76
+ let store;
77
+ let observation;
78
+ let app;
79
+ let timer;
80
+ let entered = false;
81
+ let done = false;
82
+ let failure;
83
+ let finish;
84
+ const stopped = new Promise(resolve => { finish = resolve; });
85
+ const stop = (error) => { if (done)
86
+ return; done = true; failure = error; finish(); };
87
+ const signal = () => stop();
88
+ const ioError = (error) => stop(error);
89
+ let resize = () => { };
90
+ try {
91
+ process.on("SIGTERM", signal);
92
+ process.on("SIGINT", signal);
93
+ input.on("error", ioError);
94
+ output.on("error", ioError);
95
+ if (output.isTTY) {
96
+ entered = true;
97
+ output.write(SHELL_TERMINAL_ENTER);
98
+ }
99
+ store = createLiveStore({ cwd, runId, now: options.now });
100
+ const source = store;
101
+ if (options.observeRun !== false)
102
+ observation = observeNamedRun(cwd, runId, options.environment);
103
+ let state = { ...initialShellState(), view: options.initialView ?? "home" };
104
+ let runSession = initialRunViewSession();
105
+ if (options.initialParks) {
106
+ runSession = { ...runSession, selection: Math.max(0, source.snapshot().operator.tasks.findIndex(t => t.state === "human" || t.state === "blocked")) };
107
+ state = { ...state, scroll: Number.MAX_SAFE_INTEGER };
108
+ }
109
+ let decisionsKey = "";
110
+ let visibleDecisions = [];
111
+ let selectedEvidence;
112
+ let evidenceNavigation = 0;
113
+ let activeEvidence;
114
+ const reportEvidenceSelection = (id) => {
115
+ if (id !== activeEvidence) {
116
+ activeEvidence = id;
117
+ publish();
118
+ }
119
+ };
120
+ let homeNeedsYouIndex = 0;
121
+ const reportHomeNeedsYouSelection = (_target, index) => {
122
+ if (index !== homeNeedsYouIndex) {
123
+ homeNeedsYouIndex = index;
124
+ publish();
125
+ }
126
+ };
127
+ let dragColumn;
128
+ let committed;
129
+ let committedTargets = [];
130
+ let revision = 0;
131
+ const listeners = new Set();
132
+ const publish = () => { revision++; for (const listener of listeners)
133
+ listener(); };
134
+ const unsubscribe = source.subscribe(publish);
135
+ const geometry = () => planShell(output.columns ?? 80, output.rows ?? 24, state.shortcutColumns);
136
+ const openEvidence = (identity) => {
137
+ evidenceNavigation++;
138
+ selectedEvidence = identity;
139
+ state = { ...state, view: "evidence", scroll: 0, evidenceSection: 0 };
140
+ publish();
141
+ };
142
+ let exportPath;
143
+ const beginExport = () => {
144
+ const paths = source.snapshot().graph.value?.spec.paths ?? [];
145
+ const destination = defaultExportPath({ runId, specPaths: paths,
146
+ specDir: paths.length === 1 ? dirname(resolve(cwd, paths[0])) : undefined });
147
+ state = { ...state, editor: destination ?? "", editorKind: "export" };
148
+ publish();
149
+ };
150
+ const focusTask = async () => {
151
+ const task = source.snapshot().operator.tasks[runSession.selection];
152
+ if (!task)
153
+ return;
154
+ const launch = Journal.open(cwd, runId).read().reverse().find(row => row.event === "worker-launch" && row.taskId === task.id && row.data.attempt === task.attempt);
155
+ const data = launch?.data;
156
+ const slot = data?.slot;
157
+ let result = { status: "unsupported", reason: "No recorded pane ownership; open task evidence with Enter" };
158
+ if (slot && typeof data?.driver === "string" && typeof slot.id === "string" && typeof slot.cwd === "string" && task.attempt !== undefined &&
159
+ slot.name === formatOwnedName({ role: "worker", taskId: task.id, attempt: task.attempt, runId })) {
160
+ const target = { repo: cwd, runId, taskId: task.id, attempt: task.attempt, slot, workspace: typeof data.workspace === "string" ? data.workspace : undefined };
161
+ if (options.focusDriver)
162
+ result = await options.focusDriver(target, data.driver);
163
+ else {
164
+ const { pickDriver } = await import("../../drivers/index.js");
165
+ const { loadConfig } = await import("../../config/config.js");
166
+ const driver = pickDriver(loadConfig(cwd), data.driver);
167
+ result = await driver.focus?.(target) ?? result;
168
+ }
169
+ }
170
+ if (done)
171
+ return;
172
+ state = { ...state, overlay: [result.status === "focused" ? `Focused ${task.id}` : `Pane ${result.status}`, result.reason, "Enter opens task evidence after Esc"], overlayOffset: 0 };
173
+ publish();
174
+ };
175
+ const openDetail = () => {
176
+ const snap = source.snapshot();
177
+ const task = state.view === "run" ? snap.operator.tasks[runSession.selection] : undefined;
178
+ const identity = state.view === "run" ? task?.gates.review.evidence ?? task?.mergeEvidence : selectedEvidence ?? snap.journal.history.at(-1);
179
+ if (!identity) {
180
+ if (task)
181
+ state = { ...state, overlay: [task.id, task.title ?? "", task.path ?? "path unknown", "No review evidence"].flatMap(t => wrapCells(t, geometry().bodyColumns)), overlayOffset: 0 };
182
+ return;
183
+ }
184
+ const row = source.page(identity.line, 1)[0];
185
+ state = { ...state, overlay: [identity.id, ...(task ? [task.id, task.title ?? ""].flatMap(t => wrapCells(t, geometry().bodyColumns)) : []), ...wrapCells(row?.event ? JSON.stringify(row.event, null, 2) : row?.error ?? "missing evidence", geometry().bodyColumns), ...(typeof row?.event?.data.details === "string" ? row.event.data.details.split("\n").flatMap(t => wrapCells(t, geometry().bodyColumns)) : [])], overlayOffset: 0 };
186
+ };
187
+ // The mounted leaves own their selection. Forward synthetic registry actions
188
+ // through the same Ink input bus as terminal keys instead of guessing a row
189
+ // from the snapshot. The guard prevents the shell from dispatching its echo.
190
+ let leafInput;
191
+ let forwarding = false;
192
+ const forwardLeaf = (bytes) => {
193
+ forwarding = true;
194
+ try {
195
+ leafInput?.emit("input", bytes);
196
+ }
197
+ finally {
198
+ forwarding = false;
199
+ }
200
+ };
201
+ const key = (event, terminal = false) => {
202
+ if (done)
203
+ return false;
204
+ const { input: text, key: k } = event;
205
+ if (k.ctrl && text === "c") {
206
+ stop();
207
+ return true;
208
+ }
209
+ if (exportPath !== undefined) {
210
+ if (text === "y") {
211
+ const j = Journal.open(cwd, runId);
212
+ const events = j.read();
213
+ const rows = j.readTelemetry();
214
+ const cfg = loadConfig(cwd);
215
+ const record = renderMarkdownRecord(runId, events, estimateCosts(rows, cfg.cost), rows);
216
+ const receipt = writeEvidenceExport(exportPath, record, { existsSync, writeFileSync, renameSync, unlinkSync, statSync }, true);
217
+ state = { ...state, overlay: [receipt.ok ? `Exported ${receipt.path} (${receipt.bytes} bytes)` : receipt.reason], overlayOffset: 0 };
218
+ exportPath = undefined;
219
+ }
220
+ else if (text === "n" || k.escape) {
221
+ exportPath = undefined;
222
+ state = { ...state, overlay: undefined };
223
+ }
224
+ publish();
225
+ return true;
226
+ }
227
+ if (state.reviewing) {
228
+ if (text === "y") {
229
+ state = { ...state, staged: [], reviewing: false, quit: true };
230
+ stop();
231
+ }
232
+ else if (text === "n" || k.escape)
233
+ state = { ...state, reviewing: false, overlay: undefined };
234
+ publish();
235
+ return true;
236
+ }
237
+ if (state.editor !== undefined) {
238
+ if (k.escape)
239
+ state = { ...state, editor: undefined };
240
+ else if (k.return) {
241
+ if (state.editorKind === "export") {
242
+ if (state.editor.trim()) {
243
+ const plan = planEvidenceExport(resolve(cwd, state.editor), { existsSync });
244
+ exportPath = plan.path;
245
+ state = { ...state, editor: undefined, overlay: [`${plan.exists ? "Overwrite" : "Export to"} ${plan.path}`, "y Confirm | n/Esc Cancel"], overlayOffset: 0 };
246
+ }
247
+ }
248
+ else {
249
+ selectedEvidence = undefined;
250
+ state = { ...state, query: state.editor, editor: undefined, scroll: 0 };
251
+ }
252
+ }
253
+ else if (k.backspace || k.delete)
254
+ state = { ...state, editor: [...state.editor].slice(0, -1).join("") };
255
+ else if (!k.ctrl && !k.meta && !/[\x00-\x1f\x7f]/.test(text))
256
+ state = { ...state, editor: state.editor + text };
257
+ publish();
258
+ return true;
259
+ }
260
+ if (runSession.decisions.confirming || runSession.decisions.menu) {
261
+ if (k.pageUp || k.pageDown) {
262
+ state = { ...state, overlayOffset: Math.max(0, state.overlayOffset + (k.pageDown ? 1 : -1) * geometry().bodyRows) };
263
+ publish();
264
+ return true;
265
+ }
266
+ const snap = source.snapshot();
267
+ const decisions = deriveRunDecisions(Journal.open(cwd, runId), snap.graph.value);
268
+ const next = applyRunViewKey(runSession, event, { tasks: snap.operator.tasks, decisions, verdictLines: 0 });
269
+ runSession = next.session;
270
+ if (runSession.decisions.menu)
271
+ state = { ...state, overlay: ["Actions — Enter reviews; Esc cancels", ...runSession.decisions.menu.verbs.map((v, i) => `${i === runSession.decisions.menu.selection ? ">" : " "} ${v}`)], overlayOffset: 0 };
272
+ if (next.open) {
273
+ runSession = { ...runSession, decisions: withDecisionPreview(runSession.decisions, previewDecision(next.open, { cwd, runId, by: "operator" })) };
274
+ const preview = runSession.decisions.confirming;
275
+ if (preview)
276
+ state = { ...state, overlay: decisionConfirmLines(preview).flatMap(t => wrapCells(t, geometry().bodyColumns)), overlayOffset: 0 };
277
+ }
278
+ if (next.confirm) {
279
+ void executeDecision(next.confirm, { cwd }).then(receipt => {
280
+ if (done)
281
+ return;
282
+ runSession = { ...runSession, decisions: withDecisionReceipt(runSession.decisions, receipt) };
283
+ state = { ...state, overlay: [receipt.ok ? "Decision recorded; refresh reads its journal append" : receipt.refusal], overlayOffset: 0 };
284
+ source.refresh();
285
+ }).catch(stop);
286
+ }
287
+ else if (!runSession.decisions.confirming && !runSession.decisions.menu)
288
+ state = { ...state, overlay: undefined };
289
+ publish();
290
+ return true;
291
+ }
292
+ if (k.escape) {
293
+ state = state.overlay ? { ...state, overlay: undefined } : state.help ? { ...state, help: false } : { ...state, query: "", scroll: 0 };
294
+ }
295
+ else if (text === "q") {
296
+ if (state.staged.length)
297
+ state = { ...state, reviewing: true, overlay: ["Review staged edits before quitting", ...state.staged, "y Discard staged edits and quit | n/Esc Cancel"], overlayOffset: 0 };
298
+ else {
299
+ state = { ...state, quit: true };
300
+ stop();
301
+ }
302
+ }
303
+ else if (state.overlay || state.help) {
304
+ if (text === "?")
305
+ state = { ...state, help: false };
306
+ if (k.pageDown || k.pageUp)
307
+ state = { ...state, overlayOffset: Math.max(0, state.overlayOffset + (k.pageDown ? 1 : -1) * geometry().bodyRows) };
308
+ }
309
+ else if (k.tab) {
310
+ const focus = geometry().focus;
311
+ state = { ...state, focus: focus[(Math.max(0, focus.indexOf(state.focus)) + (k.shift ? focus.length - 1 : 1)) % focus.length] };
312
+ }
313
+ else {
314
+ const action = shellBindings(state).find(b => b.key === text)?.action;
315
+ if (action === "home" || action === "run" || action === "evidence")
316
+ state = { ...state, view: action, scroll: 0, evidenceSection: action === state.view ? state.evidenceSection : 0 };
317
+ else if (action === "widen" || action === "narrow")
318
+ state = { ...state, shortcutColumns: planShell(geometry().columns, geometry().rows, geometry().shortcuts + (action === "widen" ? 1 : -1)).shortcuts };
319
+ else if (action === "help")
320
+ state = { ...state, help: true, overlayOffset: 0 };
321
+ else if (action === "filter")
322
+ state = { ...state, editor: state.query, editorKind: "filter" };
323
+ else if (action === "export") {
324
+ if (!terminal)
325
+ forwardLeaf("e");
326
+ }
327
+ else if (state.view === "evidence" && (k.leftArrow || k.rightArrow))
328
+ state = { ...state, evidenceSection: ((state.evidenceSection ?? 0) + (k.rightArrow ? 1 : 4)) % 5 };
329
+ else if ((k.pageDown || k.pageUp) && state.view !== "run")
330
+ state = { ...state, scroll: Math.max(0, state.scroll + (k.pageDown ? 1 : -1) * geometry().bodyRows) };
331
+ else if (k.return || action === "open") {
332
+ if (state.view === "run")
333
+ openDetail();
334
+ else if (!terminal && (state.view === "home" || !state.evidenceSection))
335
+ forwardLeaf("\r");
336
+ }
337
+ else if (state.view === "run") {
338
+ if (text === "o") {
339
+ void focusTask().catch(error => { if (!done) {
340
+ state = { ...state, overlay: [String(error), "Pane focus refused; Enter opens task evidence after Esc"], overlayOffset: 0 };
341
+ publish();
342
+ } });
343
+ return true;
344
+ }
345
+ const snap = source.snapshot();
346
+ const decisions = text === "a" ? deriveRunDecisions(Journal.open(cwd, runId), snap.graph.value) : [];
347
+ const task = snap.operator.tasks[runSession.selection];
348
+ const cells = task ? runGateCells(task, evidenceLookup(snap.journal.history, source.page)) : [];
349
+ runSession = applyRunViewKey(runSession, event, { tasks: snap.operator.tasks, decisions, verdictLines: cells[runSession.verdictGate]?.verdict.length ?? 0 }).session;
350
+ if (k.pageDown || k.pageUp)
351
+ state = {
352
+ ...state,
353
+ // The Run leaf owns verdict paging; the shell only moves its
354
+ // viewport far enough for that leaf's verdict panel to be seen.
355
+ scroll: k.pageDown ? geometry().bodyRows : 0,
356
+ };
357
+ if (runSession.decisions.menu)
358
+ state = { ...state, overlay: ["Actions — Enter reviews; Esc cancels", ...runSession.decisions.menu.verbs], overlayOffset: 0 };
359
+ else if (text === "a")
360
+ state = { ...state, overlay: ["No actionable park selected", "tickmarkr resume " + runId], overlayOffset: 0 };
361
+ }
362
+ else if (text === "a")
363
+ state = { ...state, overlay: ["Existing CLI actions", "tickmarkr fleet", "tickmarkr plan", "tickmarkr doctor"], overlayOffset: 0 };
364
+ }
365
+ if (!terminal && state.view !== "run" && !state.overlay && !state.help && state.editor === undefined) {
366
+ const bytes = k.upArrow ? "\x1b[A" : k.downArrow ? "\x1b[B" : k.rightArrow ? "\x1b[C" : k.leftArrow ? "\x1b[D" : k.pageUp ? "\x1b[5~" : k.pageDown ? "\x1b[6~" : text === "f" ? "f" : undefined;
367
+ if (bytes)
368
+ forwardLeaf(bytes);
369
+ }
370
+ source.input();
371
+ return true;
372
+ };
373
+ const pointer = (report) => {
374
+ const p = committed;
375
+ if (!p || p.refused || report.column < 0 || report.column >= p.columns || report.row < 0 || report.row >= p.rows)
376
+ return false;
377
+ if (dragColumn !== undefined) {
378
+ if (report.action === "release" || (report.button !== undefined && (report.button & 3) === 3)) {
379
+ dragColumn = undefined;
380
+ return true;
381
+ }
382
+ if (report.action === "move") {
383
+ const delta = report.column - dragColumn;
384
+ dragColumn = report.column;
385
+ for (let steps = Math.abs(delta); steps > 0; steps--)
386
+ key({ input: delta > 0 ? "-" : "+", key: {} });
387
+ return true;
388
+ }
389
+ }
390
+ if (report.action === "wheel-down" || report.action === "wheel-up")
391
+ return key({ input: "", key: { pageDown: report.action === "wheel-down", pageUp: report.action === "wheel-up" } });
392
+ if (report.action !== "press")
393
+ return false;
394
+ if (state.editor !== undefined || state.overlay || state.help)
395
+ return false;
396
+ const target = committedTargets.find(target => report.column >= target.column && report.column < target.column + target.columns
397
+ && report.row >= target.row && report.row < target.row + target.rows);
398
+ if (target?.kind === "resize") {
399
+ dragColumn = report.column;
400
+ return true;
401
+ }
402
+ if (target?.kind === "key") {
403
+ const binding = target.binding;
404
+ return key({ input: binding.key, key: { tab: binding.key === "Tab", return: binding.key === "Enter", pageDown: binding.action === "page" } });
405
+ }
406
+ if (report.row < p.bodyRow || report.row >= p.bodyRow + p.bodyRows || report.column < p.bodyColumn || report.column >= p.bodyColumn + p.bodyColumns)
407
+ return false;
408
+ state = { ...state, focus: "content" };
409
+ if (target?.kind === "task" || target?.kind === "park") {
410
+ const index = source.snapshot().operator.tasks.findIndex(task => task.id === target.id);
411
+ if (index >= 0) {
412
+ // A key may have changed the pending view since this row painted.
413
+ // Execute the clicked row's context, not that uncommitted view.
414
+ state = { ...state, view: "run", scroll: target.kind === "park" ? 0 : p.scroll };
415
+ const direction = index > runSession.selection ? "downArrow" : "upArrow";
416
+ for (let steps = Math.abs(index - runSession.selection); steps > 0; steps--)
417
+ key({ input: "", key: { [direction]: true } });
418
+ }
419
+ }
420
+ else if (target?.kind === "diagnostic") {
421
+ state = { ...state, overlay: [target.id], overlayOffset: 0 };
422
+ }
423
+ else if (target?.kind === "evidence") {
424
+ if (target.open)
425
+ openEvidence(target.identity);
426
+ else {
427
+ if (state.view === "evidence")
428
+ for (let section = state.evidenceSection ?? 0; section > 0; section--)
429
+ forwardLeaf("\x1b[D");
430
+ // The leaf may have moved with arrows since this identity was last
431
+ // requested. Every click is a new navigation, even to the same #L.
432
+ evidenceNavigation++;
433
+ selectedEvidence = target.identity;
434
+ state = { ...state, view: "evidence", scroll: p.scroll, evidenceSection: 0 };
435
+ }
436
+ }
437
+ publish();
438
+ return true;
439
+ };
440
+ const delivery = { geometry: () => committed, snapshot: () => ({ state, store: source.snapshot(), interaction: state }), key, pointer, refresh: () => {
441
+ if (done)
442
+ return false;
443
+ try {
444
+ if (observation?.stopRequested()) {
445
+ stop();
446
+ return false;
447
+ }
448
+ const snap = source.refresh();
449
+ if (options.observeRun !== false && snap.journal.status === "unreadable")
450
+ stop(new Error(snap.journal.error?.error ?? "journal unreadable"));
451
+ return true;
452
+ }
453
+ catch (e) {
454
+ stop(e);
455
+ return false;
456
+ }
457
+ }, diagnostics: source.diagnostics, stage: edits => { state = { ...state, staged: [...edits] }; publish(); } };
458
+ options.onDelivery?.(delivery);
459
+ options.onShellDelivery?.(delivery);
460
+ resize = () => {
461
+ const p = geometry();
462
+ if (!p.focus.includes(state.focus))
463
+ state = { ...state, focus: "content" };
464
+ source.resize(p.columns, p.rows);
465
+ };
466
+ output.on("resize", resize);
467
+ const reader = createPointerReportReader();
468
+ const stdin = new Proxy(input, { get(target, property) {
469
+ if (property === "read")
470
+ return (...args) => {
471
+ try {
472
+ const chunk = target.read(...args);
473
+ if (chunk == null)
474
+ return null;
475
+ const result = reader(String(chunk));
476
+ let keys = "";
477
+ for (const token of result.tokens) {
478
+ if (token.type === "pointer")
479
+ pointer(token.report);
480
+ else
481
+ keys += token.bytes;
482
+ }
483
+ // A standalone Escape is unambiguous after Ink's own input grace.
484
+ if (reader.pending() === "\x1b")
485
+ for (const token of reader.flush().tokens)
486
+ if (token.type === "keys")
487
+ keys += token.bytes;
488
+ return keys || null;
489
+ }
490
+ catch (error) {
491
+ stop(error);
492
+ return null;
493
+ }
494
+ };
495
+ const value = Reflect.get(target, property);
496
+ return typeof value === "function" ? value.bind(target) : value;
497
+ } });
498
+ function App() {
499
+ useSyncExternalStore(listener => { listeners.add(listener); return () => { listeners.delete(listener); }; }, () => revision);
500
+ const stdinContext = useStdin();
501
+ useLayoutEffect(() => {
502
+ leafInput = stdinContext.internal_eventEmitter;
503
+ return () => { leafInput = undefined; };
504
+ }, [stdinContext.internal_eventEmitter]);
505
+ const renderedView = state.view;
506
+ const leafActive = state.editor === undefined && !state.overlay && !state.help && !state.reviewing;
507
+ useInput((text, k) => {
508
+ if (forwarding || (leafActive && renderedView !== "run" && (k.return || (renderedView === "evidence" && text === "e"))))
509
+ return;
510
+ try {
511
+ key({ input: text, key: k }, true);
512
+ }
513
+ catch (error) {
514
+ stop(error);
515
+ }
516
+ });
517
+ const snap = source.snapshot();
518
+ const p = geometry();
519
+ const nextDecisionsKey = `${snap.journal.generation}:${snap.journal.offset}:${snap.graph.identity}`;
520
+ if (nextDecisionsKey !== decisionsKey) {
521
+ decisionsKey = nextDecisionsKey;
522
+ visibleDecisions = snap.operator.tasks.some(t => t.state === "human") ? deriveRunDecisions(Journal.open(cwd, runId), snap.graph.value) : [];
523
+ }
524
+ state = { ...state, canOpen: state.view === "home"
525
+ ? snap.operator.lifecycle !== "UNKNOWN" && (snap.operator.tasks.some(t => t.state === "human" || t.state === "blocked") || snap.journal.history.some(row => row.event))
526
+ : state.view === "run" ? snap.operator.tasks.length > 0
527
+ : false };
528
+ const model = deriveEvidenceView({ rows: snap.journal.history, source: snap.journal.source });
529
+ const journal = model.journal.map(item => {
530
+ const row = snap.journal.history.find(r => r.line === item.evidence.line);
531
+ return presentEvidenceRow(item, row?.event);
532
+ });
533
+ const homeActivity = journal.slice().reverse().map(row => ({ ...row, text: `#L${row.evidence.line} ${row.text}` }));
534
+ let evidenceRows = journal;
535
+ if (selectedEvidence && !journal.some(row => row.evidence.id === selectedEvidence.id)) {
536
+ const row = source.page(selectedEvidence.line, 1)[0];
537
+ if (row?.event)
538
+ evidenceRows = [...deriveEvidenceView({ rows: [row], source: snap.journal.source }).journal.map(item => presentEvidenceRow({ ...item, evidence: selectedEvidence }, row.event)), ...journal];
539
+ }
540
+ if (state.query)
541
+ evidenceRows = evidenceRows.filter(row => row.text.includes(state.query) || row.fullText.includes(state.query));
542
+ state = { ...state, canResize: p.shortcuts > 0 };
543
+ if (state.view === "evidence")
544
+ state = { ...state, canOpen: evidenceRows.some(row => row.evidence.id === activeEvidence) };
545
+ const bindings = shellBindings(state);
546
+ const mergedTasks = new Set();
547
+ const mergeTrend = [];
548
+ for (const row of snap.journal.history) {
549
+ if (row.event?.event === "merge" && row.event.taskId && !mergedTasks.has(row.event.taskId)) {
550
+ mergedTasks.add(row.event.taskId);
551
+ mergeTrend.push(mergedTasks.size);
552
+ }
553
+ }
554
+ // Commit pointer identities from the very model supplied to Home. Its
555
+ // human-first Needs-you ordering can differ from graph declaration order.
556
+ const homeModel = deriveHomeView({ operator: snap.operator,
557
+ trend: mergeTrend.length > 1 ? mergeTrend : [],
558
+ seats: { active: snap.operator.tasks.filter(t => t.state === "running").length, eligible: 0 },
559
+ activity: homeActivity,
560
+ diagnostics: options.diagnostics });
561
+ const focused = state.editor === undefined && !state.overlay && !state.help;
562
+ return _jsx(CockpitShell, { snapshot: options.observeRun === false ? { ...snap, journal: { ...snap.journal, source: "no run/journal.jsonl" } } : snap, state: state, columns: p.columns, rows: p.rows, version: options.binaryVersion, colour: colour, onCommit: plan => {
563
+ committed = plan;
564
+ const targets = [];
565
+ const taskRows = snap.operator.tasks.map((task, index) => ({
566
+ id: task.id,
567
+ text: fitCells(`${index === Math.min(runSession.selection, snap.operator.tasks.length - 1) ? `${GLYPHS.pointer} ` : " "}${runTaskRow(task, GATE_NAMES.map(gate => ({ letter: GATE_CELL_LETTERS[task.gates[gate].state] })))}`, Math.max(1, plan.bodyColumns - 4)),
568
+ }));
569
+ const usedTasks = new Set();
570
+ for (const row of plan.paintedRows) {
571
+ if (row.row < plan.bodyRow && (row.text.startsWith("1 Home") || row.text.startsWith("1H"))) {
572
+ for (const binding of bindings.filter(binding => ["home", "run", "evidence"].includes(binding.action))) {
573
+ const caption = plan.columns < 60 ? `${binding.key}${binding.label[0]}` : `${binding.key} ${binding.label}`;
574
+ const index = row.text.indexOf(caption);
575
+ if (index >= 0)
576
+ targets.push({ ...row, column: row.column + cellWidth(row.text.slice(0, index)), columns: cellWidth(caption), kind: "key", binding });
577
+ }
578
+ }
579
+ if (row.row >= plan.bodyRow + plan.bodyRows && row.text.includes("q Quit")) {
580
+ for (const binding of bindings) {
581
+ const caption = `${binding.key} ${binding.action === "actions" && plan.columns < 60 ? "Act" : binding.label}`;
582
+ const index = row.text.indexOf(caption);
583
+ if (index >= 0)
584
+ targets.push({ ...row, column: row.column + cellWidth(row.text.slice(0, index)), columns: cellWidth(caption), kind: "key", binding });
585
+ }
586
+ }
587
+ if (row.row < plan.bodyRow || row.row >= plan.bodyRow + plan.bodyRows)
588
+ continue;
589
+ if (plan.shortcuts && row.column === plan.bodyColumn + plan.bodyColumns && row.text === "│") {
590
+ targets.push({ ...row, kind: "resize" });
591
+ continue;
592
+ }
593
+ if (row.column < plan.bodyColumn || row.column >= plan.bodyColumn + plan.bodyColumns) {
594
+ const binding = bindings.find(binding => row.text.trim() === `${binding.key} ${binding.label}`);
595
+ if (binding)
596
+ targets.push({ ...row, kind: "key", binding });
597
+ continue;
598
+ }
599
+ if (state.view === "run") {
600
+ const task = taskRows.find(task => !usedTasks.has(task.id) && row.text === task.text);
601
+ if (task) {
602
+ usedTasks.add(task.id);
603
+ targets.push({ ...row, kind: "task", id: task.id });
604
+ }
605
+ }
606
+ else {
607
+ const cleanText = row.text.trim().startsWith(GLYPHS.pointer)
608
+ ? row.text.trim().slice(GLYPHS.pointer.length).trim()
609
+ : row.text.trim();
610
+ const evidence = state.view === "home"
611
+ ? homeActivity.find(item => row.text.trim() === item.text || cleanText === item.text)
612
+ : evidenceRows.find(item => row.evidenceId === item.evidence.id);
613
+ if (evidence)
614
+ targets.push({ ...row, column: plan.bodyColumn, columns: plan.bodyColumns,
615
+ kind: "evidence", identity: evidence.evidence, open: state.view === "home" });
616
+ if (state.view === "home" && row.text.startsWith("NEEDS YOU")) {
617
+ const target = selectNeedsYouTarget(homeModel, homeNeedsYouIndex);
618
+ if (target?.kind === "park")
619
+ targets.push({ ...row, kind: "park", id: target.id });
620
+ else if (target?.kind === "diagnostic")
621
+ targets.push({ ...row, kind: "diagnostic", id: target.id });
622
+ }
623
+ }
624
+ }
625
+ committedTargets = targets;
626
+ }, children: state.view === "home" ? _jsx(ShellGridFlow, { width: p.bodyColumns, columns: p.columns < 120 ? 2 : 3, rowHeight: p.rows < 20 ? 1 : p.columns < 120 ? 2 : 3, children: _jsx(HomeView, { width: p.bodyColumns, model: homeModel, focused: focused, onOpenPark: id => { runSession = { ...runSession, selection: Math.max(0, snap.operator.tasks.findIndex(t => t.id === id)) }; state = { ...state, view: "run", scroll: 0 }; publish(); }, onOpenDiagnostic: id => { state = { ...state, overlay: [id], overlayOffset: 0 }; publish(); }, onOpenEvidence: openEvidence, onSelectNeedsYou: reportHomeNeedsYouSelection }, String(homeModel.needsYou.length > 0)) }) :
627
+ state.view === "run" ? _jsx(RunView, { snapshot: snap.operator, rows: snap.journal.history, page: source.page, graph: snap.graph.value, decisions: visibleDecisions, session: runSession, columns: p.bodyColumns, run: approvalRunOwner(cwd, runId) }) :
628
+ _jsx(ShellJournalSelection.Provider, { value: reportEvidenceSelection, children: _jsx(EvidenceView, { width: p.bodyColumns, model: { ...model, journal: evidenceRows }, focusEvidence: selectedEvidence, focused: focused, onExport: beginExport, onSelect: id => { selectedEvidence = id; openDetail(); publish(); } }, `${state.query}:${evidenceNavigation}`) }) });
629
+ }
630
+ app = render(_jsx(App, {}), { stdin, stdout: output, debug: options.debug, exitOnCtrlC: false, patchConsole: false });
631
+ void app.waitUntilExit().then(() => stop(), stop);
632
+ timer = setInterval(delivery.refresh, options.refreshMs ?? 1000);
633
+ await stopped;
634
+ unsubscribe();
635
+ listeners.clear();
636
+ }
637
+ catch (error) {
638
+ failure = error;
639
+ }
640
+ finally {
641
+ if (timer)
642
+ clearInterval(timer);
643
+ output.off("resize", resize);
644
+ process.off("SIGTERM", signal);
645
+ process.off("SIGINT", signal);
646
+ try {
647
+ app?.unmount();
648
+ }
649
+ catch (e) {
650
+ failure ??= e;
651
+ }
652
+ store?.dispose();
653
+ try {
654
+ if (input.isTTY)
655
+ input.setRawMode(wasRaw);
656
+ if (wasPaused)
657
+ input.pause();
658
+ else
659
+ input.resume();
660
+ }
661
+ catch (e) {
662
+ failure ??= e;
663
+ }
664
+ try {
665
+ if (entered)
666
+ output.write(SHELL_TERMINAL_RESTORE);
667
+ }
668
+ catch (e) {
669
+ failure ??= e;
670
+ }
671
+ input.off("error", ioError);
672
+ output.off("error", ioError);
673
+ releaseTimeline();
674
+ try {
675
+ observation?.close();
676
+ }
677
+ catch (e) {
678
+ failure ??= e;
679
+ }
680
+ }
681
+ if (failure)
682
+ throw failure;
683
+ }