tickmarkr 1.84.0 → 1.86.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 (87) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters/catalog-remote.d.ts +64 -0
  3. package/dist/adapters/catalog-remote.js +287 -0
  4. package/dist/adapters/catalog.d.ts +96 -0
  5. package/dist/adapters/catalog.js +176 -0
  6. package/dist/adapters/claude-code.d.ts +1 -0
  7. package/dist/adapters/claude-code.js +59 -1
  8. package/dist/adapters/fake.js +42 -4
  9. package/dist/adapters/model-lints.d.ts +25 -5
  10. package/dist/adapters/model-lints.js +184 -50
  11. package/dist/adapters/model-windows.d.ts +31 -0
  12. package/dist/adapters/model-windows.js +69 -0
  13. package/dist/adapters/prompt.d.ts +5 -1
  14. package/dist/adapters/prompt.js +13 -4
  15. package/dist/adapters/registry.d.ts +25 -26
  16. package/dist/adapters/registry.js +173 -110
  17. package/dist/adapters/types.d.ts +3 -0
  18. package/dist/adapters/types.js +36 -3
  19. package/dist/brand.d.ts +5 -1
  20. package/dist/brand.js +18 -2
  21. package/dist/cli/commands/doctor.d.ts +3 -0
  22. package/dist/cli/commands/doctor.js +43 -21
  23. package/dist/cli/commands/fleet.d.ts +7 -0
  24. package/dist/cli/commands/fleet.js +94 -74
  25. package/dist/cli/commands/init.js +118 -5
  26. package/dist/cli/commands/status.js +202 -46
  27. package/dist/compile/collateral.d.ts +86 -2
  28. package/dist/compile/collateral.js +294 -3
  29. package/dist/compile/gsd.d.ts +2 -1
  30. package/dist/compile/gsd.js +68 -2
  31. package/dist/compile/native.d.ts +14 -0
  32. package/dist/compile/native.js +161 -12
  33. package/dist/config/config.d.ts +82 -5
  34. package/dist/config/config.js +253 -66
  35. package/dist/config/fleet-overlay.d.ts +25 -20
  36. package/dist/config/fleet-overlay.js +195 -77
  37. package/dist/config/fleet-why.d.ts +23 -0
  38. package/dist/config/fleet-why.js +42 -0
  39. package/dist/drivers/herdr.d.ts +21 -3
  40. package/dist/drivers/herdr.js +344 -110
  41. package/dist/gates/acceptance.js +7 -2
  42. package/dist/gates/baseline.d.ts +1 -0
  43. package/dist/gates/baseline.js +91 -13
  44. package/dist/gates/llm.d.ts +0 -1
  45. package/dist/gates/llm.js +5 -30
  46. package/dist/gates/review.d.ts +9 -1
  47. package/dist/gates/review.js +105 -10
  48. package/dist/gates/run-gates.d.ts +9 -0
  49. package/dist/gates/run-gates.js +285 -41
  50. package/dist/gates/verdict-cause.d.ts +4 -0
  51. package/dist/gates/verdict-cause.js +63 -0
  52. package/dist/graph/schema.d.ts +6 -0
  53. package/dist/graph/schema.js +8 -5
  54. package/dist/route/router.d.ts +0 -5
  55. package/dist/route/router.js +16 -20
  56. package/dist/run/consult.d.ts +6 -0
  57. package/dist/run/consult.js +35 -25
  58. package/dist/run/daemon.d.ts +48 -2
  59. package/dist/run/daemon.js +1488 -330
  60. package/dist/run/journal.d.ts +56 -3
  61. package/dist/run/journal.js +358 -4
  62. package/dist/run/stall.d.ts +35 -1
  63. package/dist/run/stall.js +118 -8
  64. package/dist/tui/cockpit/capture.d.ts +12 -0
  65. package/dist/tui/cockpit/capture.js +37 -1
  66. package/dist/tui/cockpit/components.d.ts +2 -0
  67. package/dist/tui/cockpit/components.js +8 -8
  68. package/dist/tui/cockpit/derive.d.ts +29 -2
  69. package/dist/tui/cockpit/derive.js +219 -23
  70. package/dist/tui/cockpit/run-cockpit.js +128 -27
  71. package/dist/tui/cockpit/theme.d.ts +32 -26
  72. package/dist/tui/cockpit/theme.js +11 -5
  73. package/dist/tui/ink/components.d.ts +0 -15
  74. package/dist/tui/ink/components.js +0 -17
  75. package/dist/tui/ink/fleet-app.d.ts +4 -1
  76. package/dist/tui/ink/fleet-app.js +134 -13
  77. package/fixtures/sample.native.md +1 -1
  78. package/package.json +1 -1
  79. package/skills/tickmarkr-overseer/SKILL.md +354 -34
  80. package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
  81. package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
  82. package/dist/tui/ink/studio-app.d.ts +0 -59
  83. package/dist/tui/ink/studio-app.js +0 -320
  84. package/dist/tui/save.d.ts +0 -38
  85. package/dist/tui/save.js +0 -96
  86. package/dist/tui/staging.d.ts +0 -29
  87. package/dist/tui/staging.js +0 -78
@@ -116,6 +116,75 @@ function daemonPid(events) {
116
116
  }
117
117
  return undefined;
118
118
  }
119
+ /**
120
+ * The state an event writes onto the task it names — the whole list of them, so
121
+ * "did anything move this task" is asked in one place. `task-dispatch` and
122
+ * `merge` carry more than a state and are folded by hand below; they are named
123
+ * here because they move one all the same.
124
+ *
125
+ * A Map, because an event name is whatever the journal wrote: an object's own
126
+ * lookup answers `constructor` and `toString` with something inherited and
127
+ * truthy, so a line naming one would masquerade as a state transition — writing
128
+ * a function where a TaskState belongs and releasing a park the run declared.
129
+ * A Map answers only for the keys stated here.
130
+ */
131
+ const TASK_STATE_BY_EVENT = new Map([
132
+ ["task-dispatch", "running"],
133
+ ["merge", "done"],
134
+ ["task-done", "done"],
135
+ ["task-failed", "failed"],
136
+ ["task-human", "human"],
137
+ ["task-approved", "pending"],
138
+ ]);
139
+ /** Only task identities the summary actually records can enter its fold. */
140
+ function summaryTaskIds(value) {
141
+ return Array.isArray(value)
142
+ ? value.filter((id) => typeof id === "string")
143
+ : [];
144
+ }
145
+ /**
146
+ * The parks the latest `run-end` says the daemon stopped holding, that nothing
147
+ * since resolved. The summary is the daemon's own closing statement, so a run
148
+ * whose end names a task in `human` owes the operator a verb whether or not a
149
+ * `task-human` line happens to precede it — that dependence is what let a run
150
+ * end read `done` over a parked task (OBS-252).
151
+ *
152
+ * "Since" is measured from the summary forward, and only forward. The summary
153
+ * describes the moment the daemon exited, so a state written *before* it —
154
+ * the dispatch it parked, the completion of a round it then held for review —
155
+ * is history the summary already accounts for and cannot answer it. Weighing
156
+ * the whole-journal fold against it let a line above the summary retire a park
157
+ * the summary itself declares: `run-start · task-dispatch T1 · run-end
158
+ * human:[T1]` read `interrupted`, and the same journal with a `task-done T1`
159
+ * read `done` — OBS-252 again, one line lower.
160
+ *
161
+ * So a name is resolved only when a later event moved that task: approved,
162
+ * redispatched, finished, landed. Nothing later — including a task the record
163
+ * never mentions at all — leaves the park standing, because an uncorroborated
164
+ * park is still the run's own claim that it is waiting and withholding `done`
165
+ * on a claim the surface cannot check is the fail-closed reading. It stays a
166
+ * reading of the run and of the tasks it names: a summary is a journal event,
167
+ * so its explicit task outcomes are facts the board must count and draw even
168
+ * when the more detailed task event is absent.
169
+ */
170
+ function unresolvedSummaryParks(events) {
171
+ for (let index = events.length - 1; index >= 0; index -= 1) {
172
+ const event = events[index];
173
+ if (event.event !== "run-end")
174
+ continue;
175
+ const parked = Array.isArray(event.data.human) ? event.data.human : [];
176
+ const released = new Map();
177
+ for (const later of events.slice(index + 1)) {
178
+ if (later.taskId === undefined)
179
+ continue;
180
+ if (!TASK_STATE_BY_EVENT.has(later.event))
181
+ continue;
182
+ released.set(later.taskId, later.event !== "task-human");
183
+ }
184
+ return parked.filter((id) => typeof id === "string" && released.get(id) !== true);
185
+ }
186
+ return [];
187
+ }
119
188
  function deriveTasks(events, interrupted) {
120
189
  const tasks = new Map();
121
190
  const task = (taskId, index, ts) => {
@@ -126,6 +195,8 @@ function deriveTasks(events, interrupted) {
126
195
  id: taskId,
127
196
  dispatches: [],
128
197
  state: undefined,
198
+ parkKind: undefined,
199
+ merged: false,
129
200
  lastIndex: index,
130
201
  lastTs: ts,
131
202
  phase: "pending",
@@ -134,6 +205,30 @@ function deriveTasks(events, interrupted) {
134
205
  return created;
135
206
  };
136
207
  for (const [index, event] of events.entries()) {
208
+ if (event.event === "run-end") {
209
+ // The closing summary is a recorded fold, not commentary. Apply all
210
+ // task outcomes it owns before later resume events get their ordinary
211
+ // chance to supersede them. Failure comes last so a malformed overlap
212
+ // fails closed instead of repainting a failed task done.
213
+ const summaries = [
214
+ [event.data.done, "done"],
215
+ [event.data.human, "human"],
216
+ [event.data.failed, "failed"],
217
+ ];
218
+ for (const [ids, state] of summaries) {
219
+ for (const taskId of summaryTaskIds(ids)) {
220
+ const fact = task(taskId, index, event.ts);
221
+ fact.state = state;
222
+ // A summary does not invent a park kind; it preserves one only when
223
+ // it confirms that the same park still stands.
224
+ if (state !== "human")
225
+ fact.parkKind = undefined;
226
+ fact.lastIndex = index;
227
+ fact.lastTs = event.ts;
228
+ }
229
+ }
230
+ continue;
231
+ }
137
232
  if (!event.taskId)
138
233
  continue;
139
234
  if (event.event === "task-dispatch") {
@@ -148,11 +243,25 @@ function deriveTasks(events, interrupted) {
148
243
  assignment: assignmentFrom(event.data.assignment),
149
244
  });
150
245
  fact.state = "running";
246
+ fact.parkKind = undefined;
151
247
  fact.phase = "worker";
152
248
  fact.lastIndex = index;
153
249
  fact.lastTs = event.ts;
154
250
  continue;
155
251
  }
252
+ if (event.event === "merge") {
253
+ const fact = task(event.taskId, index, event.ts);
254
+ fact.merged = true;
255
+ // A merge is a landing, and it is a state the journal recorded: the task
256
+ // is done, and whatever park it was released from is retired by the merge
257
+ // itself rather than by whichever event happened to precede it. Nothing
258
+ // here depends on a `task-done` line arriving first (OBS-256).
259
+ fact.state = "done";
260
+ fact.parkKind = undefined;
261
+ fact.lastIndex = index;
262
+ fact.lastTs = event.ts;
263
+ continue;
264
+ }
156
265
  if (event.event === "phase-start") {
157
266
  const fact = task(event.taskId, index, event.ts);
158
267
  fact.phase = typeof event.data.phase === "string" ? event.data.phase : fact.phase;
@@ -160,15 +269,19 @@ function deriveTasks(events, interrupted) {
160
269
  fact.lastTs = event.ts;
161
270
  continue;
162
271
  }
163
- const state = {
164
- "task-done": "done",
165
- "task-failed": "failed",
166
- "task-human": "human",
167
- "task-approved": "pending",
168
- }[event.event];
272
+ // `task-dispatch` and `merge` returned above; the rest write their state and
273
+ // nothing else. One list, read from one place, so a new state-writing event
274
+ // cannot move the fold without also moving what resolves a park.
275
+ const state = TASK_STATE_BY_EVENT.get(event.event);
169
276
  if (state) {
170
277
  const fact = task(event.taskId, index, event.ts);
171
278
  fact.state = state;
279
+ // The park kind travels with the park: `task-human` records it, and the
280
+ // next recorded state — approved, done, failed — retires it with the park
281
+ // it named. A resolved state must not leave its badge behind (OBS-256).
282
+ fact.parkKind = state === "human" && typeof event.data.kind === "string"
283
+ ? event.data.kind
284
+ : undefined;
172
285
  fact.lastIndex = index;
173
286
  fact.lastTs = event.ts;
174
287
  }
@@ -250,14 +363,42 @@ function recordsFailure(event) {
250
363
  || event.data.pass === false
251
364
  || event.data.ok === false
252
365
  || (typeof event.data.exitCode === "number" && event.data.exitCode !== 0)
253
- || (event.event === "run-end" && event.data.tipVerify === "failed");
366
+ || (event.event === "run-end"
367
+ && (event.data.tipVerify === "failed"
368
+ || summaryTaskIds(event.data.failed).length > 0));
254
369
  }
370
+ /**
371
+ * The outcome an event records as a success. `merge` is here and `task-done` is
372
+ * not: a merge records the landing, and a completion records only that the
373
+ * worker stopped working — the gates that judge it and the merge that lands it
374
+ * come after. A `run-end` counts only when its tip verification reads passed:
375
+ * an absent verdict is nothing verified, and a check-mark on nothing verified
376
+ * is the run claiming a pass tickmarkr never corroborated — the same
377
+ * fail-closed reading the status strip already draws (OBS-244), now on the row.
378
+ */
255
379
  function recordsSuccess(event) {
256
- return event.event === "task-done"
257
- || event.event === "merge"
380
+ return event.event === "merge"
258
381
  || event.data.pass === true
259
382
  || event.data.ok === true
260
- || (event.event === "run-end" && event.data.tipVerify !== "failed");
383
+ || (event.event === "run-end" && event.data.tipVerify === "passed");
384
+ }
385
+ /** The newest run-end speaks for the run; spotlight is context, not outcome. */
386
+ function runPresentation(runStatus, spotlight) {
387
+ switch (runStatus) {
388
+ case "failed":
389
+ return { state: "fail", word: "failed" };
390
+ case "parked":
391
+ return { state: "warn", word: "parked" };
392
+ case "interrupted":
393
+ return { state: "warn", word: "interrupted" };
394
+ case "running":
395
+ return { state: "neutral", word: "running" };
396
+ case "done":
397
+ // A task named beside the run still earns a check only from its merge.
398
+ return spotlight === undefined || spotlight.merged
399
+ ? { state: "pass", word: "pass" }
400
+ : { state: "neutral", word: "done" };
401
+ }
261
402
  }
262
403
  /**
263
404
  * A row depicts the event it names: the event's own recorded outcome decides
@@ -268,11 +409,25 @@ function recordsSuccess(event) {
268
409
  * Every state carries its word, so meaning never rides on hue alone.
269
410
  */
270
411
  function eventPresentation(event, task) {
271
- if (event.event === "escalation" || event.event === "task-human") {
412
+ if (event.event === "escalation")
272
413
  return { state: "warn", word: "warn" };
273
- }
414
+ // A park is a state, not a severity: the row says what the engagement is
415
+ // waiting for, which is the operator.
416
+ if (event.event === "task-human")
417
+ return { state: "warn", word: "parked" };
274
418
  if (recordsFailure(event))
275
419
  return { state: "fail", word: "fail" };
420
+ if (event.event === "run-end"
421
+ && summaryTaskIds(event.data.human).length > 0) {
422
+ return { state: "warn", word: "parked" };
423
+ }
424
+ // Work finished is not work landed. `task-done` records the first, so its row
425
+ // says `done` and carries no check: the check-mark belongs to the `merge` that
426
+ // records the landing, and until the journal writes one there is nothing to
427
+ // put it on (OBS-256). This is the same reading `taskPresentation` gives the
428
+ // task itself, stated here for the event because a row depicts its own event.
429
+ if (event.event === "task-done")
430
+ return { state: "neutral", word: "done" };
276
431
  if (recordsSuccess(event))
277
432
  return { state: "pass", word: "pass" };
278
433
  if (task?.state === "interrupted") {
@@ -282,19 +437,29 @@ function eventPresentation(event, task) {
282
437
  }
283
438
  function historyRow(event, tasks, spotlight, newest, runStatus) {
284
439
  const task = event.taskId === undefined ? undefined : tasks.get(event.taskId);
285
- const { state, word } = eventPresentation(event, task);
440
+ // Spotlight is context only for the taskless closing summary. A task-owned
441
+ // newest event keeps its own identity and outcome: T2's dispatch or failure
442
+ // cannot become a pass for merged T1 merely because T1 wins the spotlight.
443
+ const summary = newest && event.event === "run-end"
444
+ ? runPresentation(runStatus, spotlight)
445
+ : undefined;
446
+ const { state, word } = summary ?? eventPresentation(event, task);
447
+ const displayedTask = newest
448
+ ? event.taskId === undefined ? spotlight : task
449
+ : task;
286
450
  // The spotlight entry must keep the task, its attempt and its acting adapter
287
451
  // inside the narrowest band's width budget — that budget cannot pay for the
288
452
  // separators and the event name, so the state word and the run's status
289
453
  // (what the newest event recorded) ride after the adapter instead.
290
- const text = newest && spotlight !== undefined
291
- ? `${spotlight.id} attempt ${taskAttempt(spotlight)} ${taskActor(spotlight)} ${word}${word === runStatus ? "" : ` ${runStatus}`}`
454
+ const text = newest && displayedTask !== undefined
455
+ ? `${displayedTask.id} attempt ${taskAttempt(displayedTask)} ${taskActor(displayedTask)} ${word}${word === runStatus ? "" : ` ${runStatus}`}`
292
456
  : task !== undefined
293
457
  ? `${task.id} ${word} · ${event.event}`
294
458
  : `${word} · ${event.event}`;
295
459
  return {
296
460
  id: `event:${event.line}`,
297
461
  time: eventTime(event.ts),
462
+ timestamp: event.ts,
298
463
  state,
299
464
  text,
300
465
  };
@@ -332,14 +497,28 @@ function taskRows(events, tasks, graph) {
332
497
  // zero when a run resumes, so a task dispatched 0,1,0 was tried three times
333
498
  // and reporting its last label would say one.
334
499
  const attempts = fact?.dispatches.length ?? 0;
500
+ // `task-done` records completion; only `merge` records landing. The fixed
501
+ // Tasks-view consumer maps `done` to a check, so the derived row must keep
502
+ // an unmerged completion in the neutral `completed` state and reserve
503
+ // `done` for a fact whose merge the journal actually carries.
504
+ const state = fact?.state === "done" && fact.merged !== true
505
+ ? "completed"
506
+ : fact?.state;
335
507
  return {
336
508
  id: `task:${id}`,
337
509
  taskId: id,
338
- ...(fact?.state === undefined ? {} : { state: fact.state }),
510
+ ...(state === undefined ? {} : { state }),
339
511
  ...(attempts === 0 ? {} : { attempts }),
340
512
  ...(assignment === undefined ? {} : { actor: channelKey(assignment) }),
341
513
  ...(lastTs === undefined ? {} : { lastEventTime: eventTime(lastTs) }),
514
+ ...(lastTs === undefined ? {} : { lastEventTimestamp: lastTs }),
342
515
  ...(title === undefined ? {} : { title }),
516
+ // Only while the park stands: the fold cleared it the moment the journal
517
+ // recorded what the park resolved to.
518
+ ...(fact?.parkKind === undefined ? {} : { parkKind: fact.parkKind }),
519
+ // Landing is its own fact. `done` is work finished; only a recorded merge
520
+ // is work landed, and only that earns a check-mark.
521
+ ...(fact?.merged === true ? { merged: true } : {}),
343
522
  };
344
523
  });
345
524
  }
@@ -356,6 +535,7 @@ function gateRows(events) {
356
535
  return [{
357
536
  id: `gate:${event.line}`,
358
537
  time: eventTime(event.ts),
538
+ timestamp: event.ts,
359
539
  state: pass === true ? "pass" : pass === false ? "fail" : "neutral",
360
540
  ...(typeof event.data.gate === "string" ? { gate: event.data.gate } : {}),
361
541
  ...(event.taskId === undefined ? {} : { taskId: event.taskId }),
@@ -421,6 +601,7 @@ function fleetRows(events) {
421
601
  model: channel.model,
422
602
  ...(channel.dispatches === 0 ? {} : { dispatches: channel.dispatches }),
423
603
  lastEventTime: eventTime(channel.lastTs),
604
+ lastEventTimestamp: channel.lastTs,
424
605
  }));
425
606
  }
426
607
  /**
@@ -535,15 +716,30 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
535
716
  const tipPassed = tipVerificationPassed(events);
536
717
  const spotlight = spotlightTask(tasks);
537
718
  const samples = bucketedMetricSamples(events);
719
+ // A task the journal carried somewhere other than done — approved and awaiting
720
+ // the resume that redispatches it, most of all. `done` is a claim about all of
721
+ // them, so one outstanding task retires it.
722
+ const outstanding = taskFacts.some((task) => task.state !== undefined && task.state !== "done");
723
+ // Parked outranks every reading but failure: a run holding an unresolved
724
+ // `human` task owes the operator a verb, and the daemon's exit after a park is
725
+ // the park's own consequence — not an interruption, and never done. A run that
726
+ // also failed a task owes a fix first, so failure still leads (OBS-252).
727
+ //
728
+ // The word is owed on two readings, not one: the parks the fold holds, and
729
+ // the parks the latest run-end names and nothing since resolved. The second
730
+ // is what keeps the reading off the luck of a `task-human` line preceding the
731
+ // run-end — the run's own closing statement is enough.
538
732
  const runStatus = failed > 0
539
733
  ? "failed"
540
- : lifecycle === "superseded" || hasInterruptedTask
541
- ? "interrupted"
542
- : lifecycle === "completed"
543
- ? "done"
544
- : interrupted
545
- ? "interrupted"
546
- : "running";
734
+ : human > 0 || unresolvedSummaryParks(events).length > 0
735
+ ? "parked"
736
+ : lifecycle === "superseded" || hasInterruptedTask
737
+ ? "interrupted"
738
+ : lifecycle === "completed"
739
+ ? outstanding ? "interrupted" : "done"
740
+ : interrupted
741
+ ? "interrupted"
742
+ : "running";
547
743
  return {
548
744
  binaryVersion,
549
745
  runId: source.fileName.replace(/(?:\.interrupted)?\.journal\.jsonl$/, ""),
@@ -53,33 +53,111 @@ function IdentityHeader({ data, columns, secondary = true, }) {
53
53
  {
54
54
  title: "",
55
55
  lines: [
56
- `${data.runId} · ${data.branch} ${data.status} · ${data.elapsed}`,
56
+ frameCaption(data),
57
57
  ],
58
58
  },
59
59
  ];
60
60
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(NarrowHeaderBand, { columns: columns, content: primary, rightStrong: true }), secondary && (_jsx(NarrowHeaderBand, { columns: columns, content: secondaryBand }))] }));
61
61
  }
62
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[0] ?? "tickmarkr" }), _jsxs(BodyText, { emphasis: "strong", children: ["v", data.binaryVersion, " \u00B7 binary ", GLYPHS.pass] })] }), secondary && (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[1] ?? "tickmarkr" }), _jsxs(BodyText, { children: [data.runId, " \u00B7 ", data.branch, " ", data.status, " \u00B7 ", data.elapsed] })] }))] }));
62
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[0] ?? "tickmarkr" }), _jsxs(BodyText, { emphasis: "strong", children: ["v", data.binaryVersion, " \u00B7 binary ", GLYPHS.pass] })] }), secondary && (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[1] ?? "tickmarkr" }), _jsx(BodyText, { children: frameCaption(data) })] }))] }));
63
63
  }
64
64
  function NarrowHeaderBand({ columns, content, rightStrong = false, }) {
65
65
  const widths = allocateBandColumns(columns, content, NARROW_HEADER_ALLOCATION);
66
66
  return (_jsxs(CockpitGrid, { columns: columns, columnContents: content, allocation: NARROW_HEADER_ALLOCATION, children: [_jsx(BandLines, { columns: Math.max(1, widths[0]), lines: content[0].lines, emphasis: "strong" }), _jsx(Box, { alignItems: "flex-end", flexDirection: "column", children: _jsx(BandLines, { columns: Math.max(1, widths[1]), lines: content[1].lines, emphasis: rightStrong ? "strong" : "normal" }) })] }));
67
67
  }
68
68
  const RUN_SIDE_RAIL_WIDTH = 16;
69
+ function runInstant(runId) {
70
+ const match = runId.match(/\brun-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})/u);
71
+ return match === null
72
+ ? new Date()
73
+ : new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6]));
74
+ }
75
+ function utcClock(value) {
76
+ const match = value.match(/^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/u);
77
+ return match === null
78
+ ? undefined
79
+ : [Number(match[1]), Number(match[2]), Number(match[3])];
80
+ }
81
+ function localZoneLabel(data) {
82
+ const newestTimestamp = data.journalRows.find((row) => row.timestamp !== undefined)?.timestamp;
83
+ const recorded = newestTimestamp === undefined ? undefined : new Date(newestTimestamp);
84
+ const reference = recorded !== undefined && Number.isFinite(recorded.getTime())
85
+ ? recorded
86
+ : runInstant(data.runId);
87
+ const offset = -reference.getTimezoneOffset();
88
+ if (offset === 0)
89
+ return "UTC";
90
+ const sign = offset < 0 ? "-" : "+";
91
+ const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0");
92
+ const minutes = Math.abs(offset) % 60;
93
+ return minutes === 0
94
+ ? `${sign}${hours}`
95
+ : `${sign}${hours}:${String(minutes).padStart(2, "0")}`;
96
+ }
97
+ function localClockTime(value, timestamp) {
98
+ if (utcClock(value) === undefined)
99
+ return value;
100
+ if (timestamp === undefined)
101
+ return value;
102
+ const completeInstant = new Date(timestamp);
103
+ if (!Number.isFinite(completeInstant.getTime()))
104
+ return value;
105
+ const hours = String(completeInstant.getHours()).padStart(2, "0");
106
+ const minutes = String(completeInstant.getMinutes()).padStart(2, "0");
107
+ const seconds = String(completeInstant.getSeconds()).padStart(2, "0");
108
+ return `${hours}:${minutes}:${seconds}`;
109
+ }
110
+ /** Name the ruler an already-built caption's bare attempt number is counted on. */
111
+ function nameAttemptRuler(caption) {
112
+ return caption.replace(/(?<!engagement |run )\battempt (?=\d)/gu, "engagement attempt ");
113
+ }
114
+ /**
115
+ * The fold owns active-task identity and full event order. This surface only names the attempt
116
+ * ruler and fits that authoritative caption; TaskRows are never ranked into a competing answer.
117
+ */
118
+ function engagementProgressCaption(data, columns = Number.POSITIVE_INFINITY) {
119
+ const caption = nameAttemptRuler(data.progressCaption);
120
+ if (cellWidth(caption) <= columns)
121
+ return caption;
122
+ const actorSeparator = caption.lastIndexOf(" · ");
123
+ return actorSeparator < 0
124
+ ? caption
125
+ : `${caption.slice(0, actorSeparator)} ${caption.slice(actorSeparator + 3)}`;
126
+ }
127
+ function runAttemptLabel(attempts) {
128
+ return attempts === 1
129
+ ? "run attempt 1"
130
+ : `run attempts ${fieldReading(attempts)}`;
131
+ }
69
132
  /** The header caption: the run's identity line, planned as the header's refinable sub-region. */
70
133
  function frameCaption(data) {
71
- return `${data.runId} · ${data.branch} · ${data.status} · ${data.elapsed}`;
134
+ return `${data.runId} · ${data.branch} · ${data.status} · ${data.elapsed} · zone ${localZoneLabel(data)}`;
72
135
  }
73
136
  /** The header's fixed left part: brand, version and the two tab labels. */
74
- function headerLead(data, plan) {
137
+ function headerLead(data, plan, columns) {
75
138
  const tabs = plan.tab === "watch"
76
139
  ? `[ WATCH ]${FRAME_HEADER_GAP}${DECISIONS_TAB_TITLE}`
77
140
  : `WATCH${FRAME_HEADER_GAP}[ ${DECISIONS_TAB_TITLE} ]`;
78
- return `tickmarkr ${data.binaryVersion}${FRAME_HEADER_GAP}${tabs}`;
141
+ if (plan.regions.some((region) => region.id === "caption")) {
142
+ return `tickmarkr ${data.binaryVersion}${FRAME_HEADER_GAP}${tabs}`;
143
+ }
144
+ const withVersion = `tickmarkr ${data.binaryVersion}${FRAME_HEADER_GAP}${tabs} · zone ${localZoneLabel(data)}`;
145
+ const compactTabs = plan.tab === "watch"
146
+ ? `[WATCH]${DECISIONS_TAB_TITLE}`
147
+ : `WATCH[${DECISIONS_TAB_TITLE}]`;
148
+ const compact = `tickmarkr ${data.binaryVersion}${compactTabs} zone ${localZoneLabel(data)}`;
149
+ if (cellWidth(withVersion) <= columns)
150
+ return withVersion;
151
+ if (cellWidth(compact) <= columns)
152
+ return compact;
153
+ const markedTabs = plan.tab === "watch"
154
+ ? `>WATCH${DECISIONS_TAB_TITLE}`
155
+ : `WATCH>${DECISIONS_TAB_TITLE}`;
156
+ return `tickmarkr ${data.binaryVersion}${markedTabs} zone ${localZoneLabel(data)}`;
79
157
  }
80
158
  /** The one plain header line: brand, version and the two tab labels, the active one marked. */
81
159
  function PlannedHeader({ data, plan, columns, }) {
82
- return (_jsx(BodyText, { emphasis: "strong", children: fitCells(headerLead(data, plan), Math.max(1, columns)) }));
160
+ return (_jsx(BodyText, { emphasis: "strong", children: fitCells(headerLead(data, plan, columns), Math.max(1, columns)) }));
83
161
  }
84
162
  /** The header's caption sub-region: the run identity, right-aligned in it. */
85
163
  function PlannedCaption({ data, columns, }) {
@@ -117,7 +195,7 @@ function setupSectionLines(section, data) {
117
195
  const parked = data.taskRows.filter((row) => row.state === "human");
118
196
  if (parked.length === 0)
119
197
  return ["No parked decisions in this engagement"];
120
- return parked.map((row) => `${row.taskId} · parked · attempt ${row.attempts ?? 0}`);
198
+ return parked.map((row) => `${row.taskId} · parked · ${runAttemptLabel(row.attempts)}`);
121
199
  }
122
200
  /**
123
201
  * The plan's item rows, and the ref the frame registers their painted node
@@ -189,20 +267,26 @@ export function deriveRunViewRows(data, viewId, filterQuery = "") {
189
267
  * drew. The overview owns no list of its own, and the journal is a tail.
190
268
  */
191
269
  function promotedViewRows(data, viewId) {
270
+ // Surrendered by identity, never rebuilt: the journal view shows the fold's own records, and
271
+ // rewriting their text here — even to name the ruler its attempt is counted on — would be this
272
+ // surface authoring a row. The counters the surface DOES compose (the caption, the task rows)
273
+ // name their rulers below.
192
274
  if (viewId === "journal")
193
275
  return data.journalRows;
194
276
  if (viewId === "tasks") {
195
- return data.taskRows.map((row) => ({
277
+ return data.taskRows.filter(recordedTaskRow).map((row) => ({
196
278
  id: row.id,
197
279
  time: fieldReading(row.lastEventTime),
198
- state: taskRowState(row.state),
199
- text: `${row.taskId} · ${fieldReading(row.state)} · attempt ${fieldReading(row.attempts)} · ${fieldReading(row.actor)}${row.title === undefined ? "" : ` · ${row.title}`}`,
280
+ ...(row.lastEventTimestamp === undefined ? {} : { timestamp: row.lastEventTimestamp }),
281
+ state: taskRowState(row),
282
+ text: `${row.taskId} · ${fieldReading(row.state)} · ${runAttemptLabel(row.attempts)} · ${fieldReading(row.actor)}${row.title === undefined ? "" : ` · ${row.title}`}`,
200
283
  }));
201
284
  }
202
285
  if (viewId === "gates") {
203
286
  return data.gateRows.map((row) => ({
204
287
  id: row.id,
205
288
  time: row.time,
289
+ ...(row.timestamp === undefined ? {} : { timestamp: row.timestamp }),
206
290
  state: row.state,
207
291
  text: `${fieldReading(row.taskId)} · ${fieldReading(row.gate)} · ${row.pass === undefined ? fieldReading(undefined) : row.pass ? "pass" : "fail"}${row.details === undefined ? "" : ` · ${row.details}`}`,
208
292
  }));
@@ -211,19 +295,29 @@ function promotedViewRows(data, viewId) {
211
295
  return data.fleetRows.map((row) => ({
212
296
  id: row.id,
213
297
  time: fieldReading(row.lastEventTime),
298
+ ...(row.lastEventTimestamp === undefined ? {} : { timestamp: row.lastEventTimestamp }),
214
299
  state: "neutral",
215
300
  text: `${row.adapter} · ${row.model} · dispatches ${fieldReading(row.dispatches)}`,
216
301
  }));
217
302
  }
218
303
  return [];
219
304
  }
305
+ /** A task row exists on an operator surface only when the journal fold recorded a task fact. */
306
+ function recordedTaskRow(row) {
307
+ return row.state !== undefined
308
+ || row.lastEventTime !== undefined
309
+ || row.attempts !== undefined
310
+ || row.actor !== undefined
311
+ || row.merged === true
312
+ || row.parkKind !== undefined;
313
+ }
220
314
  /** A task's recorded state read as the component vocabulary the rows draw in. */
221
- function taskRowState(state) {
222
- if (state === "done")
315
+ function taskRowState(row) {
316
+ if (row.state === "done" && row.merged === true)
223
317
  return "pass";
224
- if (state === "failed")
318
+ if (row.state === "failed")
225
319
  return "fail";
226
- if (state === "running" || state === "human")
320
+ if (row.state === "running" || row.state === "human")
227
321
  return "active";
228
322
  return "neutral";
229
323
  }
@@ -270,7 +364,10 @@ function SizedJournalPanel({ rows, bodyRows, title, selection, focused, }) {
270
364
  : window.rows.findIndex((row) => row.id === hovered);
271
365
  const emptyRows = Math.max(0, bodyRows - window.rows.length);
272
366
  const journal = JournalRowPanel({
273
- rows: window.rows,
367
+ rows: window.rows.map((row) => ({
368
+ ...row,
369
+ time: localClockTime(row.time, row.timestamp),
370
+ })),
274
371
  title,
275
372
  selection: window.selection,
276
373
  focused,
@@ -294,7 +391,7 @@ function helpLines(entries) {
294
391
  }
295
392
  /** The lines the opened-event view draws for one journal row. */
296
393
  function eventLines(row) {
297
- return [`${row.time} · ${row.state}`, row.text];
394
+ return [`${localClockTime(row.time, row.timestamp)} · ${row.state}`, row.text];
298
395
  }
299
396
  /**
300
397
  * A view drawn from the rows its own derivation produces: its detail while the
@@ -419,15 +516,15 @@ function TailBand({ data, rows, columns, pendingWrites, }) {
419
516
  ? ["PENDING WRITES · none"]
420
517
  : deriveRunViewRows(data, "journal")
421
518
  .slice(-rows)
422
- .map((row) => `${row.time} ${row.text}`);
519
+ .map((row) => `${localClockTime(row.time, row.timestamp)} ${row.text}`);
423
520
  // A styled blank cell differs with colour on and off; these rows carry no dim style.
424
521
  return (_jsx(Box, { flexDirection: "column", height: rows, overflow: "hidden", children: Array.from({ length: rows }, (_, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: fitCells(lines[index] ?? "", columns) }) }, index))) }));
425
522
  }
426
- function ApprovedProgress({ data, caption, }) {
427
- return (_jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), caption && _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }));
523
+ function ApprovedProgress({ data, caption, columns, }) {
524
+ return (_jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), caption && _jsx(BodyText, { emphasis: "dim", children: engagementProgressCaption(data, columns) })] }));
428
525
  }
429
- function CompactProgress({ data, caption, meterWidth = 28, }) {
430
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", children: [_jsx(BodyText, { emphasis: "strong", children: "PROGRESS " }), _jsx(ProgressMeter, { value: data.progress, width: meterWidth })] }), caption && _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }));
526
+ function CompactProgress({ data, caption, meterWidth = 28, columns, }) {
527
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", children: [_jsx(BodyText, { emphasis: "strong", children: "PROGRESS " }), _jsx(ProgressMeter, { value: data.progress, width: meterWidth })] }), caption && _jsx(BodyText, { emphasis: "dim", children: engagementProgressCaption(data, columns) })] }));
431
528
  }
432
529
  /** The RUN view's interior, allocated out of the planned body span, each element granted whole or not at all. */
433
530
  function resolveRunViewContent(bodySpan, bodyColumns, data) {
@@ -450,8 +547,9 @@ function resolveRunViewContent(bodySpan, bodyColumns, data) {
450
547
  : 0;
451
548
  journal += tail;
452
549
  available -= tail;
550
+ const progressCaptionColumns = Math.max(1, bodyColumns - RUN_PANEL_HORIZONTAL_CHROME);
453
551
  const captionRows = progressBar
454
- ? wrapCells(data.progressCaption, Math.max(1, bodyColumns)).length
552
+ ? wrapCells(engagementProgressCaption(data, progressCaptionColumns), progressCaptionColumns).length
455
553
  : 0;
456
554
  const progressCaption = captionRows > 0 && available >= captionRows;
457
555
  if (progressCaption)
@@ -511,12 +609,12 @@ function LadderRunPanel({ data, content, compactMeterWidth, interaction, keyEntr
511
609
  return (_jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(keyEntries), bodyRows: content.items.rows, wrapColumns: content.items.columns, focused: focused, flexGrow: 1 }));
512
610
  }
513
611
  return (_jsx(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: _jsxs(ItemRows, { children: [_jsx(RunStats, { data: data, mode: content.stats }), content.progressBar && (content.approvedProgress
514
- ? _jsx(ApprovedProgress, { data: data, caption: content.progressCaption })
515
- : (_jsx(CompactProgress, { data: data, caption: content.progressCaption, meterWidth: compactMeterWidth }))), content.journal > 0 && (_jsx(JournalBody, { data: data, interaction: interaction, bodyRows: content.journal, keyEntries: keyEntries }))] }) }));
612
+ ? (_jsx(ApprovedProgress, { data: data, caption: content.progressCaption, columns: Math.max(1, content.items.columns - RUN_PANEL_HORIZONTAL_CHROME) }))
613
+ : (_jsx(CompactProgress, { data: data, caption: content.progressCaption, meterWidth: compactMeterWidth, columns: Math.max(1, content.items.columns - RUN_PANEL_HORIZONTAL_CHROME) }))), content.journal > 0 && (_jsx(JournalBody, { data: data, interaction: interaction, bodyRows: content.journal, keyEntries: keyEntries }))] }) }));
516
614
  }
517
615
  function LegacyRunPanel({ data, interaction, keyEntries, focused = true, }) {
518
616
  const rows = deriveRunViewRows(data, "journal");
519
- return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: "tiles" }), _jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }), _jsx(JournalBody, { data: data, interaction: interaction, bodyRows: rows.length, keyEntries: keyEntries })] }));
617
+ return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: "tiles" }), _jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), _jsx(BodyText, { emphasis: "dim", children: engagementProgressCaption(data) })] }), _jsx(JournalBody, { data: data, interaction: interaction, bodyRows: rows.length, keyEntries: keyEntries })] }));
520
618
  }
521
619
  /**
522
620
  * One planned band, drawn at exactly the span the plan gives it. Unsized (the
@@ -528,8 +626,11 @@ function FrameBand({ rows, row = false, gap, regionRef, children, }) {
528
626
  /** A sized sub-floor plan draws the plain fallback, clipped to the measured size. */
529
627
  function PlainFallbackPaint({ data, size, }) {
530
628
  const columns = Math.max(1, size.columns);
629
+ // This is an operator surface, so it names its zone once like every other one.
630
+ // The run.ci/run.non-tty machine anchors are drawn by capture.ts's
631
+ // defaultPlainRenderer, not by this component, so the label does not move them.
531
632
  const lines = [
532
- `tickmarkr ${data.binaryVersion}`,
633
+ `tickmarkr ${data.binaryVersion} · zone ${localZoneLabel(data)}`,
533
634
  `${data.runId} ${data.status} ${data.elapsed}`,
534
635
  ...data.statusItems.map((item) => item.text),
535
636
  ];
@@ -588,7 +689,7 @@ function RunCockpitPaint({ data, columns, plan, content, interaction, keyProject
588
689
  : (_jsx(RailBand, { data: data, focused: railFocused, selection: railSelection, columns: rail.columns, rows: rail.rows, sidebar: plan.sidebar })) })), _jsx(Box, { ref: regionRef("body"), width: body.columns, height: body.rows, flexShrink: 0, flexDirection: "column", overflow: "hidden", children: _jsx(ItemRowsBand.Provider, { value: { region: content.items, regionRef: regionRef("items") }, children: setup
589
690
  ? (_jsx(SetupTabPanel, { data: data, selection: railSelection, focused: contentFocused, keyEntries: keyEntries, help: currentInteraction.help, bodyRows: content.items.rows, bodyColumns: content.items.columns }))
590
691
  : activeView === "run"
591
- ? (_jsx(LadderRunPanel, { data: data, content: content, compactMeterWidth: content.sideRails ? undefined : 24, interaction: currentInteraction, keyEntries: keyEntries, focused: contentFocused }))
692
+ ? (_jsx(LadderRunPanel, { data: data, content: content, compactMeterWidth: 24, interaction: currentInteraction, keyEntries: keyEntries, focused: contentFocused }))
592
693
  : (_jsx(RunViewPanel, { data: data, viewId: activeView, interaction: currentInteraction, focused: contentFocused, keyEntries: keyEntries, bodyRows: content.items.rows, bodyColumns: content.items.columns })) }) })] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule2").rows, regionRef: regionRef("rule2"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), tail !== undefined && (_jsx(FrameBand, { rows: tail.rows, regionRef: regionRef("tail"), children: _jsx(TailBand, { data: data, rows: tail.rows, columns: tail.columns, pendingWrites: plan.content.tail === "pending-writes" }) })), _jsx(FrameBand, { rows: requiredRegion(plan, "status").rows, regionRef: regionRef("status"), children: _jsx(StatusStrip, { items: reportedStatusItems, width: plan.size.columns }) }), _jsx(FrameBand, { rows: requiredRegion(plan, "keybar").rows, regionRef: regionRef("keybar"), children: _jsx(RunKeybar, { entries: keyEntries, width: plan.size.columns, help: currentInteraction.help }) })] }));
593
694
  }
594
695
  /** Resolve every draw-time input; the plan is planFrame's own return, consumed unmodified. */