tickmarkr 1.71.0 → 1.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/codex.js +13 -3
- package/dist/cli/commands/approve.js +21 -3
- package/dist/cli/commands/fleet.d.ts +2 -0
- package/dist/cli/commands/fleet.js +212 -449
- package/dist/cli/commands/resume.js +10 -2
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +219 -30
- package/dist/cli/commands/ui.d.ts +6 -2
- package/dist/cli/commands/ui.js +2 -4
- package/dist/config/config.d.ts +4 -0
- package/dist/config/config.js +6 -1
- package/dist/drivers/herdr.d.ts +6 -0
- package/dist/drivers/herdr.js +169 -71
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +10 -5
- package/dist/gates/acceptance.js +41 -17
- package/dist/route/preference.d.ts +2 -1
- package/dist/route/preference.js +8 -2
- package/dist/run/daemon.d.ts +1 -0
- package/dist/run/daemon.js +139 -4
- package/dist/run/git.d.ts +2 -0
- package/dist/run/git.js +55 -0
- package/dist/run/journal.d.ts +6 -1
- package/dist/run/journal.js +28 -1
- package/dist/tui/ink/components.d.ts +38 -0
- package/dist/tui/ink/components.js +42 -0
- package/dist/tui/ink/fleet-app.d.ts +113 -0
- package/dist/tui/ink/fleet-app.js +678 -0
- package/dist/tui/ink/studio-app.d.ts +59 -0
- package/dist/tui/ink/studio-app.js +320 -0
- package/package.json +4 -1
- package/dist/tui/app.d.ts +0 -72
- package/dist/tui/app.js +0 -334
- package/dist/tui/engine.d.ts +0 -52
- package/dist/tui/engine.js +0 -92
- package/dist/tui/frame.d.ts +0 -22
- package/dist/tui/frame.js +0 -56
- package/dist/tui/input.d.ts +0 -41
- package/dist/tui/input.js +0 -100
- package/dist/tui/views/consult-dossier.d.ts +0 -49
- package/dist/tui/views/consult-dossier.js +0 -169
- package/dist/tui/views/diff-modal.d.ts +0 -9
- package/dist/tui/views/diff-modal.js +0 -19
- package/dist/tui/views/fleet-view.d.ts +0 -91
- package/dist/tui/views/fleet-view.js +0 -337
- package/dist/tui/views/preview-view.d.ts +0 -7
- package/dist/tui/views/preview-view.js +0 -80
- package/dist/tui/views/profile-view.d.ts +0 -15
- package/dist/tui/views/profile-view.js +0 -93
- package/dist/tui/views/routing-view.d.ts +0 -48
- package/dist/tui/views/routing-view.js +0 -234
- package/dist/tui/views/runs-view.d.ts +0 -70
- package/dist/tui/views/runs-view.js +0 -387
|
@@ -8,17 +8,25 @@ const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.b
|
|
|
8
8
|
export async function resume(argv, cwd = process.cwd()) {
|
|
9
9
|
const runId = argv[0];
|
|
10
10
|
if (!runId)
|
|
11
|
-
throw new Error("usage: tickmarkr resume <run-id>");
|
|
11
|
+
throw new Error("usage: tickmarkr resume <run-id> [--graph-changed] [--retry-failed]");
|
|
12
12
|
// T3: --graph-changed is the operator's audited release of the engagement-identity guard (Sol #2 /
|
|
13
13
|
// Fable F2) — the daemon refuses a mismatched/unbound journal unless this is set, then journals a
|
|
14
14
|
// graph-rehash event naming both hashes. Strip the flag before runId resolution so a bare id still wins.
|
|
15
15
|
const graphChanged = argv.includes("--graph-changed");
|
|
16
|
+
const retryFailed = argv.includes("--retry-failed");
|
|
16
17
|
const cfg = loadConfig(cwd);
|
|
17
18
|
const collisions = denyPreferCollisions(cfg);
|
|
18
19
|
if (collisions.length) {
|
|
19
20
|
throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
|
|
20
21
|
}
|
|
21
|
-
const s = await runDaemon(cwd, {
|
|
22
|
+
const s = await runDaemon(cwd, {
|
|
23
|
+
runId,
|
|
24
|
+
resume: true,
|
|
25
|
+
graphChanged,
|
|
26
|
+
retryFailed,
|
|
27
|
+
driver: pickDriver(cfg),
|
|
28
|
+
narrate: (event) => console.log(formatJournalNarration(event)),
|
|
29
|
+
});
|
|
22
30
|
const out = `resumed ${s.runId} — ${formatSummary(s)}`;
|
|
23
31
|
return { out, code: summaryGreen(s) ? 0 : 2 };
|
|
24
32
|
}
|
|
@@ -3,6 +3,8 @@ import { type JournalEvent } from "../../run/journal.js";
|
|
|
3
3
|
export type StatusOpts = {
|
|
4
4
|
iterations?: number;
|
|
5
5
|
sleep?: (ms: number) => Promise<void>;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
readWorkerOutput?: (taskId: string, attempt: number, runId: string) => Promise<string | undefined>;
|
|
6
8
|
};
|
|
7
9
|
export declare const GATE_KEYS: {
|
|
8
10
|
readonly build: "B";
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { BANNER, GLYPHS, dim, fail, legend, ok, rule, statusRow, title, warn } from "../../brand.js";
|
|
2
|
+
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
3
|
+
import { formatOwnedName } from "../../drivers/types.js";
|
|
2
4
|
import { blockedTasks, graphDefinitionHash, loadGraph } from "../../graph/graph.js";
|
|
3
5
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
4
6
|
import { foldActivity } from "../../run/activity.js";
|
|
5
7
|
import { Journal, engagementComparable, isQualityFailureParkKind, recordedTaskFailureKind, runHasEnded, } from "../../run/journal.js";
|
|
8
|
+
import { normalizeStallSnapshot } from "../../run/stall.js";
|
|
6
9
|
// ponytail: fixed 2s refresh; promote to config.visibility.* only when an operator asks.
|
|
7
10
|
const REFRESH_MS = 2000;
|
|
8
11
|
const NOT_COMPARABLE_NOTICE = "graph recompiled since this run — task states not comparable; run `tickmarkr run` to execute";
|
|
@@ -10,6 +13,87 @@ const NOT_COMPARABLE_NOTICE = "graph recompiled since this run — task states n
|
|
|
10
13
|
// first frame, so a live `--watch` printed once and exited 0 (OBS-11). Never unref this.
|
|
11
14
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
15
|
export const GATE_KEYS = { build: "B", test: "T", lint: "L", evidence: "E", scope: "S", acceptance: "A", review: "R" };
|
|
16
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
17
|
+
const ASCII_SPINNER = ["|", "/", "-", "\\"];
|
|
18
|
+
const SAVE_TERMINAL_TITLE = "\x1b[22;0t";
|
|
19
|
+
const RESTORE_TERMINAL_TITLE = "\x1b[23;0t";
|
|
20
|
+
const taskPhase = (value) => {
|
|
21
|
+
if (value === "worker" || value === "gates" || value === "judge" || value === "review" || value === "merge")
|
|
22
|
+
return value;
|
|
23
|
+
if (typeof value !== "string" || !value.startsWith("gate:"))
|
|
24
|
+
return undefined;
|
|
25
|
+
return GATE_NAMES.includes(value.slice("gate:".length)) ? value : undefined;
|
|
26
|
+
};
|
|
27
|
+
const phaseGate = (phase) => {
|
|
28
|
+
if (phase === "judge")
|
|
29
|
+
return "acceptance";
|
|
30
|
+
if (phase === "review")
|
|
31
|
+
return "review";
|
|
32
|
+
return phase.startsWith("gate:") ? phase.slice("gate:".length) : undefined;
|
|
33
|
+
};
|
|
34
|
+
// A phase is live only between its append-only start marker and the matching real outcome. A newer
|
|
35
|
+
// start for the same task supersedes the prior phase (gates → named gate, worker retry → worker).
|
|
36
|
+
// No clock participates in this fold: time only decorates the derived live interval at render time.
|
|
37
|
+
const livePhases = (events) => {
|
|
38
|
+
const live = new Map();
|
|
39
|
+
for (let order = 0; order < events.length; order++) {
|
|
40
|
+
const event = events[order];
|
|
41
|
+
if (event.event === "run-start" || event.event === "run-resume" || event.event === "run-end" || event.event === "superseded") {
|
|
42
|
+
live.clear();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (event.event === "phase-start" && event.taskId) {
|
|
46
|
+
const phase = taskPhase(event.data.phase);
|
|
47
|
+
const startedAt = Date.parse(event.ts);
|
|
48
|
+
const attempt = typeof event.data.attempt === "number" && Number.isInteger(event.data.attempt) && event.data.attempt >= 0
|
|
49
|
+
? event.data.attempt
|
|
50
|
+
: 0;
|
|
51
|
+
if (phase && Number.isFinite(startedAt))
|
|
52
|
+
live.set(event.taskId, { taskId: event.taskId, phase, startedAt, order, attempt });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!event.taskId)
|
|
56
|
+
continue;
|
|
57
|
+
const active = live.get(event.taskId);
|
|
58
|
+
if (!active)
|
|
59
|
+
continue;
|
|
60
|
+
const gate = phaseGate(active.phase);
|
|
61
|
+
const matched = (active.phase === "worker" && event.event === "worker-result")
|
|
62
|
+
|| (active.phase === "gates" && event.event === "gate-result")
|
|
63
|
+
|| (gate !== undefined && event.event === "gate-result" && event.data.gate === gate)
|
|
64
|
+
|| (active.phase === "merge" && event.event === "merge")
|
|
65
|
+
|| event.event === "task-done"
|
|
66
|
+
|| event.event === "task-failed"
|
|
67
|
+
|| event.event === "task-human"
|
|
68
|
+
|| event.event === "task-approved"
|
|
69
|
+
|| event.event === "task-dispatch";
|
|
70
|
+
if (matched)
|
|
71
|
+
live.delete(event.taskId);
|
|
72
|
+
}
|
|
73
|
+
return live;
|
|
74
|
+
};
|
|
75
|
+
const phaseLabel = (phase) => phase.startsWith("gate:") ? `gate ${phase.slice("gate:".length)}` : phase;
|
|
76
|
+
const fmtElapsed = (ms) => {
|
|
77
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
78
|
+
if (seconds < 60)
|
|
79
|
+
return `${seconds}s`;
|
|
80
|
+
const minutes = Math.floor(seconds / 60);
|
|
81
|
+
if (minutes < 60)
|
|
82
|
+
return `${minutes}m${seconds % 60}s`;
|
|
83
|
+
const hours = Math.floor(minutes / 60);
|
|
84
|
+
return `${hours}h${minutes % 60}m${seconds % 60}s`;
|
|
85
|
+
};
|
|
86
|
+
const workerOutputAge = (phase, now, worker) => now - (worker?.phaseStartedAt === phase.startedAt && worker.hasOutput ? worker.lastOutputAt : phase.startedAt);
|
|
87
|
+
const phaseDetail = (phase, now, worker) => {
|
|
88
|
+
const elapsed = fmtElapsed(now - phase.startedAt);
|
|
89
|
+
if (phase.phase !== "worker")
|
|
90
|
+
return `${phaseLabel(phase.phase)} · ${elapsed} elapsed`;
|
|
91
|
+
const age = fmtElapsed(workerOutputAge(phase, now, worker));
|
|
92
|
+
const output = worker?.phaseStartedAt === phase.startedAt && worker.hasOutput
|
|
93
|
+
? `last output ${age} ago`
|
|
94
|
+
: `no output ${age}`;
|
|
95
|
+
return `${phaseLabel(phase.phase)} · ${elapsed} elapsed · ${output}`;
|
|
96
|
+
};
|
|
13
97
|
const attemptStartIdx = (events, taskId) => {
|
|
14
98
|
let idx = -1;
|
|
15
99
|
for (let i = 0; i < events.length; i++) {
|
|
@@ -114,11 +198,11 @@ const terminalFailureCause = (events) => {
|
|
|
114
198
|
}
|
|
115
199
|
return undefined;
|
|
116
200
|
};
|
|
117
|
-
const liveness = (events) => {
|
|
201
|
+
const liveness = (events, now = Date.now()) => {
|
|
118
202
|
const last = events.at(-1);
|
|
119
203
|
if (!last)
|
|
120
204
|
return "last event unknown · daemon pid unknown";
|
|
121
|
-
const age = fmtAge(
|
|
205
|
+
const age = fmtAge(now - Date.parse(last.ts));
|
|
122
206
|
const pid = daemonPid(events);
|
|
123
207
|
const cause = terminalFailureCause(events);
|
|
124
208
|
if (pid === undefined)
|
|
@@ -136,7 +220,7 @@ const liveness = (events) => {
|
|
|
136
220
|
} // EPERM ⇒ alive
|
|
137
221
|
return `last event ${age} ago · daemon pid ${pid} ${state}${cause ? ` · ${cause}` : ""}`;
|
|
138
222
|
};
|
|
139
|
-
const renderFrame = (cwd) => {
|
|
223
|
+
const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness = new Map()) => {
|
|
140
224
|
const g = loadGraph(cwd);
|
|
141
225
|
const runId = Journal.latestRunId(cwd, { withJournal: true });
|
|
142
226
|
const assignments = new Map();
|
|
@@ -179,6 +263,12 @@ const renderFrame = (cwd) => {
|
|
|
179
263
|
const width = process.stdout.columns ?? 120;
|
|
180
264
|
const done = effective.tasks.filter((t) => t.status === "done").length;
|
|
181
265
|
const ended = comparable && runHasEnded(events);
|
|
266
|
+
const taskIds = new Set(g.tasks.map((task) => task.id));
|
|
267
|
+
const phases = comparable ? livePhases(events) : new Map();
|
|
268
|
+
for (const taskId of phases.keys())
|
|
269
|
+
if (!taskIds.has(taskId))
|
|
270
|
+
phases.delete(taskId);
|
|
271
|
+
const hotPhase = [...phases.values()].sort((a, b) => a.order - b.order).at(-1);
|
|
182
272
|
const cells = g.tasks.map((t) => {
|
|
183
273
|
const st = replayed?.get(t.id) ?? t.status;
|
|
184
274
|
const failureKind = comparable ? recordedTaskFailureKind(events, t.id) : undefined;
|
|
@@ -188,27 +278,34 @@ const renderFrame = (cwd) => {
|
|
|
188
278
|
const redTier = terminal && (ended
|
|
189
279
|
|| (failureKind !== undefined && isQualityFailureParkKind(failureKind))
|
|
190
280
|
|| (st === "failed" && failureKind === undefined));
|
|
191
|
-
const
|
|
192
|
-
const
|
|
281
|
+
const livePhase = phases.get(t.id);
|
|
282
|
+
const isStarved = !livePhase && starved.has(t.id);
|
|
283
|
+
const phrase = livePhase ? phaseDetail(livePhase, now, workerLiveness.get(t.id)) : isStarved ? undefined : activity.cells.get(t.id);
|
|
193
284
|
const label = isStarved ? " starved" : phrase ? ` ${phrase}` : "";
|
|
194
285
|
const channel = assignments.get(t.id) ?? "-";
|
|
195
286
|
const ctx = contexts.get(t.id);
|
|
196
287
|
const assignCol = ctx !== undefined ? `${channel}${divider}ctx ${ctx}` : channel;
|
|
197
|
-
return { t, st, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, states: comparable ? gateStates(t, events) : defaultGateStates(t) };
|
|
288
|
+
return { t, st, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, states: comparable ? gateStates(t, events) : defaultGateStates(t) };
|
|
198
289
|
});
|
|
199
290
|
if (!unicode) {
|
|
200
|
-
// machine/CI surface —
|
|
201
|
-
|
|
291
|
+
// machine/CI surface — journals without phase-start stay byte-identical; new phase-aware frames
|
|
292
|
+
// use an ASCII spinner so pipes never receive terminal-only braille/ANSI.
|
|
293
|
+
const rows = cells.map(({ t, st, label, assignCol, livePhase, states }) => {
|
|
202
294
|
const chain = gateChain(states, false);
|
|
203
|
-
const prefix = ` ${taskBox(st)} ${t.id} `;
|
|
204
|
-
const suffix = ` ${chain} ${String(st)}${label} ${assignCol}`;
|
|
295
|
+
const prefix = livePhase ? ` ${ASCII_SPINNER[animationFrame % ASCII_SPINNER.length]} ${t.id} ` : ` ${taskBox(st)} ${t.id} `;
|
|
296
|
+
const suffix = ` ${chain} ${livePhase ? "running" : String(st)}${label} ${assignCol}`;
|
|
205
297
|
return `${prefix}${shortGoal(t.goal, Math.max(0, width - prefix.length - suffix.length))}${suffix}`;
|
|
206
298
|
});
|
|
207
299
|
const header = runId
|
|
208
|
-
? `tickmarkr status${divider}run ${runId}${supersededBy ? `${divider}superseded by ${supersededBy}` : ""}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
|
|
300
|
+
? `tickmarkr status${divider}run ${runId}${supersededBy ? `${divider}superseded by ${supersededBy}` : ""}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events, now).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
|
|
209
301
|
: `tickmarkr status${divider}no runs yet${divider}${done}/${g.tasks.length} done`;
|
|
210
302
|
const legendLine = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
211
|
-
return
|
|
303
|
+
return {
|
|
304
|
+
content: [header, legendLine, ...rows].join("\n"),
|
|
305
|
+
...(hotPhase ? { hotPhase } : {}),
|
|
306
|
+
...(runId ? { runId } : {}),
|
|
307
|
+
workerPhases: [...phases.values()].filter((phase) => phase.phase === "worker"),
|
|
308
|
+
};
|
|
212
309
|
}
|
|
213
310
|
// TTY: cockpit frame composed through src/brand.ts (CLI-DESIGN.md) — dominant run title,
|
|
214
311
|
// dim chrome, semantic color only on verdicts, completion gauge, and column-aligned status
|
|
@@ -219,7 +316,7 @@ const renderFrame = (cwd) => {
|
|
|
219
316
|
const gaugeCells = 10;
|
|
220
317
|
const fill = g.tasks.length ? Math.round((done / g.tasks.length) * gaugeCells) : 0;
|
|
221
318
|
const gauge = (fill ? (anyFailed ? fail : ok)("█".repeat(fill)) : "") + (fill < gaugeCells ? dim("░".repeat(gaugeCells - fill)) : "");
|
|
222
|
-
const live = liveness(events)
|
|
319
|
+
const live = liveness(events, now)
|
|
223
320
|
.replace(/\bdead\b/, fail("dead"))
|
|
224
321
|
.replace(/\bfinished\b/, dim("finished"))
|
|
225
322
|
.replace(/\balive\b/, ok("alive"))
|
|
@@ -241,7 +338,7 @@ const renderFrame = (cwd) => {
|
|
|
241
338
|
// aligned under the goal: gate chain, live activity phrase (or channel), ctx. Long channel names
|
|
242
339
|
// and activity phrases live on line 2 only, so they can never squeeze the goal or wrap line 1.
|
|
243
340
|
const taskVerdict = (c) => c.st === "done" ? "pass" : c.redTier ? "fail" : c.st === "failed" || c.st === "human" ? "warn" : "neutral";
|
|
244
|
-
const statusWord = (c) => c.redTier ? "failed" : c.st === "failed" ? "warn" : String(c.st);
|
|
341
|
+
const statusWord = (c) => c.livePhase ? "running" : c.redTier ? "failed" : c.st === "failed" ? "warn" : String(c.st);
|
|
245
342
|
const idW = Math.max(...cells.map((c) => c.t.id.length), 2);
|
|
246
343
|
// plain-text status suffixes for width math only — starved / failed gate names / approval hint
|
|
247
344
|
const suffixPlain = (c) => (c.isStarved ? " · starved" : "") + failedSuffix(c.states) + humanGateSuffix(c.t, c.st, c.states);
|
|
@@ -251,9 +348,11 @@ const renderFrame = (cwd) => {
|
|
|
251
348
|
const goalW = Math.max(8, ...goals.map((s) => s.length));
|
|
252
349
|
const indent = " ".repeat(idW + 5); // line 2 starts under the goal column
|
|
253
350
|
const rows = cells.map((c, i) => {
|
|
254
|
-
const { t, st, failureKind, redTier, states, isStarved, phrase, channel, ctx } = c;
|
|
351
|
+
const { t, st, failureKind, redTier, states, isStarved, phrase, channel, ctx, livePhase } = c;
|
|
255
352
|
const word = statusWord(c);
|
|
256
|
-
const
|
|
353
|
+
const staleWorker = livePhase?.phase === "worker"
|
|
354
|
+
&& workerOutputAge(livePhase, now, workerLiveness.get(t.id)) >= 60_000;
|
|
355
|
+
const stWord = staleWorker ? warn(word) : st === "done" ? ok(word) : redTier ? fail(word) : st === "failed" || st === "human" ? warn(word) : word;
|
|
257
356
|
// a fail names its gate in words right here — the one moment gate identity is needed on a row
|
|
258
357
|
const f = failedGates(states);
|
|
259
358
|
const human = humanGateSuffix(t, st, states);
|
|
@@ -261,7 +360,10 @@ const renderFrame = (cwd) => {
|
|
|
261
360
|
(isStarved ? dot + fail("starved") : "") +
|
|
262
361
|
(f.length ? dot + fail(f.join(", ")) : "") +
|
|
263
362
|
(human ? dot + warn("awaiting approval") : "");
|
|
264
|
-
const
|
|
363
|
+
const taskLabel = `${t.id.padEnd(idW)} ${goals[i].padEnd(goalW)} ${statusCell}`;
|
|
364
|
+
const line1 = livePhase
|
|
365
|
+
? ` ${(staleWorker ? warn : dim)(SPINNER[animationFrame % SPINNER.length])} ${taskLabel}`
|
|
366
|
+
: ` ${statusRow(taskVerdict(c), taskLabel)}`;
|
|
265
367
|
// activity already names its channel for in-flight attempts — never repeat it
|
|
266
368
|
const detail = [
|
|
267
369
|
...(phrase ? [phrase, ...(phrase.includes(channel) || channel === "-" ? [] : [channel])] : [channel]),
|
|
@@ -272,28 +374,115 @@ const renderFrame = (cwd) => {
|
|
|
272
374
|
}).flat();
|
|
273
375
|
if (!nowLine.length)
|
|
274
376
|
rows.pop(); // cards end blank-separated; drop the dangling one
|
|
275
|
-
return
|
|
377
|
+
return {
|
|
378
|
+
content: [header, hr, gatesLegend, "", ...rows, ...nowLine].join("\n"),
|
|
379
|
+
...(hotPhase ? { hotPhase } : {}),
|
|
380
|
+
...(runId ? { runId } : {}),
|
|
381
|
+
workerPhases: [...phases.values()].filter((phase) => phase.phase === "worker"),
|
|
382
|
+
};
|
|
276
383
|
};
|
|
277
384
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
278
385
|
// cockpit surface: banner + frame on a TTY (doctor's pattern); pipes get the bare frame
|
|
279
|
-
if (!argv.includes("--watch"))
|
|
280
|
-
|
|
386
|
+
if (!argv.includes("--watch")) {
|
|
387
|
+
const { content } = renderFrame(cwd, opts.now?.() ?? Date.now());
|
|
388
|
+
return visual() ? BANNER + content : content;
|
|
389
|
+
}
|
|
281
390
|
const iterations = opts.iterations ?? Infinity;
|
|
282
391
|
const sleep = opts.sleep ?? defaultSleep;
|
|
392
|
+
const now = opts.now ?? Date.now;
|
|
283
393
|
const bounded = Number.isFinite(iterations);
|
|
284
394
|
const frames = [];
|
|
285
395
|
const sep = "\n---\n";
|
|
286
396
|
const tty = visual();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
397
|
+
const workerLiveness = new Map();
|
|
398
|
+
const herdr = HerdrDriver.available() ? new HerdrDriver() : undefined;
|
|
399
|
+
const readWorkerOutput = opts.readWorkerOutput ?? (herdr
|
|
400
|
+
? async (taskId, attempt, runId) => {
|
|
401
|
+
const name = formatOwnedName({ role: "worker", taskId, attempt, runId });
|
|
402
|
+
try {
|
|
403
|
+
return await herdr.read({ id: name, name, cwd }, 80);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
: undefined);
|
|
410
|
+
const observeWorkerOutput = async (active, runId, observedAt) => {
|
|
411
|
+
const activeIds = new Set(active.map((phase) => phase.taskId));
|
|
412
|
+
for (const taskId of workerLiveness.keys())
|
|
413
|
+
if (!activeIds.has(taskId))
|
|
414
|
+
workerLiveness.delete(taskId);
|
|
415
|
+
if (!readWorkerOutput || !runId)
|
|
416
|
+
return;
|
|
417
|
+
await Promise.all(active.map(async (phase) => {
|
|
418
|
+
const output = await readWorkerOutput(phase.taskId, phase.attempt, runId);
|
|
419
|
+
if (output === undefined)
|
|
420
|
+
return;
|
|
421
|
+
const snapshot = normalizeStallSnapshot(output);
|
|
422
|
+
const prior = workerLiveness.get(phase.taskId);
|
|
423
|
+
if (!prior || prior.phaseStartedAt !== phase.startedAt) {
|
|
424
|
+
const hasOutput = snapshot.trim().length > 0;
|
|
425
|
+
workerLiveness.set(phase.taskId, {
|
|
426
|
+
phaseStartedAt: phase.startedAt,
|
|
427
|
+
snapshot,
|
|
428
|
+
hasOutput,
|
|
429
|
+
lastOutputAt: hasOutput ? observedAt : phase.startedAt,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (snapshot !== prior.snapshot && snapshot.trim().length > 0) {
|
|
434
|
+
prior.hasOutput = true;
|
|
435
|
+
prior.lastOutputAt = observedAt;
|
|
436
|
+
}
|
|
437
|
+
prior.snapshot = snapshot;
|
|
438
|
+
}));
|
|
439
|
+
};
|
|
440
|
+
let titleSaved = false;
|
|
441
|
+
const restoreTitle = () => {
|
|
442
|
+
if (!titleSaved)
|
|
443
|
+
return;
|
|
444
|
+
titleSaved = false;
|
|
445
|
+
process.stdout.write(RESTORE_TERMINAL_TITLE);
|
|
446
|
+
};
|
|
447
|
+
const updateTitle = (hotPhase, nowMs) => {
|
|
448
|
+
if (!hotPhase) {
|
|
449
|
+
if (titleSaved) {
|
|
450
|
+
process.removeListener("exit", restoreTitle);
|
|
451
|
+
restoreTitle();
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!titleSaved) {
|
|
456
|
+
process.stdout.write(SAVE_TERMINAL_TITLE);
|
|
457
|
+
titleSaved = true;
|
|
458
|
+
process.once("exit", restoreTitle);
|
|
459
|
+
}
|
|
460
|
+
process.stdout.write(`\x1b]0;⏳ ${hotPhase.taskId} ${phaseLabel(hotPhase.phase)} ${fmtElapsed(nowMs - hotPhase.startedAt)}\x07`);
|
|
461
|
+
};
|
|
462
|
+
try {
|
|
463
|
+
for (let i = 0; i < iterations; i++) {
|
|
464
|
+
const nowMs = now();
|
|
465
|
+
const frame = renderFrame(cwd, nowMs, i, workerLiveness);
|
|
466
|
+
if (tty) {
|
|
467
|
+
updateTitle(frame.hotPhase, nowMs);
|
|
468
|
+
process.stdout.write(`\x1b[2J\x1b[H${BANNER}${frame.content}\n${legend(` watching · refresh ${REFRESH_MS / 1000}s · ^C to quit`)}`);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
process.stdout.write(frame.content + sep);
|
|
472
|
+
}
|
|
473
|
+
if (bounded)
|
|
474
|
+
frames.push(frame.content);
|
|
475
|
+
if (i + 1 < iterations) {
|
|
476
|
+
await observeWorkerOutput(frame.workerPhases, frame.runId, nowMs);
|
|
477
|
+
await sleep(REFRESH_MS);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
if (titleSaved) {
|
|
483
|
+
process.removeListener("exit", restoreTitle);
|
|
484
|
+
restoreTitle();
|
|
485
|
+
}
|
|
297
486
|
}
|
|
298
487
|
return bounded ? frames.join(sep) : "";
|
|
299
488
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
type StudioIO = {
|
|
2
|
+
input: NodeJS.ReadStream;
|
|
3
|
+
output: NodeJS.WriteStream;
|
|
4
|
+
};
|
|
5
|
+
export declare function ui(_argv: string[], io?: Partial<StudioIO>): Promise<string | {
|
|
3
6
|
out: string;
|
|
4
7
|
code: number;
|
|
5
8
|
}>;
|
|
9
|
+
export {};
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { StudioApp } from "../../tui/app.js";
|
|
2
1
|
const NON_TTY_MSG = "tickmarkr ui: studio requires a TTY — use `tickmarkr fleet --print` or `tickmarkr status --watch` for line-mode output";
|
|
3
2
|
export async function ui(_argv, io = {}) {
|
|
4
3
|
const input = io.input ?? process.stdin;
|
|
@@ -6,8 +5,7 @@ export async function ui(_argv, io = {}) {
|
|
|
6
5
|
if (input.isTTY !== true || output.isTTY !== true) {
|
|
7
6
|
return { out: NON_TTY_MSG, code: 1 };
|
|
8
7
|
}
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
await app.exited;
|
|
8
|
+
const { runStudioInk } = await import("../../tui/ink/studio-app.js");
|
|
9
|
+
await runStudioInk({ input, output });
|
|
12
10
|
return "ui: closed";
|
|
13
11
|
}
|
package/dist/config/config.d.ts
CHANGED
|
@@ -109,6 +109,10 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
109
109
|
deny: z.ZodOptional<z.ZodObject<{
|
|
110
110
|
adapters: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
111
111
|
models: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
112
|
+
workers: z.ZodOptional<z.ZodObject<{
|
|
113
|
+
adapters: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
114
|
+
models: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
115
|
+
}, z.core.$strip>>;
|
|
112
116
|
}, z.core.$strip>>;
|
|
113
117
|
}, z.core.$strip>;
|
|
114
118
|
tiers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
package/dist/config/config.js
CHANGED
|
@@ -51,6 +51,9 @@ const PrefBlockSchema = z.object({
|
|
|
51
51
|
adapters: z.array(z.string()).optional(),
|
|
52
52
|
models: z.array(z.string()).optional(),
|
|
53
53
|
});
|
|
54
|
+
const DenyBlockSchema = PrefBlockSchema.extend({
|
|
55
|
+
workers: PrefBlockSchema.optional(),
|
|
56
|
+
});
|
|
54
57
|
// v1.20 REC-02: operator-maintained price table for the usage/cost report (src/report/cost.ts). Two
|
|
55
58
|
// channel economics, never conflated (spec cost model): API = tokens × per-Mtok rate; sub = flat plan
|
|
56
59
|
// amortized over a windows/month RANGE (+ an API-equivalent counterfactual when the model has a rate).
|
|
@@ -125,7 +128,7 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
125
128
|
// Pre-v1.21 doctor.json lacks per-model verdicts. Keep legacy unknown-is-routable behavior opt-in.
|
|
126
129
|
allowUnverifiedModels: z.boolean(),
|
|
127
130
|
allow: PrefBlockSchema.optional(),
|
|
128
|
-
deny:
|
|
131
|
+
deny: DenyBlockSchema.optional(),
|
|
129
132
|
}),
|
|
130
133
|
tiers: z.record(z.string(), TierEntrySchema),
|
|
131
134
|
pricing: z.record(z.string(), z.number()),
|
|
@@ -471,6 +474,8 @@ export function configTemplate(overlay) {
|
|
|
471
474
|
# deny: # optional fleet denylist; deny beats allow on conflict
|
|
472
475
|
# models:
|
|
473
476
|
# - pi:zai/glm-5.2 # OBS-57: pi passes run-start probe but hangs at finish without TICKMARKR_RESULT — remove after no-trailer demotion ships (v1.46 provider-outage taxonomy)
|
|
477
|
+
# workers: # optional worker-only deny; flat adapters/models still deny every role
|
|
478
|
+
# models: [kimi:kimi-code/k3]
|
|
474
479
|
# # incident-born deny/pin entries MUST name OBS id + root cause + removal condition (see docs/codebase/CONVENTIONS.md)
|
|
475
480
|
# # entry grammar: adapter id | model id | adapter:model (entries in either list accept all three forms)
|
|
476
481
|
# # a hint pinning a denied channel FAILS at plan time (RoutingError) — never silent reroute
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -12,12 +12,16 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
12
12
|
private groups;
|
|
13
13
|
private groupSerial;
|
|
14
14
|
private deliverySerial;
|
|
15
|
+
private dispatchLeases;
|
|
16
|
+
private deliveredPanes;
|
|
15
17
|
private ws;
|
|
16
18
|
private callerPane;
|
|
17
19
|
private watches;
|
|
18
20
|
constructor(bin?: string, workersPerTab?: number);
|
|
19
21
|
private serial;
|
|
20
22
|
private deliveryQueue;
|
|
23
|
+
private reserveDispatch;
|
|
24
|
+
private verifyPaneIdentityBinding;
|
|
21
25
|
private deliveryMatches;
|
|
22
26
|
static available(): boolean;
|
|
23
27
|
private herdr;
|
|
@@ -26,6 +30,8 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
26
30
|
private paneWidth;
|
|
27
31
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
28
32
|
private tabSlot;
|
|
33
|
+
private reclaimStaleLabel;
|
|
34
|
+
private staleLabelPanes;
|
|
29
35
|
private groupSlot;
|
|
30
36
|
private newGeneration;
|
|
31
37
|
private renameGroupTab;
|