tickmarkr 2.5.4 → 2.5.5

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 (59) hide show
  1. package/dist/adapters/registry.js +6 -1
  2. package/dist/adapters/types.d.ts +3 -0
  3. package/dist/cli/commands/approve.d.ts +1 -0
  4. package/dist/cli/commands/approve.js +54 -6
  5. package/dist/cli/commands/doctor.js +44 -29
  6. package/dist/cli/commands/fleet.js +195 -33
  7. package/dist/cli/commands/init.js +196 -6
  8. package/dist/cli/commands/resume.js +4 -2
  9. package/dist/cli/commands/run.js +11 -2
  10. package/dist/cli/commands/verify.js +1 -1
  11. package/dist/cli/help.d.ts +2 -0
  12. package/dist/cli/help.js +3 -1
  13. package/dist/config/config.d.ts +14 -8
  14. package/dist/config/config.js +29 -26
  15. package/dist/config/fleet-overlay.d.ts +3 -9
  16. package/dist/config/fleet-overlay.js +68 -11
  17. package/dist/config/fleet-why.d.ts +7 -0
  18. package/dist/config/fleet-why.js +5 -0
  19. package/dist/drivers/index.d.ts +15 -1
  20. package/dist/drivers/index.js +38 -10
  21. package/dist/drivers/orca.d.ts +99 -10
  22. package/dist/drivers/orca.js +586 -97
  23. package/dist/gates/baseline.d.ts +3 -0
  24. package/dist/gates/baseline.js +2 -1
  25. package/dist/gates/cache.d.ts +100 -0
  26. package/dist/gates/cache.js +389 -0
  27. package/dist/gates/run-gates.d.ts +3 -0
  28. package/dist/gates/run-gates.js +117 -10
  29. package/dist/gates/test-manifest.d.ts +99 -0
  30. package/dist/gates/test-manifest.js +389 -0
  31. package/dist/gates/test-reporter.d.ts +4 -0
  32. package/dist/gates/test-reporter.js +49 -0
  33. package/dist/route/preference.d.ts +22 -1
  34. package/dist/route/preference.js +123 -25
  35. package/dist/route/router.js +31 -6
  36. package/dist/run/daemon.d.ts +13 -0
  37. package/dist/run/daemon.js +160 -70
  38. package/dist/run/git.d.ts +10 -1
  39. package/dist/run/git.js +43 -9
  40. package/dist/run/journal.d.ts +14 -2
  41. package/dist/run/journal.js +78 -10
  42. package/dist/run/lease.d.ts +14 -0
  43. package/dist/run/lease.js +87 -0
  44. package/dist/run/merge.d.ts +2 -0
  45. package/dist/run/merge.js +91 -3
  46. package/dist/run/operator-state.d.ts +11 -0
  47. package/dist/run/operator-state.js +17 -3
  48. package/dist/tui/cockpit/board.d.ts +96 -0
  49. package/dist/tui/cockpit/board.js +346 -0
  50. package/dist/tui/cockpit/decision-actions.js +2 -0
  51. package/dist/tui/cockpit/layout.d.ts +5 -1
  52. package/dist/tui/cockpit/layout.js +8 -3
  53. package/dist/tui/cockpit/live-runtime.js +71 -21
  54. package/dist/tui/cockpit/run-view.d.ts +7 -5
  55. package/dist/tui/cockpit/run-view.js +12 -11
  56. package/dist/tui/ink/fleet-app.d.ts +41 -27
  57. package/dist/tui/ink/fleet-app.js +204 -31
  58. package/package.json +1 -1
  59. package/skills/tickmarkr-overseer/SKILL.md +173 -113
@@ -0,0 +1,96 @@
1
+ import { type RunGraph } from "../../graph/schema.js";
2
+ import type { OperatorSnapshot } from "../../run/operator-state.js";
3
+ /** The prototype's palette (tasks-redesign.mjs:24-30): xterm-256 indices, stated once. */
4
+ export declare const BOARD_PALETTE: {
5
+ readonly chrome: 238;
6
+ readonly mute: 243;
7
+ readonly body: 252;
8
+ readonly faint: 240;
9
+ readonly accent: 75;
10
+ readonly pass: 71;
11
+ readonly fail: 174;
12
+ readonly warn: 179;
13
+ readonly park: 140;
14
+ };
15
+ /** The seven gates, declaration order; every cell is exactly CELL columns wide. */
16
+ export declare const BOARD_GATES: readonly ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
17
+ export declare const BOARD_CELL = 3;
18
+ export declare const BOARD_AREA_W = 16;
19
+ export declare const BOARD_DEPS_W = 14;
20
+ export declare const BOARD_NOTE_RESERVE = 28;
21
+ export declare const BOARD_FIXED: number;
22
+ export declare const BOARD_EFFORT_BAR_MAX = 46;
23
+ /** The board's own key hints — only the ones the Run view handles; nothing advertised is inert. */
24
+ export declare const BOARD_KEYS: readonly ["↑↓ move", "↵ task detail", "q quit"];
25
+ /** Width band the Run view draws in (RULING-231-19 §2: ≥150 full · 110–149 note wraps · <110 stack). */
26
+ export type BoardBand = "full" | "wrap" | "stacked";
27
+ export declare const boardBand: (columns: number) => BoardBand;
28
+ export declare const stripBoardAnsi: (s: string) => string;
29
+ /** ANSI-aware clip at `n` visible cells (prototype `clip`), cut on a grapheme boundary so a
30
+ * double-cell glyph never crosses the limit and an emoji is never emitted as a lone surrogate;
31
+ * a clipped line closes its own colour. */
32
+ export declare function clipBoard(s: string, n: number): string;
33
+ export declare const BOARD_AREA_ORDER: string[];
34
+ /** {codes, unmapped} — abstentions applied, set semantics, misses reported rather than dropped. */
35
+ export declare function areasFor(files: readonly string[]): {
36
+ codes: string[];
37
+ unmapped: string[];
38
+ };
39
+ export interface BoardInput {
40
+ readonly runId: string;
41
+ readonly snapshot: OperatorSnapshot;
42
+ readonly graph?: Pick<RunGraph, "tasks">;
43
+ /** The clock the header's elapsed/ago fields read. */
44
+ readonly now: number;
45
+ /** A live daemon holds this run's lock. */
46
+ readonly live?: boolean;
47
+ /** The task the focus marker sits on; absent draws the prototype's plain indent. */
48
+ readonly selection?: string;
49
+ /** Footer key hints, joined the prototype's way; `false` leaves the footer to the caller (see boardFooter). */
50
+ readonly keys?: readonly string[] | false;
51
+ readonly colour?: boolean;
52
+ }
53
+ /** One task row, in the parts the Run view recomposes per width band. */
54
+ export interface BoardRow {
55
+ readonly id: string;
56
+ /** indent (or the focus marker) + id + area + deps + title */
57
+ readonly head: string;
58
+ /** the seven gate cells */
59
+ readonly strip: string;
60
+ /** channel + attempts */
61
+ readonly tail: string;
62
+ /** `◍ parked ×n · ↻ n review rounds · ✖ gate` / `waiting on …` / `in flight`, joined by ` · ` */
63
+ readonly note: string;
64
+ /** The note's segments, so a band can wrap it across continuation rows without cutting one. */
65
+ readonly noteParts: readonly string[];
66
+ }
67
+ export interface BoardFrame {
68
+ /** Header through the gate header rule, in the prototype's order. */
69
+ readonly top: readonly string[];
70
+ readonly rows: readonly BoardRow[];
71
+ /** Header row parts, aligned to the row parts. */
72
+ readonly gateHeader: {
73
+ readonly head: string;
74
+ readonly strip: string;
75
+ readonly tail: string;
76
+ };
77
+ /** Legend, retired ids, rule, effort fold, footer. */
78
+ readonly bottom: readonly string[];
79
+ readonly width: number;
80
+ readonly lbl: number;
81
+ readonly chan: number;
82
+ }
83
+ /** The prototype's frame, sectioned. `renderBoard` joins it row for row. */
84
+ export declare function boardFrame(input: BoardInput, width: number): BoardFrame;
85
+ /** The footer row: the key hints in chrome, the prototype's leading space kept. */
86
+ export declare const boardFooter: (keys: readonly string[], colour?: boolean) => string;
87
+ /** The prototype's frame as lines, one row per task, unclipped — exactly what the design signed off. */
88
+ export declare function renderBoard(input: BoardInput, width: number): string[];
89
+ /**
90
+ * The Run view's three width bands over the same frame: full draws the prototype's rows; wrap
91
+ * puts a note that does not fit on its own second row; stacked draws the gate cells, channel and
92
+ * attempts under the row so nothing is truncated away.
93
+ */
94
+ export declare function renderBoardLines(input: BoardInput, width: number): string[];
95
+ /** Pack note segments greedily into rows of `avail` cells; a segment wider than a row splits at the cell. */
96
+ export declare function wrapNote(parts: readonly string[], sep: string, firstAvail: number, avail: number): string[];
@@ -0,0 +1,346 @@
1
+ import { GATE_NAMES } from "../../graph/schema.js";
2
+ import { cellWidth, sliceCells } from "./width.js";
3
+ /* ------------------------------------------------------------------------ */
4
+ /* BD-1 — the approved task board (RULING-231-19 §2, OBS-965 add.2). */
5
+ /* */
6
+ /* A pure, Ink-free port of .overseer/design-prototypes/tasks-redesign.mjs. */
7
+ /* Palette, glyphs and column arithmetic are the prototype's constants; the */
8
+ /* two contract corrections are the declaration-order subtitle and the */
9
+ /* four-bucket green run-ended header. Facts come from the fold's snapshot */
10
+ /* and the graph the Run view already receives — never from the journal. */
11
+ /* ------------------------------------------------------------------------ */
12
+ // The prototype closes every span with a full reset (ESC[0m). Inside Ink that reset would also drop
13
+ // the shell's own foreground and background for the rest of the row, so spans close with the default
14
+ // foreground/background codes instead — chalk re-opens the enclosing colour after each of them.
15
+ const E = "\x1b[", R = `${E}0m`, CLOSE = `${E}39m`;
16
+ const c = (n) => (s) => `${E}38;5;${n}m${s}${CLOSE}`;
17
+ const bgc = (f, b) => (s) => `${E}38;5;${f};48;5;${b}m${s}${E}39;49m`;
18
+ const boldc = (s) => `${E}1m${s}${E}22m`;
19
+ const plain = (s) => s;
20
+ /** The prototype's palette (tasks-redesign.mjs:24-30): xterm-256 indices, stated once. */
21
+ export const BOARD_PALETTE = { chrome: 238, mute: 243, body: 252, faint: 240, accent: 75, pass: 71, fail: 174, warn: 179, park: 140 };
22
+ /** The seven gates, declaration order; every cell is exactly CELL columns wide. */
23
+ export const BOARD_GATES = GATE_NAMES;
24
+ export const BOARD_CELL = 3;
25
+ export const BOARD_AREA_W = 16;
26
+ export const BOARD_DEPS_W = 14;
27
+ export const BOARD_NOTE_RESERVE = 28;
28
+ export const BOARD_FIXED = 4 + 5 + BOARD_AREA_W + BOARD_DEPS_W + BOARD_CELL * BOARD_GATES.length + 2 + 5;
29
+ export const BOARD_EFFORT_BAR_MAX = 46;
30
+ /** The board's own key hints — only the ones the Run view handles; nothing advertised is inert. */
31
+ export const BOARD_KEYS = ["↑↓ move", "↵ task detail", "q quit"];
32
+ export const boardBand = (columns) => (columns >= 150 ? "full" : columns >= 110 ? "wrap" : "stacked");
33
+ export const stripBoardAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
34
+ const vw = (s) => cellWidth(stripBoardAnsi(s));
35
+ const pad = (s, n) => s + " ".repeat(Math.max(0, n - vw(s)));
36
+ /** ANSI-aware clip at `n` visible cells (prototype `clip`), cut on a grapheme boundary so a
37
+ * double-cell glyph never crosses the limit and an emoji is never emitted as a lone surrogate;
38
+ * a clipped line closes its own colour. */
39
+ export function clipBoard(s, n) {
40
+ const { head, tail } = sliceCells(s, n);
41
+ return tail && head.includes("\x1b") ? `${head}${R}` : head;
42
+ }
43
+ // ── AREA — derived from files[], never declared (prototype :48-94) ────────────────────────────────
44
+ const AREAS = [
45
+ ["SPEC", [/^src\/(compile|graph|plan)\//]],
46
+ ["FLEET", [/^src\/(route|config|adapters)\//]],
47
+ ["RUN", [/^src\/(run|drivers|eval)\//]],
48
+ ["GATE", [/^src\/gates\//]],
49
+ ["UI", [/^src\/(cli|tui|report)\//, /^src\/brand\.ts/]],
50
+ ["DOCS", [/^(docs|skills)\//]],
51
+ ["REPO", [/^(scripts|schema)\//, /^\.github\//, /^tests\/(lint|repo|helpers)\//, /^(vitest\.config|package\.json|tsconfig)/]],
52
+ ];
53
+ export const BOARD_AREA_ORDER = AREAS.map(([n]) => n);
54
+ const expandBraces = (p) => {
55
+ const m = /^(.*?)\{([^}]*)\}(.*)$/.exec(p);
56
+ return m ? m[2].split(",").flatMap((alt) => expandBraces(`${m[1]}${alt.trim()}${m[3]}`)) : [p];
57
+ };
58
+ const areaOf = (path) => {
59
+ for (const [name, res] of AREAS)
60
+ if (res.some((re) => re.test(path)))
61
+ return name;
62
+ return null;
63
+ };
64
+ /** {codes, unmapped} — abstentions applied, set semantics, misses reported rather than dropped. */
65
+ export function areasFor(files) {
66
+ const unmapped = [];
67
+ const hit = new Set(), softTest = new Set(), softCli = new Set();
68
+ for (const f of files.flatMap(expandBraces)) {
69
+ const a = areaOf(f) ?? areaOf(f.replace(/^tests\//, "src/"));
70
+ if (!a) {
71
+ unmapped.push(f);
72
+ continue;
73
+ }
74
+ if (/^tests\//.test(f))
75
+ softTest.add(a);
76
+ else if (/^src\/cli\//.test(f))
77
+ softCli.add(a);
78
+ else
79
+ hit.add(a);
80
+ }
81
+ let codes = [...hit];
82
+ if (!codes.length)
83
+ codes = [...softCli];
84
+ if (!codes.length)
85
+ codes = [...softTest];
86
+ codes.sort((x, y) => BOARD_AREA_ORDER.indexOf(x) - BOARD_AREA_ORDER.indexOf(y));
87
+ return { codes, unmapped };
88
+ }
89
+ const hm = (ms) => `${Math.floor(ms / 3.6e6)}h${String(Math.floor((ms % 3.6e6) / 6e4)).padStart(2, "0")}m`;
90
+ const ago = (ms) => (ms < 6e4 ? "just now" : ms < 3.6e6 ? `${Math.round(ms / 6e4)}m ago` : `${Math.round(ms / 3.6e6)}h ago`);
91
+ const isDone = (t) => t !== undefined && (t.merged || t.state === "completed");
92
+ /** The prototype's frame, sectioned. `renderBoard` joins it row for row. */
93
+ export function boardFrame(input, width) {
94
+ const W = Math.max(1, Math.floor(width));
95
+ const col = input.colour === false ? () => plain : c;
96
+ const bold = input.colour === false ? plain : boldc;
97
+ const bg = input.colour === false ? () => plain : bgc;
98
+ const chrome = col(BOARD_PALETTE.chrome), mute = col(BOARD_PALETTE.mute), body = col(BOARD_PALETTE.body), faint = col(BOARD_PALETTE.faint);
99
+ const accent = col(BOARD_PALETTE.accent), pass = col(BOARD_PALETTE.pass), fail = col(BOARD_PALETTE.fail), warn = col(BOARD_PALETTE.warn), park = col(BOARD_PALETTE.park);
100
+ const rule = (n = W) => chrome("─".repeat(n));
101
+ const s = input.snapshot;
102
+ const byId = new Map(s.tasks.map((t) => [t.id, t]));
103
+ // The fold's comparability boundary: a graph whose identity the journal did not record (a
104
+ // recompile) lends nothing — no titles, deps, declarations or denominator — so the rows are the
105
+ // journal's tasks alone and every gate reads as declared.
106
+ const graphTasks = input.graph?.tasks && s.comparable ? input.graph.tasks : s.tasks.map((t) => ({ id: t.id, title: t.title ?? "", deps: [], files: [], gates: undefined }));
107
+ const uncomparable = input.graph !== undefined && !s.comparable;
108
+ const doneIds = new Set(s.tasks.filter(isDone).map((t) => t.id));
109
+ const t0 = s.firstEventAt ? Date.parse(s.firstEventAt) : input.now;
110
+ const t1 = s.lastEventAt ? Date.parse(s.lastEventAt) : input.now;
111
+ const wide = W >= 118;
112
+ const top = [];
113
+ top.push(`${bg(16, 2)(" tickmarkr ")} ${bold(input.runId)} ${chrome("│")} ` +
114
+ `${pass(`${doneIds.size}/${graphTasks.length} done`)} ` +
115
+ (wide
116
+ ? `${chrome("│")} ${warn(`${s.resumes} restarts`)} ${chrome("│")} ${fail(`${s.escalations} escalations`)} `
117
+ : `${chrome("│")} ${fail(`${s.escalations} esc`)} `) +
118
+ `${chrome("│")} ${mute(`${hm(t1 - t0)}${wide ? ` · ${ago(input.now - t1)}` : ""}`)}` +
119
+ `${input.live ? ` ${chrome("│")} ${pass("● LIVE")}` : ""}`);
120
+ top.push("");
121
+ // RUN ENDED — the four-bucket rule (RULING-231-19 §2): green needs failed, human, blocked and
122
+ // pending every one empty AND the tip not failed; "tip verified" alone never borrows the colour.
123
+ if (s.latestRunEnd) {
124
+ const buckets = ["failed", "human", "blocked", "pending"].map((k) => [k, s.buckets[k]]);
125
+ const unknown = buckets.filter(([, v]) => v === undefined).map(([k]) => k);
126
+ const parked = buckets.filter(([, v]) => v !== undefined && v.length > 0).map(([k, v]) => `${k} ${v.join(",")}`);
127
+ const tally = `${doneIds.size} merged · ${(s.buckets.human ?? []).length} parked · ${(s.buckets.blocked ?? []).length} dep-blocked`;
128
+ const green = !unknown.length && !parked.length && (s.currentTip === "passed" || s.currentTip === "not required");
129
+ const now = green
130
+ ? `${pass("RUN ENDED — GREEN")} ${mute(`tip ${s.currentTip} · ${tally}`)}`
131
+ : s.currentTip === "failed"
132
+ ? `${fail("RUN ENDED — TIP VERIFY FAILED")} ${mute(tally)}`
133
+ : parked.length || unknown.length
134
+ ? `${fail("RUN ENDED — NOT GREEN")} ${mute(`${[...parked, ...unknown.map((k) => `${k} bucket unknown`)].join(" · ")} · ${tally}`)}`
135
+ : `${warn("RUN ENDED — TIP NOT VERIFIED")} ${mute(`tip ${s.currentTip} · ${tally}`)}`;
136
+ top.push(` ${accent("▌")} ${bold("NOW")} ${now}`);
137
+ top.push("");
138
+ }
139
+ top.push(` ${accent("▌")} ${bold("TASKS")} ${mute(uncomparable ? `${doneIds.size} of ${graphTasks.length} journal tasks merged · graph not comparable` : `${doneIds.size} of ${graphTasks.length} merged in this graph`)}` +
140
+ (wide ? mute(" · rows in dispatch order · gates left→right in declaration order") : ""));
141
+ top.push("");
142
+ // ── aligned gate header: each label truncated to the cell it owns (prototype :382-394) ──
143
+ const elastic = Math.max(30, W - BOARD_FIXED - BOARD_NOTE_RESERVE);
144
+ const LBL = Math.min(46, Math.max(18, Math.round(elastic * 0.62)));
145
+ const CHAN = Math.min(26, Math.max(12, elastic - LBL));
146
+ const gateHead = BOARD_GATES.map((g) => faint(pad(g.slice(0, BOARD_CELL - 1), BOARD_CELL))).join("");
147
+ const gateHeader = {
148
+ head: ` ${pad("", 5)}${pad(faint("area"), BOARD_AREA_W)}${pad(faint("deps"), BOARD_DEPS_W)}${pad(faint("task"), LBL)}`,
149
+ strip: gateHead,
150
+ tail: ` ${pad(faint("channel"), CHAN)}${pad(faint("att"), 5)}${faint("note")}`,
151
+ };
152
+ const headerRule = ` ${chrome("─".repeat(Math.min(W - 4, 5 + LBL + BOARD_CELL * BOARD_GATES.length + 2 + CHAN + 5 + 30)))}`;
153
+ const areaTally = new Set(), unmappedPaths = new Set();
154
+ const rows = [];
155
+ for (const g of graphTasks) {
156
+ const t = byId.get(g.id);
157
+ const declared = new Set(g.gates ?? BOARD_GATES);
158
+ const cell = (k) => {
159
+ const st = t?.gates[k]?.state;
160
+ return st === "passed" ? true : st === "failed" ? false : undefined;
161
+ };
162
+ const strip = BOARD_GATES.map((k) => {
163
+ const v = cell(k);
164
+ return pad(v === true ? pass("✔") : v === false ? fail("✖") : !declared.has(k) ? faint("─") : faint("·"), BOARD_CELL);
165
+ }).join("");
166
+ const unrun = BOARD_GATES.filter((k) => declared.has(k) && cell(k) === undefined).length;
167
+ const failedGates = BOARD_GATES.filter((k) => cell(k) === false);
168
+ const parks = t?.parks ?? 0, reviews = t?.reviewRounds ?? 0, attempts = t?.dispatches ?? 0;
169
+ const hot = parks > 3 || failedGates.length > 0 || unrun > 2;
170
+ const marker = input.selection === g.id ? ` ${accent("❯")} ` : " ";
171
+ const id = hot ? accent(bold(pad(g.id, 5))) : mute(pad(g.id, 5));
172
+ const { codes: acodes, unmapped: aun } = areasFor(g.files ?? []);
173
+ acodes.forEach((a) => areaTally.add(a));
174
+ aun.forEach((u) => unmappedPaths.add(u));
175
+ let alabel = acodes.length ? acodes.join("+") : (aun.length ? "UNMAPPED" : "—");
176
+ if (alabel.length > BOARD_AREA_W - 2) {
177
+ const keep = [];
178
+ for (const a of acodes) {
179
+ if ([...keep, a].join("+").length + 3 > BOARD_AREA_W - 2)
180
+ break;
181
+ keep.push(a);
182
+ }
183
+ alabel = `${keep.join("+")}+${acodes.length - keep.length}`;
184
+ }
185
+ const area = pad(hot ? body(alabel) : faint(alabel), BOARD_AREA_W);
186
+ const depIds = g.deps ?? [];
187
+ const depText = depIds.length ? depIds.join(",") : "—";
188
+ const depLabel = depText.length <= BOARD_DEPS_W - 2
189
+ ? depText
190
+ : depIds.length > 1 ? `${depIds[0]}+${depIds.length - 1}` : `${depIds[0].slice(0, BOARD_DEPS_W - 3)}…`;
191
+ const deps = pad(faint(depLabel), BOARD_DEPS_W);
192
+ const titleText = sliceCells(g.title ?? "", LBL - 2).head;
193
+ const title = hot ? body(pad(titleText, LBL)) : faint(pad(titleText, LBL));
194
+ const note = [
195
+ parks >= 3 ? park(`◍ parked ×${parks}`) : "",
196
+ reviews > 2 ? warn(`↻ ${reviews} review rounds`) : "",
197
+ failedGates.length ? fail(`✖ ${failedGates.join(",")}`) : "",
198
+ !attempts
199
+ ? (() => {
200
+ const blockers = depIds.filter((d) => !doneIds.has(d));
201
+ if (!blockers.length)
202
+ return faint("queued");
203
+ const room = Math.max(10, W - BOARD_FIXED - LBL - CHAN);
204
+ let shown = blockers, dropped = 0;
205
+ while (shown.length > 1 && `waiting on ${shown.join(", ")}`.length + (dropped ? 5 : 0) > room) {
206
+ shown = shown.slice(0, -1);
207
+ dropped = blockers.length - shown.length;
208
+ }
209
+ return faint(`waiting on ${shown.join(", ")}${dropped ? ` +${dropped}` : ""}`);
210
+ })()
211
+ : unrun === declared.size ? faint("in flight")
212
+ : unrun > 2 ? faint(`${declared.size - unrun}/${declared.size} gates run`) : "",
213
+ ].filter(Boolean);
214
+ rows.push({
215
+ id: g.id,
216
+ head: `${marker}${id}${area}${deps}${title}`,
217
+ strip,
218
+ tail: ` ${pad(mute(sliceCells(t?.channel ?? "—", CHAN - 2).head), CHAN)}${pad(mute(String(attempts)), 5)}`,
219
+ note: note.join(mute(" · ")),
220
+ noteParts: note,
221
+ });
222
+ }
223
+ const bottom = [];
224
+ bottom.push("");
225
+ bottom.push(` ${faint("area")} ${BOARD_AREA_ORDER.map((a) => (areaTally.has(a) ? body(a) : faint(a))).join(mute(" · "))}${unmappedPaths.size ? mute(" ") + fail(`${unmappedPaths.size} unmapped`) : ""}`);
226
+ if (unmappedPaths.size)
227
+ bottom.push(` ${pad("", 8)}${fail("UNMAPPED")} ${mute([...unmappedPaths].slice(0, 3).join(" "))}${unmappedPaths.size > 3 ? mute(` +${unmappedPaths.size - 3}`) : ""}`);
228
+ bottom.push(` ${faint("gates")} ${BOARD_GATES.map((k) => `${body(k.slice(0, BOARD_CELL - 1))} ${mute(k)}`).join(" ")}`);
229
+ bottom.push(` ${faint(" ")} ${pass("✔")} ${mute("passed")} ${fail("✖")} ${mute("failed")} ${faint("·")} ${mute("pending")} ${faint("─")} ${mute("not declared (acceptance and review are the optional two)")}`);
230
+ bottom.push("");
231
+ // the gap is stated, never silently skipped — derived, so it cannot go stale
232
+ const retired = s.tasks.map((t) => t.id).filter((id) => !graphTasks.some((g) => g.id === id)).sort();
233
+ // A retired task is still one of the fold's tasks, so the selection can land on it: its row carries
234
+ // the same marker the graph rows do — paint and navigation cannot diverge.
235
+ for (const id of retired)
236
+ bottom.push(`${input.selection === id ? ` ${accent("❯")} ` : " "}${faint(id)} ${faint("— not in this graph (retired at compile)")}`);
237
+ if (retired.length)
238
+ bottom.push("");
239
+ bottom.push(rule());
240
+ bottom.push("");
241
+ // ── where the run actually spent itself, per task (prototype :477-491) ──
242
+ const worst = graphTasks.map((g) => { const t = byId.get(g.id); return { id: g.id, p: t?.parks ?? 0, r: t?.reviewRounds ?? 0, a: t?.dispatches ?? 0 }; })
243
+ .sort((x, y) => (y.p + y.r + y.a) - (x.p + x.r + x.a)).slice(0, 4);
244
+ bottom.push(` ${accent("▌")} ${bold("WHERE THE EFFORT WENT")} ${mute(`dispatches · review rounds · human parks — top ${worst.length} of ${graphTasks.length} tasks`)}`);
245
+ bottom.push("");
246
+ const maxTot = Math.max(...worst.map((w) => w.a + w.r + w.p), 1);
247
+ const barw = Math.min(BOARD_EFFORT_BAR_MAX, Math.max(16, W - 60));
248
+ for (const w of worst) {
249
+ // The prototype rounds each segment on its own; the three share one budget, so the cells rounding
250
+ // pushes past barw come off the longest segment (4·1·1 at 46 rounds to 31+8+8) — never past the maximum.
251
+ const len = [w.a, w.r, w.p].map((n) => Math.round((n / maxTot) * barw));
252
+ for (let over = len.reduce((x, y) => x + y, 0) - barw; over > 0; over--)
253
+ len[len.indexOf(Math.max(...len))]--;
254
+ const seg = (n, paint) => paint("█".repeat(n));
255
+ bottom.push(` ${accent(pad(w.id, 6))}${seg(len[0], accent)}${seg(len[1], warn)}${seg(len[2], park)} ` +
256
+ `${mute(`${w.a} dispatch`)}${mute(" · ")}${warn(`${w.r} review`)}${mute(" · ")}${park(`${w.p} park`)}`);
257
+ }
258
+ bottom.push("");
259
+ bottom.push(` ${mute("legend")} ${accent("█")} ${faint("dispatch")} ${warn("█")} ${faint("review round")} ${park("█")} ${faint("human park")}`);
260
+ bottom.push("");
261
+ if (input.keys !== false)
262
+ bottom.push(boardFooter(input.keys ?? BOARD_KEYS, input.colour !== false));
263
+ return { top: [...top, `${gateHeader.head}${gateHeader.strip}${gateHeader.tail}`, headerRule], rows, gateHeader, bottom, width: W, lbl: LBL, chan: CHAN };
264
+ }
265
+ /** The footer row: the key hints in chrome, the prototype's leading space kept. */
266
+ export const boardFooter = (keys, colour = true) => (colour ? c(BOARD_PALETTE.chrome) : plain)(` ${keys.join(" · ")}`);
267
+ /** The prototype's frame as lines, one row per task, unclipped — exactly what the design signed off. */
268
+ export function renderBoard(input, width) {
269
+ const f = boardFrame(input, width);
270
+ return [...f.top, ...f.rows.map((r) => `${r.head}${r.strip}${r.tail}${r.note}`), ...f.bottom];
271
+ }
272
+ /**
273
+ * The Run view's three width bands over the same frame: full draws the prototype's rows; wrap
274
+ * puts a note that does not fit on its own second row; stacked draws the gate cells, channel and
275
+ * attempts under the row so nothing is truncated away.
276
+ */
277
+ export function renderBoardLines(input, width) {
278
+ const band = boardBand(width);
279
+ const f = boardFrame(input, width);
280
+ const IND = 9, ind = " ".repeat(IND);
281
+ const sep = (input.colour === false ? plain : c(BOARD_PALETTE.mute))(" · ");
282
+ const lines = [...f.top];
283
+ if (band === "stacked") {
284
+ // The gate header rides with the cells it labels; the title header stays with the title column.
285
+ lines.splice(lines.length - 2, 1, f.gateHeader.head, `${ind}${f.gateHeader.strip}${f.gateHeader.tail}`);
286
+ }
287
+ // A note never truncates: what does not fit beside its row continues on indented rows below it.
288
+ const noteRows = (r, lead) => {
289
+ const [first, ...rest] = wrapNote(r.noteParts, sep, Math.max(1, width - vw(lead)), Math.max(1, width - IND));
290
+ lines.push(`${lead}${first ?? ""}`, ...rest.map((l) => `${ind}${l}`));
291
+ };
292
+ for (const r of f.rows) {
293
+ if (band === "stacked") {
294
+ lines.push(r.head, `${ind}${r.strip}${r.tail}`);
295
+ if (r.note)
296
+ noteRows(r, ind);
297
+ continue;
298
+ }
299
+ const row = `${r.head}${r.strip}${r.tail}`;
300
+ if (!r.note || vw(row) + vw(r.note) <= width)
301
+ lines.push(`${row}${r.note}`);
302
+ else if (band === "full" && vw(row) < width - 12)
303
+ noteRows(r, row);
304
+ else {
305
+ lines.push(row);
306
+ noteRows(r, ind);
307
+ }
308
+ }
309
+ lines.push(...f.bottom);
310
+ return lines.map((l) => clipBoard(l, width));
311
+ }
312
+ /** Pack note segments greedily into rows of `avail` cells; a segment wider than a row splits at the cell. */
313
+ export function wrapNote(parts, sep, firstAvail, avail) {
314
+ const out = [];
315
+ let cur = "";
316
+ const room = () => (out.length ? avail : firstAvail);
317
+ const flush = () => { if (cur)
318
+ out.push(cur); cur = ""; };
319
+ for (const part of parts) {
320
+ const joined = cur ? `${cur}${sep}${part}` : part;
321
+ if (vw(joined) <= room()) {
322
+ cur = joined;
323
+ continue;
324
+ }
325
+ flush();
326
+ let rest = part;
327
+ while (vw(rest) > room()) {
328
+ const [head, tail] = splitBoard(rest, room());
329
+ if (!head)
330
+ break;
331
+ out.push(head);
332
+ rest = tail;
333
+ }
334
+ cur = rest;
335
+ }
336
+ flush();
337
+ return out;
338
+ }
339
+ /** ANSI-aware split at `n` visible cells on a grapheme boundary; the tail re-opens the colours the head left active. */
340
+ function splitBoard(s, n) {
341
+ const { head, tail } = sliceCells(s, n);
342
+ let active = "";
343
+ for (const [code] of head.matchAll(/\x1b\[[0-9;]*m/g))
344
+ active = code === CLOSE || code === R ? "" : active + code;
345
+ return [head + (active ? CLOSE : ""), active + tail];
346
+ }
@@ -3,6 +3,8 @@ import { APPROVAL_ENACTS, approvalDispositionForRelease, approvalEnactment, appr
3
3
  import { stateDirName } from "../../graph/graph.js";
4
4
  import { Journal } from "../../run/journal.js";
5
5
  const parkDiagnostic = (park) => {
6
+ if (park.kind === "scope-request")
7
+ return park.approveCommand ?? "scope-request requires tickmarkr approve with --files <glob,…>";
6
8
  if (park.tombstone)
7
9
  return "tombstone — permanent by design; no verb releases it";
8
10
  if (park.kind === "gate-fail" && park.failedGate === undefined) {
@@ -304,7 +304,11 @@ export type FramePlan = PlannedFrame | PlainFallbackPlan;
304
304
  */
305
305
  export declare function planFrame(size: MeasuredSize, view?: FrameView, state?: FrameState): FramePlan;
306
306
  export declare function resolveCockpitLayout(columns: number, rows: number): CockpitLayout;
307
- /** FINAL §3.1: one geometry for paint, focus and pointer hit testing. */
307
+ /**
308
+ * FINAL §3.1: one geometry for paint, focus and pointer hit testing. A ZERO shortcut budget is the
309
+ * rail-less mount (BD-1, RULING-231-19 §2): the daemon-placed board is a one-view observer, so it
310
+ * plans no rail and no shortcuts at any width — the budget is honoured, never floored back to 20.
311
+ */
308
312
  export declare function planShell(columns: number, rows: number, shortcutColumns?: number): {
309
313
  columns: number;
310
314
  rows: number;
@@ -389,12 +389,17 @@ export function resolveCockpitLayout(columns, rows) {
389
389
  },
390
390
  };
391
391
  }
392
- /** FINAL §3.1: one geometry for paint, focus and pointer hit testing. */
392
+ /**
393
+ * FINAL §3.1: one geometry for paint, focus and pointer hit testing. A ZERO shortcut budget is the
394
+ * rail-less mount (BD-1, RULING-231-19 §2): the daemon-placed board is a one-view observer, so it
395
+ * plans no rail and no shortcuts at any width — the budget is honoured, never floored back to 20.
396
+ */
393
397
  export function planShell(columns, rows, shortcutColumns = 22) {
394
398
  columns = Math.max(0, Math.floor(columns));
395
399
  rows = Math.max(0, Math.floor(rows));
396
- const rail = columns >= 90 ? 15 : 0;
397
- const shortcuts = columns >= 120 ? Math.max(20, Math.min(columns - 59, Math.floor(shortcutColumns))) : 0;
400
+ const railless = shortcutColumns === 0;
401
+ const rail = columns >= 90 && !railless ? 15 : 0;
402
+ const shortcuts = columns >= 120 && !railless ? Math.max(20, Math.min(columns - 59, Math.floor(shortcutColumns))) : 0;
398
403
  const bodyColumn = 1 + (rail ? rail + 1 : 0);
399
404
  return {
400
405
  columns, rows, refused: columns < 40 || rows < 14,