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
package/dist/run/stall.js CHANGED
@@ -18,6 +18,62 @@ const ELAPSED_RE = /(?<![\w.])\d+(?:\.\d+)?(?:ms|[hms])(?!\w)/g;
18
18
  export function normalizeStallSnapshot(text) {
19
19
  return text.replace(ANSI_RE, "").replace(SPINNER_RE, "").replace(ELAPSED_RE, "");
20
20
  }
21
+ // T1 (OBS-262): the rescue nudge's adapter scope — claude-code only (steering path proven,
22
+ // OBS-122). Widening it is a future fixture-capture chore (an occupied-frame capture per adapter,
23
+ // OBS-181 scar), never a drive-by edit. Lives in the stall module so the watchdog's policy and its
24
+ // scope constant cannot drift apart.
25
+ export const NUDGEABLE_ADAPTERS = new Set(["claude-code"]);
26
+ // T1 (OBS-263): a LIVE provider banner is the last thing the pane printed — the worker stopped
27
+ // underneath it. A "quota"/"rate limit" mention inside the task prompt, a diff hunk, or earlier
28
+ // output sits ABOVE the transcript tail and must never fail an attempt over, so the in-loop quota
29
+ // classifier reads only this many trailing non-empty rows instead of the whole retained snapshot.
30
+ // ponytail: rows, not a banner grammar — the ceiling is a worker frozen with a quota mention as its
31
+ // literal last output; the two-consecutive-slices + tracker-silence gates bound that cost to one
32
+ // failover within the routing floor. Upgrade path is a per-adapter banner fixture if it ever bites.
33
+ export const QUOTA_BANNER_TAIL_ROWS = 12;
34
+ export function stallSnapshotTail(text, rows = QUOTA_BANNER_TAIL_ROWS) {
35
+ return normalizeStallSnapshot(text)
36
+ .split("\n")
37
+ .filter((line) => line.trim().length > 0)
38
+ .slice(-rows)
39
+ .join("\n");
40
+ }
41
+ // T1 review (chrome-blind-matcher class, OBS-152/155): the tail of a RENDERED TUI frame is not
42
+ // "what the pane printed last" — its bottom rows are fixed composer/welcome chrome. Codex pins
43
+ // "• You have 3 usage limit resets available." there, so a raw-tail QUOTA_RE match fires on every
44
+ // frame of a wedged pane (verified against all 8 frames of tests/fixtures/codex-mcp-spinner) and
45
+ // would fail a live worker over mid-work. Filter the KNOWN chrome instead of everything on screen
46
+ // at some anchor: a novelty baseline cannot distinguish "chrome that was already there" from "a
47
+ // real banner the CLI printed before the first poll read" — a channel throttled at launch paints
48
+ // its banner inside the first BLOCKED_POLL_MS slice, so the banner BECOMES the baseline and is
49
+ // exculpated forever (proven by execution: banner-from-the-first-loop-read fails over on shipped
50
+ // 843328b0, parks human under the baseline). This line is semantically the opposite of exhaustion
51
+ // — resets AVAILABLE — so matching it out can never hide a real banner. Closed allowlist, same
52
+ // philosophy as the normalizer's: a new adapter's quota-flavored chrome is a fixture-capture
53
+ // chore, never a drive-by edit.
54
+ const QUOTA_CHROME_RE = /usage limit resets? available/i;
55
+ export function stallSnapshotBannerRows(text, rows = QUOTA_BANNER_TAIL_ROWS) {
56
+ return stallSnapshotTail(text, rows)
57
+ .split("\n")
58
+ .filter((line) => !QUOTA_CHROME_RE.test(line))
59
+ .join("\n");
60
+ }
61
+ // T1 (OBS-262/263, speed-spec §2): past fifteen minutes with a FLAT token-usage counter, row
62
+ // growth alone no longer re-arms the inactivity window — cosmetic repaint rows are not paid work.
63
+ // Token usage is the paid-work signal the tracker already samples; token growth always re-arms.
64
+ export const ROW_REARM_TOKEN_FLAT_MS = 15 * 60_000;
65
+ // Test seam, same pattern as the daemon's timing seams: production reads the constant.
66
+ let rowRearmTokenFlatMs = ROW_REARM_TOKEN_FLAT_MS;
67
+ export function setRowRearmTokenFlatMsForTests(ms) {
68
+ rowRearmTokenFlatMs = ms;
69
+ }
70
+ export function resetRowRearmTokenFlatMsForTests() {
71
+ rowRearmTokenFlatMs = ROW_REARM_TOKEN_FLAT_MS;
72
+ }
73
+ // T1 review (read-ceiling blindness): the daemon samples panes through `driver.read(slot, N)` —
74
+ // a bounded window. This constant IS that N, and the daemon's pane reads must use it (never a
75
+ // literal) so the tracker's saturation check below cannot drift away from the real read depth.
76
+ export const PANE_READ_ROWS = 1000;
21
77
  /**
22
78
  * Monotonic worker-progress measure for the stall watchdog.
23
79
  *
@@ -25,32 +81,86 @@ export function normalizeStallSnapshot(text) {
25
81
  * evidence of work. A rendered transcript is only known to have grown when it occupies more
26
82
  * non-empty rows than any prior sample. Same-row rewrites are deliberately ambiguous and do not
27
83
  * advance the clock: a recoverable early consult is safer than silencing the watchdog forever.
84
+ *
85
+ * CEILING: `transcriptRows` is a monotone high-water over the daemon's bounded pane read
86
+ * (PANE_READ_ROWS lines), so it is a one-way ratchet whose signal goes blind once the pane's
87
+ * content exceeds the read window — the same full window slides and `observe()` can never
88
+ * report row growth again. Past that point a `false` return means "unmeasurable", not "no
89
+ * output" — consumers making a kill decision (the dead-channel fast-kill) must check
90
+ * `rowSignalSaturated` and stand down on it.
28
91
  */
29
92
  export class StallProgressTracker {
30
- transcriptRows = 0;
93
+ transcriptRows = 0; // non-empty high-water over the bounded read — the growth signal
94
+ rawWindowLines = 0; // raw-line high-water — the SATURATION signal (see the getter)
31
95
  seedSubmitted = false;
32
96
  contextTokens;
33
- observe(sample) {
34
- let advanced = false;
97
+ lastTokenGrowthAt; // undefined until the first token sample anchors the flat-clock
98
+ rowGrowthAt; // raw row-growth clock — NEVER suppressed by the flat-token rule
99
+ /** True once a sample FILLED the bounded read window on RAW lines (blanks and chrome-only
100
+ * rows included): the pane's real extent is then unknown — genuinely new content scrolls out
101
+ * of the read and the row high-water can never advance again — so a flat tracker is blindness,
102
+ * not silence. The raw window is the saturation signal, NOT the normalized non-empty count: a
103
+ * production `read(slot, PANE_READ_ROWS)` returns at most PANE_READ_ROWS lines including blank
104
+ * and chrome-only rows (measured 730 non-empty of 1000 on the codex-mcp-spinner fixture), so
105
+ * comparing the non-empty high-water against PANE_READ_ROWS could never engage and the
106
+ * fast-kill's stand-down was unreachable. Sticky by construction (the high-water never
107
+ * decreases). */
108
+ get rowSignalSaturated() {
109
+ return this.rawWindowLines >= PANE_READ_ROWS;
110
+ }
111
+ /** Raw row-growth clock: the last observe() that advanced the row high-water, recorded even
112
+ * when the flat-token rule suppresses the progress REPORT (observe returns false). T1 review:
113
+ * the dead-channel fast-kill's "no output growth" leg must read this, not the suppressed
114
+ * progress clock — a metered adapter whose sticky token counter freezes the report while the
115
+ * pane keeps streaming rows is alive, and only this clock sees it. */
116
+ get lastRowGrowthAt() {
117
+ return this.rowGrowthAt;
118
+ }
119
+ observe(sample, now = Date.now()) {
120
+ let rowsAdvanced = false;
121
+ // raw window high-water first — this is the saturation signal (rowSignalSaturated), and it
122
+ // must see the sample exactly as the bounded read returned it, blanks and chrome included.
123
+ const rawLines = sample.paneText.split("\n").length;
124
+ if (rawLines > this.rawWindowLines)
125
+ this.rawWindowLines = rawLines;
35
126
  const rows = normalizeStallSnapshot(sample.paneText)
36
127
  .split("\n")
37
128
  .filter((line) => line.trim().length > 0)
38
129
  .length;
39
130
  if (rows > this.transcriptRows) {
40
131
  this.transcriptRows = rows;
41
- advanced = true;
132
+ rowsAdvanced = true;
133
+ this.rowGrowthAt = now; // raw signal — advances even when the report below is suppressed
42
134
  }
135
+ let seedAdvanced = false;
43
136
  if (sample.seedSubmitted && !this.seedSubmitted) {
44
137
  this.seedSubmitted = true;
45
- advanced = true;
138
+ seedAdvanced = true;
46
139
  }
140
+ let tokensAdvanced = false;
47
141
  const tokens = sample.contextTokens;
48
142
  if (tokens !== undefined && Number.isFinite(tokens)) {
49
143
  if (tokens > (this.contextTokens ?? 0))
50
- advanced = true;
51
- this.contextTokens = Math.max(this.contextTokens ?? 0, tokens);
144
+ tokensAdvanced = true;
145
+ // T1 review: ANY movement re-anchors the flat-clock, not just a new high-water mark — a
146
+ // context compaction drops the counter, and the climb back below the old peak is still paid
147
+ // work. A high-water comparison would freeze the anchor forever on the first decrease. (The
148
+ // first token sample always counts as movement: contextTokens starts undefined.)
149
+ if (tokens !== this.contextTokens)
150
+ this.lastTokenGrowthAt = now;
151
+ this.contextTokens = tokens;
152
+ }
153
+ if (tokensAdvanced || seedAdvanced)
154
+ return true;
155
+ if (rowsAdvanced) {
156
+ // T1: row growth past the flat-token cap is cosmetic — the paid-work counter has not moved,
157
+ // so the inactivity window must NOT re-arm on it. A tracker that never sees a token sample
158
+ // (unmetered adapter) keeps the old row-growth behavior.
159
+ if (this.lastTokenGrowthAt !== undefined && now - this.lastTokenGrowthAt >= rowRearmTokenFlatMs)
160
+ return false;
161
+ return true;
52
162
  }
53
- return advanced;
163
+ return false;
54
164
  }
55
165
  }
56
166
  // ─── v1.65 T2: LLM-bound transcript filter ──────────────────────────────────────────────────────
@@ -209,6 +209,18 @@ export type RegeneratedGoldenFrame = GoldenFrameCase & {
209
209
  readonly emitted: string;
210
210
  };
211
211
  export declare function regenerateGoldenFrames(binaryVersion?: string): Promise<readonly RegeneratedGoldenFrame[]>;
212
+ /**
213
+ * Canonicalize the one release-owned token in an ACTIVE golden frame.
214
+ *
215
+ * The grammar is deliberately positional: plain output owns the whole first
216
+ * line, while a setup frame owns the right-anchored `v<semver> · binary ✓`
217
+ * suffix. The latter preserves the suffix's right edge, so a longer version
218
+ * does not turn renderer padding into drift while any actual alignment change
219
+ * still does. Retired rendered frames never enter this exception.
220
+ */
221
+ export declare function normalizeGoldenFrameVersionForComparison(frame: GoldenFrameCase, bytes: string): string;
222
+ /** An absent committed frame is a mismatch; it is never an empty agreement. */
223
+ export declare function goldenFrameMatchesCommitted(frame: RegeneratedGoldenFrame, committed: string | undefined): boolean;
212
224
  export declare function findGoldenFrameMismatches(regenerated: readonly RegeneratedGoldenFrame[], committed: ReadonlyMap<string, string>): string[];
213
225
  export type RegeneratedColourFrame = ColourFrameCase & {
214
226
  readonly output: string;
@@ -564,13 +564,49 @@ export async function regenerateGoldenFrames(binaryVersion) {
564
564
  }
565
565
  return regenerated;
566
566
  }
567
+ const SEMVER_TOKEN = String.raw `\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?`;
568
+ const PLAIN_VERSION_HEADER = new RegExp(`^(tickmarkr v)(${SEMVER_TOKEN})$`, "u");
569
+ const FRAME_VERSION_HEADER = new RegExp(`^(.*\\S)( +)v(${SEMVER_TOKEN})( · binary ✓)$`, "u");
570
+ const COMPARISON_VERSION = "<binary-version>";
571
+ /**
572
+ * Canonicalize the one release-owned token in an ACTIVE golden frame.
573
+ *
574
+ * The grammar is deliberately positional: plain output owns the whole first
575
+ * line, while a setup frame owns the right-anchored `v<semver> · binary ✓`
576
+ * suffix. The latter preserves the suffix's right edge, so a longer version
577
+ * does not turn renderer padding into drift while any actual alignment change
578
+ * still does. Retired rendered frames never enter this exception.
579
+ */
580
+ export function normalizeGoldenFrameVersionForComparison(frame, bytes) {
581
+ if (isRetiredGoldenFrame(frame))
582
+ return bytes;
583
+ const lineBreak = bytes.indexOf("\n");
584
+ const header = lineBreak === -1 ? bytes : bytes.slice(0, lineBreak);
585
+ const rest = lineBreak === -1 ? "" : bytes.slice(lineBreak);
586
+ let normalized = header;
587
+ if (!frame.interactive || frame.ci) {
588
+ normalized = header.replace(PLAIN_VERSION_HEADER, `$1${COMPARISON_VERSION}`);
589
+ }
590
+ else {
591
+ normalized = header.replace(FRAME_VERSION_HEADER, (_match, prefix, gap, versionToken, suffix) => `${prefix}${" ".repeat(gap.length + versionToken.length)}v${COMPARISON_VERSION}${suffix}`);
592
+ }
593
+ return normalized === header ? bytes : `${normalized}${rest}`;
594
+ }
595
+ /** An absent committed frame is a mismatch; it is never an empty agreement. */
596
+ export function goldenFrameMatchesCommitted(frame, committed) {
597
+ return committed !== undefined
598
+ && normalizeGoldenFrameVersionForComparison(frame, committed)
599
+ === normalizeGoldenFrameVersionForComparison(frame, frame.output);
600
+ }
567
601
  export function findGoldenFrameMismatches(regenerated, committed) {
568
602
  // Membership is judged against the whole shipped manifest, retired cases
569
603
  // included: a retired fixture is claimed, so only its bytes are exempt.
570
604
  // Anything committed that no case claims is still named.
571
605
  const expected = new Set(GOLDEN_FRAME_CASES.map((frame) => frame.fixture));
572
606
  return [
573
- ...regenerated.flatMap((frame) => committed.get(frame.fixture) === frame.output ? [] : [frame.fixture]),
607
+ ...regenerated.flatMap((frame) => goldenFrameMatchesCommitted(frame, committed.get(frame.fixture))
608
+ ? []
609
+ : [frame.fixture]),
574
610
  ...[...committed.keys()].filter((fixture) => !expected.has(fixture)),
575
611
  ].sort();
576
612
  }
@@ -39,6 +39,8 @@ export declare const KEYBAR_KEYS: {
39
39
  export type JournalRow = {
40
40
  readonly id: string;
41
41
  readonly time: string;
42
+ /** The complete recorded instant behind `time`; absent only on non-event/legacy rows. */
43
+ readonly timestamp?: string;
42
44
  readonly state: ComponentState;
43
45
  readonly text: string;
44
46
  };
@@ -170,24 +170,24 @@ function statePresentation(state) {
170
170
  case "active":
171
171
  return {
172
172
  glyph: GLYPHS.toggleActive,
173
- color: `ansi256(${COCKPIT_DATA_RAMP[1].xterm})`,
173
+ color: COCKPIT_DATA_RAMP[1].hex,
174
174
  };
175
175
  case "inactive":
176
176
  return { glyph: GLYPHS.toggleInactive, dimmed: true };
177
177
  case "pass":
178
178
  return {
179
179
  glyph: GLYPHS.pass,
180
- color: `ansi256(${COCKPIT_DATA_RAMP[1].xterm})`,
180
+ color: COCKPIT_DATA_RAMP[1].hex,
181
181
  };
182
182
  case "fail":
183
183
  return {
184
184
  glyph: GLYPHS.fail,
185
- color: `ansi256(${COCKPIT_INKS.failure.xterm})`,
185
+ color: COCKPIT_INKS.failure.hex,
186
186
  };
187
187
  case "warn":
188
188
  return {
189
189
  glyph: GLYPHS.attention,
190
- color: `ansi256(${COCKPIT_INKS.warning.xterm})`,
190
+ color: COCKPIT_INKS.warning.hex,
191
191
  };
192
192
  case "neutral":
193
193
  return { glyph: GLYPHS.neutral, dimmed: true };
@@ -201,7 +201,7 @@ export function Panel({ title, focused = false, width, flexGrow, children, }) {
201
201
  const content = Children.map(children, (child) => typeof child === "string" || typeof child === "number"
202
202
  ? _jsx(BodyText, { children: child })
203
203
  : child);
204
- return (_jsxs(Box, { flexDirection: "column", flexGrow: flexGrow, width: width, paddingX: 1, borderStyle: focused ? "double" : "round", borderColor: `ansi256(${focused ? COCKPIT_DATA_RAMP[0].xterm : COCKPIT_DATA_RAMP[3].xterm})`, borderDimColor: !focused, children: [_jsx(Box, { children: focused ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: GLYPHS.pointer }), _jsxs(BodyText, { emphasis: "strong", children: [" ", title] })] })) : (_jsx(BodyText, { emphasis: "strong", children: title })) }), _jsx(Box, { flexDirection: "column", children: content })] }));
204
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: flexGrow, width: width, paddingX: 1, borderStyle: focused ? "double" : "round", borderColor: focused ? COCKPIT_DATA_RAMP[0].hex : COCKPIT_DATA_RAMP[3].hex, borderDimColor: !focused, children: [_jsx(Box, { children: focused ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: COCKPIT_DATA_RAMP[0].hex, children: GLYPHS.pointer }), _jsxs(BodyText, { emphasis: "strong", children: [" ", title] })] })) : (_jsx(BodyText, { emphasis: "strong", children: title })) }), _jsx(Box, { flexDirection: "column", children: content })] }));
205
205
  }
206
206
  export function CockpitGrid({ children, columns, columnContents, allocation, }) {
207
207
  const items = Children.toArray(children);
@@ -229,7 +229,7 @@ export function Sparkline({ samples, }) {
229
229
  const level = sparklineLevel(sample, minimum, maximum);
230
230
  const rampIndex = COCKPIT_DATA_RAMP.length - 1 - Math.round((level / (SPARKLINE_GLYPHS.length - 1)) * (COCKPIT_DATA_RAMP.length - 1));
231
231
  const ink = COCKPIT_DATA_RAMP[rampIndex] ?? COCKPIT_DATA_RAMP[0];
232
- return (_jsx(Text, { color: `ansi256(${ink.xterm})`, children: SPARKLINE_GLYPHS[level] }, `${index}:${sample}`));
232
+ return (_jsx(Text, { color: ink.hex, children: SPARKLINE_GLYPHS[level] }, `${index}:${sample}`));
233
233
  }) }));
234
234
  }
235
235
  export function StatTile({ label, value, samples, focused, width, }) {
@@ -242,7 +242,7 @@ export function ProgressMeter({ value, width = 24, }) {
242
242
  const valueLabel = Number.isInteger(percentage)
243
243
  ? percentage.toFixed(0)
244
244
  : percentage.toFixed(1);
245
- return (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", "aria-role": "progressbar", "aria-label": `${valueLabel}% complete`, children: [_jsx(Text, { color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: "█".repeat(filled) }), _jsx(Text, { dimColor: true, children: "░".repeat(safeWidth - filled) }), _jsxs(BodyText, { emphasis: "strong", children: [" ", valueLabel, "%"] })] }));
245
+ return (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", "aria-role": "progressbar", "aria-label": `${valueLabel}% complete`, children: [_jsx(Text, { color: COCKPIT_DATA_RAMP[0].hex, children: "█".repeat(filled) }), _jsx(Text, { dimColor: true, children: "░".repeat(safeWidth - filled) }), _jsxs(BodyText, { emphasis: "strong", children: [" ", valueLabel, "%"] })] }));
246
246
  }
247
247
  function OneLineItems({ children, width, label, }) {
248
248
  return (_jsx(Box, { flexDirection: "row", flexWrap: "nowrap", width: width, height: 1, overflow: "hidden", "aria-label": label, children: children }));
@@ -285,6 +285,6 @@ export function StatusStrip({ items, width, }) {
285
285
  }
286
286
  export function JournalRowPanel({ rows, width, focused, title = "JOURNAL", selection, hover, }) {
287
287
  return (_jsx(Panel, { title: title, focused: focused, width: width, children: rows.map((row, index) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "dim", children: row.time }), _jsx(BodyText, { children: " " }), _jsx(StateGlyph, { state: row.state }), _jsx(BodyText, { emphasis: index === hover ? "strong" : "normal", children: index === hover ? HOVER_MARKER : " " }), index === selection && (_jsxs(BodyText, { emphasis: "strong", children: [GLYPHS.pointer, " "] })), index === hover
288
- ? (_jsx(Text, { inverse: true, color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: row.text }))
288
+ ? (_jsx(Text, { inverse: true, color: COCKPIT_DATA_RAMP[0].hex, children: row.text }))
289
289
  : _jsx(BodyText, { children: row.text })] }, row.id))) }));
290
290
  }
@@ -3,7 +3,14 @@ import type { RunViewId } from "./views.js";
3
3
  export declare const SPARKLINE_BUCKET_WINDOW = 12;
4
4
  export declare function selectSparklineBucketWidthMs(elapsedMs: number): number;
5
5
  type MetricSample = number | null;
6
- export type RunStatus = "done" | "failed" | "interrupted" | "running";
6
+ /**
7
+ * What the run is, in one word. `parked` is first-class beside the rest because
8
+ * a run holding an unresolved `human` task owes the operator a verb — it is not
9
+ * done, and calling it done is the render OBS-252 caught. The task-level word
10
+ * for the same fact stays `human` (TaskState): that is the event the journal
11
+ * writes, and `parked` is what the fold of it means for the run.
12
+ */
13
+ export type RunStatus = "done" | "failed" | "interrupted" | "parked" | "running";
7
14
  export type RunCockpitData = {
8
15
  readonly binaryVersion: string;
9
16
  readonly runId: string;
@@ -80,12 +87,30 @@ export type TaskRow = {
80
87
  readonly attempts?: number;
81
88
  readonly actor?: string;
82
89
  readonly lastEventTime?: string;
90
+ /** Complete UTC instant paired with `lastEventTime`; additive for existing consumers. */
91
+ readonly lastEventTimestamp?: string;
83
92
  readonly title?: string;
93
+ /**
94
+ * The park the task is standing in right now, as the daemon recorded it
95
+ * (`task-human` data.kind). A resolved park has no kind at all: the badge
96
+ * retires with the state it belonged to, so a merged task never wears the
97
+ * `gate-fail` it was released from an hour earlier (OBS-256).
98
+ */
99
+ readonly parkKind?: string;
100
+ /**
101
+ * Recorded merged, and only then. A task the journal carried to `done` has
102
+ * finished its work; a task the journal recorded a `merge` for has landed.
103
+ * The two are different rows, so a consumer drawing a check-mark has the fact
104
+ * that earns one rather than having to read `done` as if it were the same.
105
+ */
106
+ readonly merged?: true;
84
107
  };
85
108
  /** One recorded gate result. `details` is the record's own text, verbatim. */
86
109
  export type GateRow = {
87
110
  readonly id: string;
88
111
  readonly time: string;
112
+ /** Complete UTC instant paired with `time`; additive for existing consumers. */
113
+ readonly timestamp?: string;
89
114
  readonly state: ComponentState;
90
115
  readonly gate?: string;
91
116
  readonly taskId?: string;
@@ -99,8 +124,10 @@ export type FleetRow = {
99
124
  readonly model: string;
100
125
  readonly dispatches?: number;
101
126
  readonly lastEventTime?: string;
127
+ /** Complete UTC instant paired with `lastEventTime`; additive for existing consumers. */
128
+ readonly lastEventTimestamp?: string;
102
129
  };
103
- export type TaskState = "done" | "failed" | "human" | "pending" | "running" | "interrupted";
130
+ export type TaskState = "completed" | "done" | "failed" | "human" | "pending" | "running" | "interrupted";
104
131
  /**
105
132
  * The source identities the named view's rows carry, in draw order. Selection
106
133
  * is repaired against these, so it follows a row across a refresh instead of