tickmarkr 2.5.3 → 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.
- 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 +54 -6
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +195 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +144 -3
- package/dist/cli/commands/resume.js +6 -2
- package/dist/cli/commands/run.js +21 -2
- package/dist/cli/commands/verify.js +1 -1
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/compile/collateral.d.ts +2 -0
- package/dist/compile/collateral.js +50 -9
- package/dist/compile/ownership.d.ts +7 -0
- package/dist/compile/ownership.js +59 -22
- package/dist/config/config.d.ts +23 -8
- package/dist/config/config.js +42 -28
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +68 -11
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +14 -3
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +38 -10
- package/dist/drivers/orca.d.ts +99 -10
- package/dist/drivers/orca.js +586 -97
- package/dist/drivers/types.d.ts +1 -0
- package/dist/gates/baseline.d.ts +3 -0
- package/dist/gates/baseline.js +2 -1
- package/dist/gates/cache.d.ts +100 -0
- package/dist/gates/cache.js +389 -0
- package/dist/gates/review.d.ts +35 -2
- package/dist/gates/review.js +64 -16
- package/dist/gates/run-gates.d.ts +5 -0
- package/dist/gates/run-gates.js +134 -18
- package/dist/gates/test-manifest.d.ts +99 -0
- package/dist/gates/test-manifest.js +389 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +49 -0
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.d.ts +13 -0
- package/dist/route/router.js +95 -17
- package/dist/run/daemon.d.ts +13 -0
- package/dist/run/daemon.js +398 -96
- package/dist/run/git.d.ts +35 -1
- package/dist/run/git.js +111 -10
- package/dist/run/journal.d.ts +14 -2
- package/dist/run/journal.js +78 -10
- package/dist/run/lease.d.ts +14 -0
- package/dist/run/lease.js +87 -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/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 +83 -31
- 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 +41 -27
- package/dist/tui/ink/fleet-app.js +204 -31
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +180 -99
package/dist/run/merge.js
CHANGED
|
@@ -2,8 +2,10 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { shq } from "../adapters/types.js";
|
|
4
4
|
import { ceilingKillResult, classifyFreshRunnerOutput, classifyRunnerOutput, fileCountDeficit, waitForCalmWindow, effectiveCeilingMs, fingerprint, freshFailures, } from "../gates/baseline.js";
|
|
5
|
+
import { evaluateManifestedTest, isVitestTestCommand } from "../gates/test-manifest.js";
|
|
6
|
+
import { computeVerificationIdentity, formatReusedDetails, getVerdictStore, getWorktreeTree, resolveStateDir, isInfraResult, } from "../gates/cache.js";
|
|
5
7
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
6
|
-
import { describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
8
|
+
import { describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
7
9
|
export function integrationBranch(cfg, runId) {
|
|
8
10
|
return `${cfg.integrationBranchPrefix}${runId}`;
|
|
9
11
|
}
|
|
@@ -120,6 +122,11 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
|
|
|
120
122
|
}
|
|
121
123
|
return results;
|
|
122
124
|
}
|
|
125
|
+
const stateDir = resolveStateDir(intWt, runDir);
|
|
126
|
+
const store = getVerdictStore(stateDir);
|
|
127
|
+
// Publish only after the entire verification completes. The daemon kills a cancelled
|
|
128
|
+
// verifier process, so completed early gates must remain in memory until then.
|
|
129
|
+
const pending = new Map();
|
|
123
130
|
for (const [gate, cmd] of gatesToRun) {
|
|
124
131
|
if (runStartCommands !== undefined) {
|
|
125
132
|
const expectedCmd = gate === "test"
|
|
@@ -150,6 +157,73 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
|
|
|
150
157
|
const entry = gate === "test" && (hasTipCommand || (!identity.test && identity.tipTest))
|
|
151
158
|
? baseline?.commands.tipTest
|
|
152
159
|
: baseline?.commands[gate];
|
|
160
|
+
// A distinct tipTest selects a different forgiveness entry even when the command
|
|
161
|
+
// converges with the task command. Include that effective entry in the identity.
|
|
162
|
+
const verificationBaseline = baseline ? { commands: { ...baseline.commands } } : undefined;
|
|
163
|
+
if (verificationBaseline) {
|
|
164
|
+
if (entry)
|
|
165
|
+
verificationBaseline.commands[gate] = entry;
|
|
166
|
+
else
|
|
167
|
+
delete verificationBaseline.commands[gate];
|
|
168
|
+
}
|
|
169
|
+
// A tip verifier may reuse only a completed green from its own scope.
|
|
170
|
+
const id = await computeVerificationIdentity({
|
|
171
|
+
worktree: intWt,
|
|
172
|
+
gate,
|
|
173
|
+
scope: "tip",
|
|
174
|
+
command: cmd,
|
|
175
|
+
baseline: verificationBaseline,
|
|
176
|
+
selectedSet: undefined,
|
|
177
|
+
capacity: resolvedCapacity(),
|
|
178
|
+
});
|
|
179
|
+
// Persistent reuse requires the run's durable journal. Without it there is no
|
|
180
|
+
// recorded cycle provenance to reconcile with the caller's capacity checks.
|
|
181
|
+
const hit = journalFound ? store.get(id) : undefined;
|
|
182
|
+
if (hit && id && hit.pass && !isInfraResult(hit)) {
|
|
183
|
+
results.push({
|
|
184
|
+
gate,
|
|
185
|
+
cmd,
|
|
186
|
+
pass: hit.pass,
|
|
187
|
+
exitCode: hit.exitCode ?? 0,
|
|
188
|
+
fingerprints: hit.meta?.fingerprints ?? [],
|
|
189
|
+
...(hit.meta?.forgiven ? { forgiven: true } : {}),
|
|
190
|
+
details: formatReusedDetails(hit.details, id),
|
|
191
|
+
...(hit.meta?.reportPath ? { reportPath: hit.meta.reportPath } : {}),
|
|
192
|
+
...(hit.meta?.spawnedCommand ? { spawnedCommand: hit.meta.spawnedCommand } : {}),
|
|
193
|
+
...(hit.meta?.artifact ? { artifact: hit.meta.artifact } : {}),
|
|
194
|
+
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (id && journalFound)
|
|
198
|
+
pending.set(gate, id);
|
|
199
|
+
// VL-1: tip verify and standalone verify read the SAME invocation-bound report the gate would —
|
|
200
|
+
// a detected vitest test command never reaches the stdout-count/file-count path below (this
|
|
201
|
+
// gate's own manifest is always the FULL suite; verify has no selected screen to hold). A
|
|
202
|
+
// scripted test command that is not the detected runner falls through unchanged.
|
|
203
|
+
if (gate === "test" && isVitestTestCommand(cmd, intWt)) {
|
|
204
|
+
const outcome = await evaluateManifestedTest(cmd, intWt, {
|
|
205
|
+
baselineDurations: entry?.fileDurations,
|
|
206
|
+
longestFile: entry?.longestFile,
|
|
207
|
+
overallCeilingMs: effectiveCeilingMs(entry),
|
|
208
|
+
artifactDir: runDir,
|
|
209
|
+
});
|
|
210
|
+
const artifact = join(runDir, `tip-verify-${gate}.log`);
|
|
211
|
+
if (!outcome.pass)
|
|
212
|
+
writeFileSync(artifact, outcome.details);
|
|
213
|
+
results.push({
|
|
214
|
+
gate,
|
|
215
|
+
cmd,
|
|
216
|
+
pass: outcome.pass,
|
|
217
|
+
exitCode: outcome.exitCode,
|
|
218
|
+
reportPath: outcome.reportPath,
|
|
219
|
+
spawnedCommand: outcome.meta.spawnedCommand,
|
|
220
|
+
fingerprints: outcome.meta.failingTests ?? [],
|
|
221
|
+
details: outcome.details,
|
|
222
|
+
...(outcome.classification ? { cause: outcome.classification } : {}),
|
|
223
|
+
...(outcome.pass ? {} : { artifact }),
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
153
227
|
// OBS-534: the ceiling is the BATTERY's, derived by effectiveCeilingMs from the same baseline entry
|
|
154
228
|
// this loop already reads for forgiveness two lines down — never the flat DEFAULT_SHELL_TIMEOUT_MS
|
|
155
229
|
// `sh` defaults to. A suite whose capture measured 600007ms carries a recorded 1800021ms ceiling;
|
|
@@ -216,7 +290,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
|
|
|
216
290
|
const pass = !deficit && (r.code === 0 || greenTeardown || forgiven);
|
|
217
291
|
if (!pass)
|
|
218
292
|
writeFileSync(artifact, raw);
|
|
219
|
-
|
|
293
|
+
const tipResult = {
|
|
220
294
|
gate,
|
|
221
295
|
cmd,
|
|
222
296
|
pass,
|
|
@@ -233,7 +307,21 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
|
|
|
233
307
|
...(forgiven && !deficit ? { forgiven: true } : {}),
|
|
234
308
|
...(cause ? { cause } : {}),
|
|
235
309
|
...(pass ? {} : { artifact }),
|
|
236
|
-
}
|
|
310
|
+
};
|
|
311
|
+
results.push(tipResult);
|
|
312
|
+
}
|
|
313
|
+
// Recheck through the Git runner before publishing: cancellation disables further
|
|
314
|
+
// spawns in the daemon child, and a command that changed the tree invalidates reuse.
|
|
315
|
+
const completedTree = pending.size ? await getWorktreeTree(intWt) : undefined;
|
|
316
|
+
for (const result of results) {
|
|
317
|
+
const id = pending.get(result.gate);
|
|
318
|
+
if (id && id.tree === completedTree && !isInfraResult(result)) {
|
|
319
|
+
store.set(id, { ...result, meta: {
|
|
320
|
+
fingerprints: result.fingerprints, forgiven: result.forgiven,
|
|
321
|
+
reportPath: result.reportPath, spawnedCommand: result.spawnedCommand,
|
|
322
|
+
artifact: result.artifact,
|
|
323
|
+
} });
|
|
324
|
+
}
|
|
237
325
|
}
|
|
238
326
|
return results;
|
|
239
327
|
}
|
|
@@ -30,11 +30,19 @@ export interface OperatorTask {
|
|
|
30
30
|
evidence?: EvidenceIdentity;
|
|
31
31
|
mergeEvidence?: EvidenceIdentity;
|
|
32
32
|
gates: Record<string, OperatorGate>;
|
|
33
|
+
/** BD-1: the board's per-task counters — latest dispatch channel, review verdicts drawn, human parks. */
|
|
34
|
+
channel?: string;
|
|
35
|
+
reviewRounds: number;
|
|
36
|
+
parks: number;
|
|
33
37
|
}
|
|
34
38
|
export interface OperatorSnapshot {
|
|
35
39
|
sequence: number;
|
|
36
40
|
observedAt: number;
|
|
37
41
|
lastEventAt?: string;
|
|
42
|
+
firstEventAt?: string;
|
|
43
|
+
/** BD-1 header counts: run-resume and escalation rows seen. */
|
|
44
|
+
resumes: number;
|
|
45
|
+
escalations: number;
|
|
38
46
|
lifecycle: "PENDING" | "RUNNING" | "PARTIAL" | "APPROVED" | "COMPLETE" | "UNKNOWN";
|
|
39
47
|
label: string;
|
|
40
48
|
green: boolean;
|
|
@@ -63,6 +71,9 @@ export declare class OperatorStateFold {
|
|
|
63
71
|
private approved;
|
|
64
72
|
private tipFailed;
|
|
65
73
|
private lastEventAt?;
|
|
74
|
+
private firstEventAt?;
|
|
75
|
+
private resumes;
|
|
76
|
+
private escalations;
|
|
66
77
|
private passed;
|
|
67
78
|
private total;
|
|
68
79
|
apply(record: OperatorRecord): void;
|
|
@@ -16,11 +16,19 @@ export class OperatorStateFold {
|
|
|
16
16
|
approved = false;
|
|
17
17
|
tipFailed = false;
|
|
18
18
|
lastEventAt;
|
|
19
|
+
firstEventAt;
|
|
20
|
+
resumes = 0;
|
|
21
|
+
escalations = 0;
|
|
19
22
|
passed = 0;
|
|
20
23
|
total = 0;
|
|
21
24
|
apply(record) {
|
|
22
25
|
const { event: e } = record;
|
|
23
26
|
this.lastEventAt = e.ts;
|
|
27
|
+
this.firstEventAt ??= e.ts;
|
|
28
|
+
if (e.event === "run-resume")
|
|
29
|
+
this.resumes++;
|
|
30
|
+
if (e.event === "escalation")
|
|
31
|
+
this.escalations++;
|
|
24
32
|
const ref = identity(record);
|
|
25
33
|
if (e.event === "run-start" || e.event === "run-resume") {
|
|
26
34
|
if (e.event === "run-start" && !this.start) {
|
|
@@ -72,6 +80,9 @@ export class OperatorStateFold {
|
|
|
72
80
|
t.path = typeof e.data.worktree === "string" ? e.data.worktree : typeof e.data.cwd === "string" ? e.data.cwd : undefined;
|
|
73
81
|
t.pane = typeof e.data.pane === "string" ? e.data.pane : undefined;
|
|
74
82
|
t.alarmMs = typeof e.data.alarmMs === "number" ? e.data.alarmMs : undefined;
|
|
83
|
+
const a = e.data.assignment;
|
|
84
|
+
if (a && typeof a.adapter === "string" && typeof a.model === "string")
|
|
85
|
+
t.channel = `${a.adapter}:${a.model}`;
|
|
75
86
|
}
|
|
76
87
|
if (e.event === "task-done")
|
|
77
88
|
t.state = "completed";
|
|
@@ -85,6 +96,7 @@ export class OperatorStateFold {
|
|
|
85
96
|
t.state = "failed";
|
|
86
97
|
if (e.event === "task-human") {
|
|
87
98
|
t.state = "human";
|
|
99
|
+
t.parks++;
|
|
88
100
|
t.parkKind = typeof e.data.kind === "string" ? e.data.kind : undefined;
|
|
89
101
|
}
|
|
90
102
|
if (e.event === "task-blocked")
|
|
@@ -98,6 +110,8 @@ export class OperatorStateFold {
|
|
|
98
110
|
if (typeof gate === "string" && GATE_NAMES.includes(gate)) {
|
|
99
111
|
if (e.event === "gate-start")
|
|
100
112
|
t.gates[gate] = { state: "running", evidence: ref };
|
|
113
|
+
if (e.event === "gate-result" && gate === "review")
|
|
114
|
+
t.reviewRounds++;
|
|
101
115
|
if (e.event === "gate-result")
|
|
102
116
|
t.gates[gate] = {
|
|
103
117
|
state: e.data.disabled === true ? "disabled" : e.data.skipped === true ? "not-run" : e.data.pass === true ? "passed" : e.data.pass === false ? "failed" : "unknown", evidence: ref,
|
|
@@ -107,7 +121,7 @@ export class OperatorStateFold {
|
|
|
107
121
|
task(id) {
|
|
108
122
|
let t = this.tasks.get(id);
|
|
109
123
|
if (!t) {
|
|
110
|
-
t = { id, state: "unknown", merged: false, dispatches: 0, gates: emptyGates() };
|
|
124
|
+
t = { id, state: "unknown", merged: false, dispatches: 0, reviewRounds: 0, parks: 0, gates: emptyGates() };
|
|
111
125
|
this.tasks.set(id, t);
|
|
112
126
|
}
|
|
113
127
|
return t;
|
|
@@ -117,7 +131,7 @@ export class OperatorStateFold {
|
|
|
117
131
|
const comparable = this.comparableTo(hash);
|
|
118
132
|
const ids = comparable ? [...graph.tasks.map(t => t.id), ...[...this.tasks.keys()].filter(id => !graph.tasks.some(t => t.id === id))] : [...this.tasks.keys()];
|
|
119
133
|
const tasks = ids.map(id => {
|
|
120
|
-
const t = this.tasks.get(id) ?? { id, state: "unknown", merged: false, dispatches: 0, gates: emptyGates() };
|
|
134
|
+
const t = this.tasks.get(id) ?? { id, state: "unknown", merged: false, dispatches: 0, reviewRounds: 0, parks: 0, gates: emptyGates() };
|
|
121
135
|
return { ...t, title: comparable ? graph.tasks.find(g => g.id === id)?.title : undefined, gates: Object.fromEntries(Object.entries(t.gates).map(([g, cell]) => [g, { ...cell }])) };
|
|
122
136
|
});
|
|
123
137
|
const buckets = Object.fromEntries(bucketNames.map(k => [k, this.end?.buckets[k] === undefined ? undefined : [...this.end.buckets[k]]]));
|
|
@@ -134,7 +148,7 @@ export class OperatorStateFold {
|
|
|
134
148
|
&& bucketNames.every(k => buckets[k]?.length === 0);
|
|
135
149
|
const lifecycle = !readable || !this.start ? "UNKNOWN" : this.active ? "RUNNING" : this.approved ? "APPROVED" : green ? "COMPLETE" : this.end ? "PARTIAL" : "PENDING";
|
|
136
150
|
return {
|
|
137
|
-
sequence, observedAt, lastEventAt: this.lastEventAt, lifecycle,
|
|
151
|
+
sequence, observedAt, lastEventAt: this.lastEventAt, firstEventAt: this.firstEventAt, resumes: this.resumes, escalations: this.escalations, lifecycle,
|
|
138
152
|
label: this.approved ? "approved; resume required" : lifecycle,
|
|
139
153
|
green, currentTip, comparable, comparison: comparable ? "matching graph" : "not comparable",
|
|
140
154
|
merged,
|
|
@@ -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[];
|