tickmarkr 2.5.4 → 2.5.6
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.
- package/README.md +3 -1
- package/dist/adapters/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +118 -9
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +199 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/run.js +11 -2
- package/dist/cli/commands/verify.js +102 -81
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/config/config.d.ts +25 -8
- package/dist/config/config.js +41 -29
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +114 -19
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +39 -10
- package/dist/drivers/orca.d.ts +119 -10
- package/dist/drivers/orca.js +781 -110
- package/dist/gates/baseline.d.ts +17 -3
- package/dist/gates/baseline.js +63 -15
- package/dist/gates/cache.d.ts +101 -0
- package/dist/gates/cache.js +401 -0
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +106 -14
- package/dist/gates/run-gates.d.ts +9 -1
- package/dist/gates/run-gates.js +407 -102
- package/dist/gates/test-manifest.d.ts +128 -0
- package/dist/gates/test-manifest.js +463 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +57 -0
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +45 -1
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.js +31 -6
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +15 -0
- package/dist/run/daemon.js +638 -167
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +50 -1
- package/dist/run/git.js +131 -12
- package/dist/run/journal.d.ts +16 -2
- package/dist/run/journal.js +115 -25
- package/dist/run/lease.d.ts +58 -0
- package/dist/run/lease.js +310 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- package/dist/run/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -3
- package/dist/tui/cockpit/board.d.ts +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +71 -21
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +49 -27
- package/dist/tui/ink/fleet-app.js +229 -38
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +173 -113
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
397
|
-
const
|
|
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,
|
|
@@ -9,7 +9,8 @@ import { render, useInput, useStdin } from "ink";
|
|
|
9
9
|
import { useLayoutEffect, useSyncExternalStore } from "react";
|
|
10
10
|
import { createLiveStore } from "./live-store.js";
|
|
11
11
|
import { HomeView, deriveHomeView, selectNeedsYouTarget } from "./home-view.js";
|
|
12
|
-
import { RunView, applyRunViewKey, initialRunViewSession, runGateCells, evidenceLookup
|
|
12
|
+
import { RunView, applyRunViewKey, initialRunViewSession, runGateCells, evidenceLookup } from "./run-view.js";
|
|
13
|
+
import { renderBoardLines, stripBoardAnsi } from "./board.js";
|
|
13
14
|
import { EvidenceView, deriveEvidenceView, defaultExportPath, planEvidenceExport, writeEvidenceExport } from "./evidence-view.js";
|
|
14
15
|
import { ShellGridFlow, ShellJournalSelection } from "./components.js";
|
|
15
16
|
import { CockpitShell, initialShellState } from "./shell.js";
|
|
@@ -18,12 +19,11 @@ import { shellBindings } from "./keys.js";
|
|
|
18
19
|
import { createPointerReportReader, POINTER_TRACKING_ON, POINTER_TRACKING_OFF } from "./pointer.js";
|
|
19
20
|
import { deriveRunDecisions, previewDecision, executeDecision, withDecisionPreview, withDecisionReceipt, decisionConfirmLines } from "./decision-actions.js";
|
|
20
21
|
import { approvalRunOwner } from "../../cli/commands/approve.js";
|
|
21
|
-
import { GATE_NAMES } from "../../graph/schema.js";
|
|
22
22
|
import { GLYPHS } from "../../brand.js";
|
|
23
23
|
import { Journal } from "../../run/journal.js";
|
|
24
|
-
import { observeNamedRun } from "../../run/supervision.js";
|
|
24
|
+
import { observeNamedRun, WATCH_OWNER_ENV } from "../../run/supervision.js";
|
|
25
25
|
import { formatOwnedName } from "../../drivers/types.js";
|
|
26
|
-
import { cellWidth,
|
|
26
|
+
import { cellWidth, wrapCells } from "./width.js";
|
|
27
27
|
import { resolveShellColourMode } from "./theme.js";
|
|
28
28
|
let timelineUsers = 0;
|
|
29
29
|
let timelineObserver;
|
|
@@ -100,7 +100,10 @@ export async function runConsolidatedCockpit(options) {
|
|
|
100
100
|
const source = store;
|
|
101
101
|
if (options.observeRun !== false)
|
|
102
102
|
observation = observeNamedRun(cwd, runId, options.environment);
|
|
103
|
-
|
|
103
|
+
// BD-1 (RULING-231-19 §2): the daemon-placed board carries the watch owner token and mounts
|
|
104
|
+
// rail-less — a zero shortcut budget the plan honours at every width. The manual cockpit keeps its rail.
|
|
105
|
+
const railless = Boolean((options.environment ?? process.env)[WATCH_OWNER_ENV]);
|
|
106
|
+
let state = { ...initialShellState(), view: options.initialView ?? "home", ...(railless ? { shortcutColumns: 0 } : {}) };
|
|
104
107
|
let runSession = initialRunViewSession();
|
|
105
108
|
if (options.initialParks) {
|
|
106
109
|
runSession = { ...runSession, selection: Math.max(0, source.snapshot().operator.tasks.findIndex(t => t.state === "human" || t.state === "blocked")) };
|
|
@@ -172,6 +175,29 @@ export async function runConsolidatedCockpit(options) {
|
|
|
172
175
|
state = { ...state, overlay: [result.status === "focused" ? `Focused ${task.id}` : `Pane ${result.status}`, result.reason, "Enter opens task evidence after Esc"], overlayOffset: 0 };
|
|
173
176
|
publish();
|
|
174
177
|
};
|
|
178
|
+
// The board is taller than a small body; a selection moved by keys or a park opened from Home
|
|
179
|
+
// scrolls the shell's viewport just far enough that the selected row (and the rows that wrap
|
|
180
|
+
// under it) is seen. The rows are the very lines the Run view draws at this body width.
|
|
181
|
+
const followSelection = () => {
|
|
182
|
+
const snap = source.snapshot();
|
|
183
|
+
const task = snap.operator.tasks[runSession.selection];
|
|
184
|
+
if (!task)
|
|
185
|
+
return;
|
|
186
|
+
const g = geometry();
|
|
187
|
+
const lines = renderBoardLines({ runId, snapshot: snap.operator, graph: snap.graph.value, now: (options.now ?? Date.now)(), selection: task.id, keys: false, colour: false }, g.bodyColumns);
|
|
188
|
+
const first = lines.findIndex(l => l.startsWith(" ❯ "));
|
|
189
|
+
if (first < 0)
|
|
190
|
+
return;
|
|
191
|
+
let last = first;
|
|
192
|
+
while (last + 1 < lines.length && lines[last + 1].startsWith(" ".repeat(9)))
|
|
193
|
+
last++;
|
|
194
|
+
const top = first + 1, bottom = last + 1; // the RUN summary line sits above the board
|
|
195
|
+
const scroll = state.scroll;
|
|
196
|
+
if (top < scroll)
|
|
197
|
+
state = { ...state, scroll: top };
|
|
198
|
+
else if (bottom >= scroll + g.bodyRows)
|
|
199
|
+
state = { ...state, scroll: bottom - g.bodyRows + 1 };
|
|
200
|
+
};
|
|
175
201
|
const openDetail = () => {
|
|
176
202
|
const snap = source.snapshot();
|
|
177
203
|
const task = state.view === "run" ? snap.operator.tasks[runSession.selection] : undefined;
|
|
@@ -189,6 +215,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
189
215
|
// from the snapshot. The guard prevents the shell from dispatching its echo.
|
|
190
216
|
let leafInput;
|
|
191
217
|
let forwarding = false;
|
|
218
|
+
let lastRawInput = "";
|
|
192
219
|
const forwardLeaf = (bytes) => {
|
|
193
220
|
forwarding = true;
|
|
194
221
|
try {
|
|
@@ -347,12 +374,15 @@ export async function runConsolidatedCockpit(options) {
|
|
|
347
374
|
const task = snap.operator.tasks[runSession.selection];
|
|
348
375
|
const cells = task ? runGateCells(task, evidenceLookup(snap.journal.history, source.page)) : [];
|
|
349
376
|
runSession = applyRunViewKey(runSession, event, { tasks: snap.operator.tasks, decisions, verdictLines: cells[runSession.verdictGate]?.verdict.length ?? 0 }).session;
|
|
377
|
+
if (k.upArrow || k.downArrow)
|
|
378
|
+
followSelection();
|
|
350
379
|
if (k.pageDown || k.pageUp)
|
|
351
380
|
state = {
|
|
352
381
|
...state,
|
|
353
|
-
// The Run leaf owns verdict paging; the shell only moves its
|
|
354
|
-
//
|
|
355
|
-
|
|
382
|
+
// The Run leaf owns verdict paging; the shell only moves its viewport so that leaf's
|
|
383
|
+
// verdict panel is seen — it sits below the board and the selected task's cells, so the
|
|
384
|
+
// viewport goes to the leaf's end (clamped by the shell), not one page down.
|
|
385
|
+
scroll: k.pageDown ? Number.MAX_SAFE_INTEGER : 0,
|
|
356
386
|
};
|
|
357
387
|
if (runSession.decisions.menu)
|
|
358
388
|
state = { ...state, overlay: ["Actions — Enter reviews; Esc cancels", ...runSession.decisions.menu.verbs], overlayOffset: 0 };
|
|
@@ -501,16 +531,35 @@ export async function runConsolidatedCockpit(options) {
|
|
|
501
531
|
useSyncExternalStore(listener => { listeners.add(listener); return () => { listeners.delete(listener); }; }, () => revision);
|
|
502
532
|
const stdinContext = useStdin();
|
|
503
533
|
useLayoutEffect(() => {
|
|
504
|
-
|
|
505
|
-
|
|
534
|
+
const emitter = stdinContext.internal_eventEmitter;
|
|
535
|
+
leafInput = emitter;
|
|
536
|
+
// Ink strips an escape sequence's ESC before useInput sees it, so an unnamed CSI and typed
|
|
537
|
+
// "[..." look alike there. Its parser emits every escape sequence as its own event, so the RAW
|
|
538
|
+
// event says which it was. Registered in a layout effect, this listener runs before useInput's.
|
|
539
|
+
const raw = (data) => { lastRawInput = data; };
|
|
540
|
+
emitter?.on("input", raw);
|
|
541
|
+
return () => { leafInput = undefined; emitter?.off("input", raw); };
|
|
506
542
|
}, [stdinContext.internal_eventEmitter]);
|
|
507
543
|
const renderedView = state.view;
|
|
508
544
|
const leafActive = state.editor === undefined && !state.overlay && !state.help && !state.reviewing;
|
|
509
545
|
useInput((text, k) => {
|
|
510
546
|
if (forwarding || (leafActive && renderedView !== "run" && (k.return || (renderedView === "evidence" && text === "e"))))
|
|
511
547
|
return;
|
|
548
|
+
// OBS-1002: Ink hands a coalesced chunk ("a\r", "na\r" — a paste, key repeat under load, a
|
|
549
|
+
// `pane run` message) to the handler as ONE string with no key flags, and a single-key map
|
|
550
|
+
// ignores it. Split every non-escape chunk into one event per code point; an escape sequence
|
|
551
|
+
// (arrows, page keys) is one key and already carries its flags.
|
|
552
|
+
const flagged = k.ctrl || k.meta || k.upArrow || k.downArrow || k.leftArrow || k.rightArrow || k.pageUp || k.pageDown || k.return || k.escape || k.tab || k.backspace || k.delete;
|
|
553
|
+
const points = !flagged && !lastRawInput.startsWith("\x1b") && [...text].length > 1 ? [...text] : [text];
|
|
512
554
|
try {
|
|
513
|
-
|
|
555
|
+
for (const point of points) {
|
|
556
|
+
const event = points.length === 1 ? { input: text, key: k }
|
|
557
|
+
: point === "\r" || point === "\n" ? { input: "", key: { return: true } }
|
|
558
|
+
: point === "\t" ? { input: "", key: { tab: true } }
|
|
559
|
+
: point === "\x1b" ? { input: "", key: { escape: true } }
|
|
560
|
+
: { input: point, key: { shift: /[A-Z]/.test(point) } };
|
|
561
|
+
key(event, true);
|
|
562
|
+
}
|
|
514
563
|
}
|
|
515
564
|
catch (error) {
|
|
516
565
|
stop(error);
|
|
@@ -564,10 +613,9 @@ export async function runConsolidatedCockpit(options) {
|
|
|
564
613
|
return _jsx(CockpitShell, { snapshot: options.observeRun === false ? { ...snap, journal: { ...snap.journal, source: "no run/journal.jsonl" } } : snap, state: state, columns: p.columns, rows: p.rows, version: options.binaryVersion, colour: colour, onCommit: plan => {
|
|
565
614
|
committed = plan;
|
|
566
615
|
const targets = [];
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}));
|
|
616
|
+
// A board row opens with the prototype's four-cell indent or the focus marker, then the id;
|
|
617
|
+
// the first painted row naming a task is that task's target (the effort fold names it again, later).
|
|
618
|
+
const boardRowId = (text) => /^(?: {4}| {2}❯ )(\S+)/u.exec(stripBoardAnsi(text))?.[1];
|
|
571
619
|
const usedTasks = new Set();
|
|
572
620
|
for (const row of plan.paintedRows) {
|
|
573
621
|
if (row.row < plan.bodyRow && (row.text.startsWith("1 Home") || row.text.startsWith("1H"))) {
|
|
@@ -599,10 +647,12 @@ export async function runConsolidatedCockpit(options) {
|
|
|
599
647
|
continue;
|
|
600
648
|
}
|
|
601
649
|
if (state.view === "run") {
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
650
|
+
const drawn = boardRowId(row.text);
|
|
651
|
+
// A narrow body clips the row inside a long id; the drawn prefix still names one task.
|
|
652
|
+
const id = drawn === undefined ? undefined : (snap.operator.tasks.find(task => task.id === drawn) ?? snap.operator.tasks.find(task => task.id.startsWith(drawn)))?.id;
|
|
653
|
+
if (id !== undefined && !usedTasks.has(id)) {
|
|
654
|
+
usedTasks.add(id);
|
|
655
|
+
targets.push({ ...row, kind: "task", id });
|
|
606
656
|
}
|
|
607
657
|
}
|
|
608
658
|
else {
|
|
@@ -625,8 +675,8 @@ export async function runConsolidatedCockpit(options) {
|
|
|
625
675
|
}
|
|
626
676
|
}
|
|
627
677
|
committedTargets = targets;
|
|
628
|
-
}, children: state.view === "home" ? _jsx(ShellGridFlow, { width: p.bodyColumns, columns: p.columns < 120 ? 2 : 3, rowHeight: p.rows < 20 ? 1 : p.columns < 120 ? 2 : 3, children: _jsx(HomeView, { width: p.bodyColumns, model: homeModel, focused: focused, onOpenPark: id => { runSession = { ...runSession, selection: Math.max(0, snap.operator.tasks.findIndex(t => t.id === id)) }; state = { ...state, view: "run", scroll: 0 }; publish(); }, onOpenDiagnostic: id => { state = { ...state, overlay: [id], overlayOffset: 0 }; publish(); }, onOpenEvidence: openEvidence, onSelectNeedsYou: reportHomeNeedsYouSelection }, String(homeModel.needsYou.length > 0)) }) :
|
|
629
|
-
state.view === "run" ? _jsx(RunView, { snapshot: snap.operator, rows: snap.journal.history, page: source.page, graph: snap.graph.value, decisions: visibleDecisions, session: runSession, columns: p.bodyColumns, run: approvalRunOwner(cwd, runId) }) :
|
|
678
|
+
}, children: state.view === "home" ? _jsx(ShellGridFlow, { width: p.bodyColumns, columns: p.columns < 120 ? 2 : 3, rowHeight: p.rows < 20 ? 1 : p.columns < 120 ? 2 : 3, children: _jsx(HomeView, { width: p.bodyColumns, model: homeModel, focused: focused, onOpenPark: id => { runSession = { ...runSession, selection: Math.max(0, snap.operator.tasks.findIndex(t => t.id === id)) }; state = { ...state, view: "run", scroll: 0 }; followSelection(); publish(); }, onOpenDiagnostic: id => { state = { ...state, overlay: [id], overlayOffset: 0 }; publish(); }, onOpenEvidence: openEvidence, onSelectNeedsYou: reportHomeNeedsYouSelection }, String(homeModel.needsYou.length > 0)) }) :
|
|
679
|
+
state.view === "run" ? _jsx(RunView, { snapshot: snap.operator, rows: snap.journal.history, page: source.page, graph: snap.graph.value, decisions: visibleDecisions, session: runSession, columns: p.bodyColumns, run: approvalRunOwner(cwd, runId), now: options.now }) :
|
|
630
680
|
_jsx(ShellJournalSelection.Provider, { value: reportEvidenceSelection, children: _jsx(EvidenceView, { width: p.bodyColumns, model: { ...model, journal: evidenceRows }, focusEvidence: selectedEvidence, focused: focused, onExport: beginExport, onSelect: id => { selectedEvidence = id; openDetail(); publish(); } }, `${state.query}:${evidenceNavigation}`) }) });
|
|
631
681
|
}
|
|
632
682
|
app = render(_jsx(App, {}), { stdin, stdout: output, debug: options.debug, exitOnCtrlC: false, patchConsole: false });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
2
|
import type { ApprovalRunOwner } from "../../cli/commands/approve.js";
|
|
3
3
|
import { type GateName, type RunGraph } from "../../graph/schema.js";
|
|
4
4
|
import type { JournalEvent } from "../../run/journal.js";
|
|
@@ -83,8 +83,8 @@ export declare function applyRunViewKey(session: RunViewSession, event: RunViewK
|
|
|
83
83
|
* a closed run whose record lacks a bucket says so rather than reading absence as empty.
|
|
84
84
|
*/
|
|
85
85
|
export declare function runSummaryLine(s: OperatorSnapshot): string;
|
|
86
|
-
/**
|
|
87
|
-
export declare
|
|
86
|
+
/** The board's footer keys merged with the cockpit's own — every one of them handled by this view or the shell. */
|
|
87
|
+
export declare const RUN_VIEW_KEYS: readonly string[];
|
|
88
88
|
export interface RunViewProps {
|
|
89
89
|
readonly snapshot: OperatorSnapshot;
|
|
90
90
|
/** Journal rows the store retains (C2 `journal.history`); evidence outside them reaches `page`. */
|
|
@@ -95,6 +95,8 @@ export interface RunViewProps {
|
|
|
95
95
|
readonly session: RunViewSession;
|
|
96
96
|
readonly columns: number;
|
|
97
97
|
readonly run: ApprovalRunOwner;
|
|
98
|
+
/** The clock the board header reads; injectable so a pinned frame does not drift. */
|
|
99
|
+
readonly now?: () => number;
|
|
98
100
|
}
|
|
99
|
-
/** The Run body
|
|
100
|
-
export declare function RunView({ snapshot, rows, page, graph, decisions, session, columns, run }: RunViewProps): ReactElement;
|
|
101
|
+
/** The Run body: the approved board (BD-1) over the fold, then the selected task's detail panels. */
|
|
102
|
+
export declare function RunView({ snapshot, rows, page, graph, decisions, session, columns, run, now }: RunViewProps): ReactElement;
|