tickmarkr 1.96.0 → 1.97.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/brand.d.ts +3 -0
- package/dist/brand.js +3 -1
- package/dist/cli/commands/status.js +171 -130
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +98 -5
package/dist/brand.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare function paneDispatchCommand(scriptPath: string): string;
|
|
|
26
26
|
export declare const BRAND_RAMP: readonly [84, 78, 41, 35];
|
|
27
27
|
/** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
|
|
28
28
|
export declare const brand: (s: string) => string;
|
|
29
|
+
/** Compact product chip — black ink on the terminal theme's ANSI green. */
|
|
30
|
+
export declare const brandChip: (s: string) => string;
|
|
29
31
|
/** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
|
|
30
32
|
export declare const ok: (s: string) => string;
|
|
31
33
|
/** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
|
|
@@ -39,6 +41,7 @@ export declare const bold: (s: string) => string;
|
|
|
39
41
|
/** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
|
|
40
42
|
export declare const TOKENS: {
|
|
41
43
|
readonly brand: (s: string) => string;
|
|
44
|
+
readonly brandChip: (s: string) => string;
|
|
42
45
|
readonly ok: (s: string) => string;
|
|
43
46
|
readonly fail: (s: string) => string;
|
|
44
47
|
readonly warn: (s: string) => string;
|
package/dist/brand.js
CHANGED
|
@@ -108,6 +108,8 @@ const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === u
|
|
|
108
108
|
const sgr = (code) => (s) => visual() ? `\x1b[${code}m${s}${R}` : s;
|
|
109
109
|
/** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
|
|
110
110
|
export const brand = sgr(`38;5;${BRAND_RAMP[2]}`);
|
|
111
|
+
/** Compact product chip — black ink on the terminal theme's ANSI green. */
|
|
112
|
+
export const brandChip = sgr("30;42");
|
|
111
113
|
/** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
|
|
112
114
|
export const ok = brand;
|
|
113
115
|
/** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
|
|
@@ -119,7 +121,7 @@ export const dim = sgr("2");
|
|
|
119
121
|
/** Emphasis (titles, selection, the product name) — bold. */
|
|
120
122
|
export const bold = sgr("1");
|
|
121
123
|
/** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
|
|
122
|
-
export const TOKENS = { brand, ok, fail, warn, dim, bold };
|
|
124
|
+
export const TOKENS = { brand, brandChip, ok, fail, warn, dim, bold };
|
|
123
125
|
/**
|
|
124
126
|
* The glyph vocabulary — plain characters only; color layers on via tokens so
|
|
125
127
|
* shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { brandChip, dim, fail, GLYPHS, legend, ok, rule, title, warn } from "../../brand.js";
|
|
4
4
|
import { DEFAULT_CONFIG, loadConfig } from "../../config/config.js";
|
|
5
5
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
6
6
|
import { formatOwnedName, parseOwnedName, } from "../../drivers/types.js";
|
|
@@ -13,7 +13,7 @@ import { desiredPanes } from "../../run/reconcile.js";
|
|
|
13
13
|
import { normalizeStallSnapshot } from "../../run/stall.js";
|
|
14
14
|
import { readSupervision, supervisionText } from "../../run/supervision.js";
|
|
15
15
|
import { deriveRunCockpitData, } from "../../tui/cockpit/derive.js";
|
|
16
|
-
import { COCKPIT_COLUMN_FLOOR
|
|
16
|
+
import { COCKPIT_COLUMN_FLOOR } from "../../tui/cockpit/layout.js";
|
|
17
17
|
import { cellWidth, fitCells, wrapCells } from "../../tui/cockpit/width.js";
|
|
18
18
|
// ponytail: fixed 2s refresh; promote to config.visibility.* only when an operator asks.
|
|
19
19
|
const REFRESH_MS = 2000;
|
|
@@ -376,11 +376,6 @@ export const gateChain = (states, unicode) => GATE_NAMES.map((gate, i) => unicod
|
|
|
376
376
|
? GATE_STATE_TOKEN[states[i]](gateBox(states[i], true))
|
|
377
377
|
: `${GATE_KEYS[gate]}${gateBox(states[i], false)}`).join(" ");
|
|
378
378
|
export const failedGates = (states) => GATE_NAMES.filter((_, i) => states[i] === "fail");
|
|
379
|
-
// plain-text form of the failed-gate words for column math; rendered with a dim dot + red names
|
|
380
|
-
const failedSuffix = (states) => {
|
|
381
|
-
const f = failedGates(states);
|
|
382
|
-
return f.length ? ` · ${f.join(", ")}` : "";
|
|
383
|
-
};
|
|
384
379
|
// a designed human gate parks the task BEFORE any gate result exists (daemon.ts execTask), so the
|
|
385
380
|
// awaited approval is named from task state alone — never from gate results. Failed gates win the
|
|
386
381
|
// cell when present: a post-approval park is not awaiting the designed gate. Plain text for column
|
|
@@ -401,19 +396,46 @@ export const shortGoal = (goal, max) => {
|
|
|
401
396
|
};
|
|
402
397
|
/** Columns the machine row always spends on the task title, however long its machinery segment runs. */
|
|
403
398
|
const MACHINE_TITLE_FLOOR = 24;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
399
|
+
// The approved task board derives an area from files[] rather than adding another graph field.
|
|
400
|
+
// Known tickmarkr domains keep their reviewed names; other repositories fall back to their first
|
|
401
|
+
// meaningful path segment so the shipped surface never turns a foreign project into UNMAPPED rows.
|
|
402
|
+
const TASK_AREA_RULES = [
|
|
403
|
+
["SPEC", [/^src\/(compile|graph|plan)\//u]],
|
|
404
|
+
["FLEET", [/^src\/(route|config|adapters)\//u]],
|
|
405
|
+
["RUN", [/^src\/(run|drivers|eval)\//u]],
|
|
406
|
+
["GATE", [/^src\/gates\//u]],
|
|
407
|
+
["UI", [/^src\/(cli|tui|report)\//u, /^src\/brand\.ts/u]],
|
|
408
|
+
["DOCS", [/^(docs|skills)\//u]],
|
|
409
|
+
["REPO", [/^(scripts|schema)\//u, /^\.github\//u, /^tests\/(lint|repo|helpers)\//u, /^(vitest\.config|package\.json|tsconfig)/u]],
|
|
410
|
+
];
|
|
411
|
+
const expandAreaBraces = (path) => {
|
|
412
|
+
const match = /^(.*?)\{([^}]*)\}(.*)$/u.exec(path);
|
|
413
|
+
return match
|
|
414
|
+
? match[2].split(",").flatMap((part) => expandAreaBraces(`${match[1]}${part.trim()}${match[3]}`))
|
|
415
|
+
: [path];
|
|
416
|
+
};
|
|
417
|
+
const areaForPath = (path) => {
|
|
418
|
+
const candidates = [path, path.replace(/^tests\//u, "src/")];
|
|
419
|
+
for (const candidate of candidates) {
|
|
420
|
+
for (const [name, patterns] of TASK_AREA_RULES) {
|
|
421
|
+
if (patterns.some((pattern) => pattern.test(candidate)))
|
|
422
|
+
return name;
|
|
423
|
+
}
|
|
415
424
|
}
|
|
416
|
-
|
|
425
|
+
const parts = path.replace(/^[!./]+/u, "").split("/").filter((part) => part && !/[*?[\]]/u.test(part));
|
|
426
|
+
const root = parts[0];
|
|
427
|
+
if (!root)
|
|
428
|
+
return "UNMAPPED";
|
|
429
|
+
const domain = ["src", "app", "packages", "tests"].includes(root) && parts[1] ? parts[1] : root;
|
|
430
|
+
return domain.replace(/\.[^.]+$/u, "").toUpperCase();
|
|
431
|
+
};
|
|
432
|
+
const taskArea = (task) => {
|
|
433
|
+
const paths = task.files.flatMap(expandAreaBraces);
|
|
434
|
+
if (paths.length === 0)
|
|
435
|
+
return "—";
|
|
436
|
+
const primary = paths.filter((path) => !path.startsWith("tests/") && !path.startsWith("src/cli/"));
|
|
437
|
+
const source = primary.length ? primary : paths;
|
|
438
|
+
return [...new Set(source.map(areaForPath))].join("+");
|
|
417
439
|
};
|
|
418
440
|
/**
|
|
419
441
|
* Every board line leaves through the cockpit's own wrap, measured in display cells: no drawn line
|
|
@@ -482,7 +504,7 @@ const foldTaskEffort = (tasks, events) => {
|
|
|
482
504
|
const effortPanel = (tasks, events, columns) => {
|
|
483
505
|
if (!events) {
|
|
484
506
|
return [
|
|
485
|
-
` ${title("WHERE THE EFFORT WENT")}`,
|
|
507
|
+
` ${ok("▌")} ${title("WHERE THE EFFORT WENT")}`,
|
|
486
508
|
legend(` dispatch · review · park counts unavailable — ${NOT_COMPARABLE_CLAIM}`),
|
|
487
509
|
];
|
|
488
510
|
}
|
|
@@ -502,7 +524,7 @@ const effortPanel = (tasks, events, columns) => {
|
|
|
502
524
|
return `${prefix}${fitCells(stack, barColumns)} ${counts}`;
|
|
503
525
|
});
|
|
504
526
|
return [
|
|
505
|
-
` ${title("WHERE THE EFFORT WENT")}`,
|
|
527
|
+
` ${ok("▌")} ${title("WHERE THE EFFORT WENT")}`,
|
|
506
528
|
legend(` dispatches · review rounds · human parks · top ${top.length} of ${tasks.length} tasks`),
|
|
507
529
|
"",
|
|
508
530
|
...rows,
|
|
@@ -945,129 +967,148 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
945
967
|
workerPhases: [...phases.values()].filter((phase) => phase.phase === "worker"),
|
|
946
968
|
};
|
|
947
969
|
}
|
|
948
|
-
// TTY:
|
|
949
|
-
//
|
|
950
|
-
// rows (pad plain text FIRST, colorize after: ANSI has zero display width and would corrupt
|
|
951
|
-
// padEnd math)
|
|
952
|
-
// The cockpit owns this board's column contract: `COCKPIT_COLUMN_FLOOR` is the narrowest board it
|
|
953
|
-
// plans, so below it the terminal — not a second floor stated here — decides what happens to the
|
|
954
|
-
// drawn rows. Every column figure below is derived from this one.
|
|
970
|
+
// TTY: the operator-approved task table. The daemon gives this surface a ~110-column pane first;
|
|
971
|
+
// narrower terminals keep the same facts in stacked rows. Every cut/wrap goes through width.ts.
|
|
955
972
|
const boardColumns = Math.max(COCKPIT_COLUMN_FLOOR, width);
|
|
956
973
|
const dot = dim(" · ");
|
|
957
|
-
const
|
|
958
|
-
const gaugeCells = 10;
|
|
959
|
-
const fill = total ? Math.round((done / total) * gaugeCells) : 0;
|
|
974
|
+
const separator = ` ${dim("│")} `;
|
|
960
975
|
const tipFailed = tipPhase?.state === "failed";
|
|
976
|
+
const anyFailed = cells.some((cell) => cell.redTier);
|
|
961
977
|
const progressTone = anyFailed || tipFailed ? fail : tipPhase ? warn : ok;
|
|
962
|
-
const gauge = (fill ? progressTone("█".repeat(fill)) : "") + (fill < gaugeCells ? dim("░".repeat(gaugeCells - fill)) : "");
|
|
963
|
-
const live = liveness(events, now)
|
|
964
|
-
.replace(/\bdead\b/, fail("dead"))
|
|
965
|
-
.replace(/\bfinished\b/, dim("finished"))
|
|
966
|
-
.replace(/\balive\b/, ok("alive"))
|
|
967
|
-
.replaceAll(" · ", dot);
|
|
968
978
|
const tally = tipPhase
|
|
969
979
|
? `${progressTone(`${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
|
|
970
|
-
: `${done}/${total} done`;
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
980
|
+
: done === total && total > 0 ? ok(`${done}/${total} done`) : `${done}/${total} done`;
|
|
981
|
+
const live = liveness(events, now)
|
|
982
|
+
.replace(/\bdead\b/u, fail("dead"))
|
|
983
|
+
.replace(/\bfinished\b/u, dim("finished"))
|
|
984
|
+
.replace(/\balive\b/u, ok("alive"))
|
|
985
|
+
.replaceAll(" · ", dot);
|
|
986
|
+
const header = [
|
|
987
|
+
brandChip(" tickmarkr "),
|
|
988
|
+
title(runId ?? "no runs yet"),
|
|
989
|
+
tally,
|
|
990
|
+
...(events.length ? [`${events.filter((event) => event.event === "run-resume").length} restarts`] : []),
|
|
991
|
+
...(events.length ? [`${events.filter((event) => event.event === "escalation").length} escalations`] : []),
|
|
977
992
|
...(runId && supersededBy ? [warn(`superseded by ${supersededBy}`)] : []),
|
|
978
993
|
...(runId && !comparable ? [warn(NOT_COMPARABLE_NOTICE)] : []),
|
|
979
994
|
...(verify === undefined
|
|
980
995
|
? []
|
|
981
996
|
: [verify === "verify passed" ? ok(verify) : tipFailed ? fail(verify) : warn(verify)]),
|
|
982
|
-
`${gauge} ${tipPhase ? tally : done === total && total > 0 ? ok(tally) : tally}`,
|
|
983
997
|
...(runId ? [live] : []),
|
|
984
998
|
...(journalRowsOnly ? [`zone ${localZoneLabel(zoneReference)}`] : []),
|
|
985
|
-
].join(
|
|
986
|
-
const hr = rule(Math.min(boardColumns, 100));
|
|
987
|
-
// OBS-104 run-level now line: names the most recent journal event. TTY frame only — the non-TTY
|
|
988
|
-
// machine surface is byte-pinned (status-brand golden) and must not drift. Rendered BELOW the task
|
|
989
|
-
// rows: the line names task ids, and rows must stay the first id-bearing lines for grep consumers.
|
|
999
|
+
].join(separator);
|
|
990
1000
|
const nowLine = activity.now ? [legend(` now: ${activity.now}`)] : [];
|
|
991
|
-
const gatesLegend = legend(` gates: ${GATE_NAMES.join(" · ")}`);
|
|
992
|
-
// Every known tier, beaten or not: a tier missing from this line would read as a healthy one.
|
|
993
1001
|
const supervisionLegend = legend(` ${supervisionText(supervision)}`);
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const avail = Math.max(8, boardColumns - (5 + idW) - 2 - stW);
|
|
1008
|
-
const titles = cells.map((c) => shortGoal(c.t.title, avail));
|
|
1009
|
-
const titleW = Math.max(8, ...titles.map(cellWidth));
|
|
1010
|
-
// Line 2 starts under the title column — but never past what the board can pay for: an id near
|
|
1011
|
-
// the schema's 64-character maximum would otherwise indent the machinery clean off a narrow row.
|
|
1012
|
-
const chainCells = cellWidth(gateChain(GATE_NAMES.map(() => "open"), true));
|
|
1013
|
-
const indent = " ".repeat(Math.max(0, Math.min(idW + 5, boardColumns - chainCells - 2)));
|
|
1014
|
-
const rows = cells.map((c, i) => {
|
|
1015
|
-
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, channel, ctx, livePhase, pane } = c;
|
|
1016
|
-
const word = statusWord(c);
|
|
1017
|
-
// The alarm reads the SAME evidence the detail phrase names — a worktree delta the daemon proved
|
|
1018
|
-
// counts, or the card would flag a worker it just called alive one line below (OBS-538 review).
|
|
1019
|
-
const staleWorker = livePhase?.phase === "worker"
|
|
1020
|
-
&& workerSilenceMs(livePhase, now, workerLiveness.get(t.id)) >= 60_000;
|
|
1021
|
-
const stWord = staleWorker ? warn(word) : merged ? ok(word) : redTier ? fail(word) : st === "failed" || st === "human" ? warn(word) : word;
|
|
1022
|
-
// a fail names its gate in words right here — the one moment gate identity is needed on a row
|
|
1023
|
-
const f = failedGates(states);
|
|
1024
|
-
const human = humanGateSuffix(t, graphTaskStatus(st, t.status), states);
|
|
1025
|
-
const statusCell = stWord +
|
|
1026
|
-
(isStarved ? dot + fail("starved") : "") +
|
|
1027
|
-
(f.length ? dot + fail(f.join(", ")) : "") +
|
|
1028
|
-
(human ? dot + warn("awaiting approval") : "");
|
|
1029
|
-
const taskTitle = titles[i];
|
|
1030
|
-
const taskLabel = `${t.id.padEnd(idW)} ${taskTitle}${" ".repeat(titleW - cellWidth(taskTitle))} ${statusCell}`;
|
|
1031
|
-
const line1 = livePhase
|
|
1032
|
-
? ` ${(staleWorker ? warn : dim)(SPINNER[animationFrame % SPINNER.length])} ${taskLabel}`
|
|
1033
|
-
: ` ${statusRow(taskVerdict(c), taskLabel)}`;
|
|
1034
|
-
// activity already names its channel for in-flight attempts — never repeat it
|
|
1035
|
-
const chain = gateChain(states, true);
|
|
1036
|
-
// A finished task blocks nobody — the same journal-folded done-ness the map was built on.
|
|
1002
|
+
const taskSectionSummary = `${done} of ${total} merged · rows in graph order · gates left→right in pipeline order`;
|
|
1003
|
+
const taskSection = ` ${ok("▌")} ${title("TASKS")} ${dim(taskSectionSummary)}`;
|
|
1004
|
+
const effortByTask = new Map(foldTaskEffort(g.tasks, record && !comparable ? [] : events)
|
|
1005
|
+
.map((task) => [task.taskId, task]));
|
|
1006
|
+
const gateMatrix = (states) => states.map((state) => {
|
|
1007
|
+
const glyph = gateBox(state, true);
|
|
1008
|
+
const tone = state === "pass" ? ok : state === "fail" ? fail : dim;
|
|
1009
|
+
return `${tone(glyph)} `;
|
|
1010
|
+
}).join("");
|
|
1011
|
+
const gateHeader = GATE_NAMES.map((gate) => fitCells(gate.slice(0, 2), 3)).join("");
|
|
1012
|
+
const noteFor = (cell) => {
|
|
1013
|
+
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, ctx, livePhase, pane } = cell;
|
|
1014
|
+
const effort = effortByTask.get(t.id);
|
|
1037
1015
|
const blocking = graphTaskStatus(st, t.status) === "done" ? [] : dependents.get(t.id) ?? [];
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1016
|
+
const failed = failedGates(states);
|
|
1017
|
+
const human = humanGateSuffix(t, graphTaskStatus(st, t.status), states);
|
|
1018
|
+
return [
|
|
1019
|
+
livePhase ? `${SPINNER[animationFrame % SPINNER.length]} ${phrase ?? "running"}` : "",
|
|
1020
|
+
!livePhase && isStarved ? "starved" : "",
|
|
1021
|
+
!livePhase && !isStarved && !merged ? redTier ? "failed" : phrase ?? (st === "failed" ? "warn" : surfaceStatusWord(st)) : "",
|
|
1022
|
+
redTier && phrase ? phrase : "",
|
|
1023
|
+
priorGraph ? PRIOR_GRAPH_MARKER : "",
|
|
1024
|
+
failed.length ? `failed · ${failed.join(", ")}` : "",
|
|
1025
|
+
human ? "awaiting approval" : "",
|
|
1026
|
+
!merged && failureKind && !phrase?.includes(failureKind) ? failureKind : "",
|
|
1027
|
+
(effort?.parks ?? 0) >= 3 ? `parked ×${effort.parks}` : "",
|
|
1028
|
+
(effort?.reviews ?? 0) > 2 ? `${effort.reviews} review rounds` : "",
|
|
1029
|
+
blocking.length ? `blocks ${blocking.join(", ")}` : "",
|
|
1030
|
+
pane ? `pane ${pane}` : "",
|
|
1031
|
+
ctx !== undefined ? `ctx ${ctx}` : "",
|
|
1032
|
+
].filter(Boolean).join(" · ");
|
|
1033
|
+
};
|
|
1034
|
+
const staleWorker = (cell) => cell.livePhase?.phase === "worker"
|
|
1035
|
+
&& workerSilenceMs(cell.livePhase, now, workerLiveness.get(cell.t.id)) >= 60_000;
|
|
1036
|
+
const tableRows = () => {
|
|
1037
|
+
const idWidth = Math.min(8, Math.max(5, ...cells.map((cell) => cellWidth(cell.t.id))));
|
|
1038
|
+
const areaWidth = 16;
|
|
1039
|
+
const depsWidth = 14;
|
|
1040
|
+
const gatesWidth = GATE_NAMES.length * 3;
|
|
1041
|
+
const channelWidth = boardColumns >= 140 ? 22 : 12;
|
|
1042
|
+
const attemptWidth = 5;
|
|
1043
|
+
const noteWidth = boardColumns >= 140 ? 24 : boardColumns >= 120 ? 20 : 14;
|
|
1044
|
+
const fixed = 4 + idWidth + areaWidth + depsWidth + gatesWidth + 2 + channelWidth + attemptWidth + noteWidth;
|
|
1045
|
+
const taskWidth = boardColumns - fixed;
|
|
1046
|
+
const fitColumn = (text, columns, gap = 2) => fitCells(text, Math.max(1, columns - gap)) + " ".repeat(Math.min(gap, columns - 1));
|
|
1047
|
+
if (taskWidth < 12) {
|
|
1048
|
+
return cells.flatMap((cell) => {
|
|
1049
|
+
const effort = effortByTask.get(cell.t.id);
|
|
1050
|
+
const deps = cell.t.deps.length ? cell.t.deps.join(", ") : "—";
|
|
1051
|
+
const tone = cell.redTier ? fail : cell.merged ? ok : staleWorker(cell) || cell.st === "failed" || cell.st === "human" ? warn : dim;
|
|
1052
|
+
const machinery = [
|
|
1053
|
+
`deps ${deps}`,
|
|
1054
|
+
gateMatrix(cell.states),
|
|
1055
|
+
cell.channel,
|
|
1056
|
+
`att ${effort?.dispatches ?? 0}`,
|
|
1057
|
+
].join(" ");
|
|
1058
|
+
return [
|
|
1059
|
+
` ${tone(cell.t.id)} ${dim(taskArea(cell.t))} ${sanitizeTaskText(cell.t.title)}`,
|
|
1060
|
+
...wrapCells(` ${dim(machinery)}`, boardColumns, { continuationPrefix: " " }),
|
|
1061
|
+
...wrapCells(` ${dim(noteFor(cell))}`, boardColumns, { continuationPrefix: " " }).filter((line) => line.trim()),
|
|
1062
|
+
"",
|
|
1063
|
+
];
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
const headerRow = ` ${fitColumn("", idWidth, 1)}${fitColumn("area", areaWidth)}${fitColumn("deps", depsWidth)}${fitColumn("task", taskWidth)}${gateHeader} ${fitColumn("channel", channelWidth)}${fitColumn("att", attemptWidth, 1)}note`;
|
|
1067
|
+
const ruleRow = ` ${"─".repeat(boardColumns - 4)}`;
|
|
1068
|
+
const rows = cells.flatMap((cell) => {
|
|
1069
|
+
const effort = effortByTask.get(cell.t.id);
|
|
1070
|
+
const columns = [
|
|
1071
|
+
wrapCells(cell.t.id, idWidth - 1),
|
|
1072
|
+
wrapCells(taskArea(cell.t), areaWidth - 2),
|
|
1073
|
+
wrapCells(cell.t.deps.length ? cell.t.deps.join(", ") : "—", depsWidth - 2),
|
|
1074
|
+
[sanitizeTaskText(cell.t.title)],
|
|
1075
|
+
[gateMatrix(cell.states)],
|
|
1076
|
+
[cell.channel],
|
|
1077
|
+
[String(effort?.dispatches ?? 0)],
|
|
1078
|
+
wrapCells(noteFor(cell), noteWidth),
|
|
1079
|
+
];
|
|
1080
|
+
const height = Math.max(...columns.map((column) => column.length));
|
|
1081
|
+
const idTone = cell.redTier ? fail : cell.merged ? ok : staleWorker(cell) || cell.st === "failed" || cell.st === "human" ? warn : dim;
|
|
1082
|
+
const titleTone = cell.redTier || cell.livePhase || (effort?.parks ?? 0) >= 3 ? title : dim;
|
|
1083
|
+
return Array.from({ length: height }, (_, index) => ` ${idTone(fitColumn(columns[0][index] ?? "", idWidth, 1))}`
|
|
1084
|
+
+ dim(fitColumn(columns[1][index] ?? "", areaWidth))
|
|
1085
|
+
+ dim(fitColumn(columns[2][index] ?? "", depsWidth))
|
|
1086
|
+
+ titleTone(fitColumn(columns[3][index] ?? "", taskWidth))
|
|
1087
|
+
+ fitCells(columns[4][index] ?? "", gatesWidth)
|
|
1088
|
+
+ " "
|
|
1089
|
+
+ dim(fitColumn(columns[5][index] ?? "", channelWidth))
|
|
1090
|
+
+ dim(fitColumn(columns[6][index] ?? "", attemptWidth, 1))
|
|
1091
|
+
+ dim(fitCells(columns[7][index] ?? "", noteWidth)));
|
|
1092
|
+
});
|
|
1093
|
+
return [legend(headerRow), dim(ruleRow), ...rows];
|
|
1094
|
+
};
|
|
1095
|
+
const gatesLegend = legend(` gates ${GATE_NAMES.map((gate) => `${gate.slice(0, 2)} ${gate}`).join(" ")}`);
|
|
1068
1096
|
const effort = effortPanel(g.tasks, record && !comparable ? undefined : events, boardColumns);
|
|
1069
1097
|
return {
|
|
1070
|
-
content: boardRows([
|
|
1098
|
+
content: boardRows([
|
|
1099
|
+
header,
|
|
1100
|
+
...nowLine,
|
|
1101
|
+
supervisionLegend,
|
|
1102
|
+
"",
|
|
1103
|
+
taskSection,
|
|
1104
|
+
"",
|
|
1105
|
+
...tableRows(),
|
|
1106
|
+
gatesLegend,
|
|
1107
|
+
"",
|
|
1108
|
+
rule(boardColumns),
|
|
1109
|
+
"",
|
|
1110
|
+
...effort,
|
|
1111
|
+
], boardColumns).join("\n"),
|
|
1071
1112
|
...(hotPhase ? { hotPhase } : {}),
|
|
1072
1113
|
...(runId ? { runId } : {}),
|
|
1073
1114
|
workerPhases: [...phases.values()].filter((phase) => phase.phase === "worker"),
|
|
@@ -1101,10 +1142,10 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1101
1142
|
const namedRunId = positionalRunId(argv);
|
|
1102
1143
|
if (argv.includes("--oneline"))
|
|
1103
1144
|
return oneLine(cwd, namedRunId);
|
|
1104
|
-
//
|
|
1145
|
+
// The task-table frame owns its compact one-line brand chip; the four-row global banner would
|
|
1146
|
+
// create the second header the approved replacement removes.
|
|
1105
1147
|
if (!argv.includes("--watch")) {
|
|
1106
|
-
|
|
1107
|
-
return visual() ? BANNER + content : content;
|
|
1148
|
+
return renderFrame(cwd, opts.now?.() ?? Date.now(), 0, undefined, false, namedRunId).content;
|
|
1108
1149
|
}
|
|
1109
1150
|
const eventStream = argv.some((arg) => arg === "--events" || arg === "--jsonl" || arg === "--decision-events");
|
|
1110
1151
|
const webhookUrl = opts.webhookUrl
|
|
@@ -1227,7 +1268,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1227
1268
|
frame = renderFrame(cwd, nowMs, i, workerLiveness, true, namedRunId);
|
|
1228
1269
|
if (tty) {
|
|
1229
1270
|
updateTitle(frame.hotPhase, nowMs);
|
|
1230
|
-
process.stdout.write(`\x1b[2J\x1b[H${
|
|
1271
|
+
process.stdout.write(`\x1b[2J\x1b[H${frame.content}\n${legend(` watching · refresh ${REFRESH_MS / 1000}s · ^C to quit`)}`);
|
|
1231
1272
|
}
|
|
1232
1273
|
else {
|
|
1233
1274
|
process.stdout.write(frame.content + sep);
|
package/package.json
CHANGED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
#
|
|
26
26
|
# First output line is the machine-greppable verdict:
|
|
27
27
|
# DELIVERED_SUBMITTED | QUEUED_BEHIND_TURN | SEND_UNSUBMITTED | INTERRUPT_FAILED | TARGET_GONE
|
|
28
|
-
# | REFUSED_SIZE | REFUSED_BOX_OCCUPIED
|
|
28
|
+
# | REFUSED_SIZE | REFUSED_BOX_OCCUPIED | SEND_UNVERIFIED | SEND_REFUSED | SEND_FAILED
|
|
29
|
+
#
|
|
30
|
+
# A success verdict names the surface that delivered when it was not the default one, so a fallback
|
|
31
|
+
# is visible in the transcript rather than inferred (OBS-552).
|
|
29
32
|
#
|
|
30
33
|
# This verifies SUBMISSION — the text left the input box — never that the seat understood or acted.
|
|
31
34
|
# Read the seat's ARTIFACT for that; a send receipt is not a response.
|
|
@@ -46,6 +49,14 @@ status_of() {
|
|
|
46
49
|
| sed -n 's/.*"agent_status":"\([a-z_]*\)".*/\1/p' | head -1
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
# The submit-fallback needs a PANE id, and $TARGET may be an agent NAME. `agent get` answers for a
|
|
53
|
+
# detected-but-undrivable seat — it is only `agent prompt` that refuses one (OBS-552) — so this
|
|
54
|
+
# resolves either target form. The first `pane_id` in the response is the agent's own.
|
|
55
|
+
pane_of() {
|
|
56
|
+
herdr agent get "$TARGET" 2>/dev/null \
|
|
57
|
+
| sed -n 's/.*"pane_id":"\([^"]*\)".*/\1/p' | head -1
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
# The rendered `❯` line — the only state that discriminates submitted from sitting. head -1 returns
|
|
50
61
|
# the first rendered line, i.e. a PREFIX of a wrapped draft, so callers compare prefixes, never
|
|
51
62
|
# whole sentences (OBS-396: a full-sentence match returns false on a message that arrived intact).
|
|
@@ -54,6 +65,24 @@ prompt_line() {
|
|
|
54
65
|
| sed -n 's/^[[:space:]]*❯[[:space:]]*//p' | head -1 | sed 's/[[:space:]]*$//'
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
# STAGED-QUEUE DISCRIMINATOR (OBS-552 addendum, captured 2026-08-19 on a live kimi seat). After its
|
|
69
|
+
# first turn ends, kimi stages every later message into a SECOND `❯` region —
|
|
70
|
+
# `❯ <text> ↑ to edit · ctrl-s to steer immediately` — and EMPTIES the ordinary box when it does. So
|
|
71
|
+
# `prompt_line`'s emptiness is not submission on a kimi seat: it is the queue swallowing the
|
|
72
|
+
# directive. Six consecutive paths were tried against that state and the pane revision never moved
|
|
73
|
+
# off 3; `seat-send.sh` called the first one DELIVERED_SUBMITTED. This reads the staging AFFORDANCE
|
|
74
|
+
# rather than the vendor name, so any TUI that grows the same queue is covered without a matcher list.
|
|
75
|
+
# Returns the staged TEXT only: the `❯` marker and the trailing affordance are chrome, and leaving the
|
|
76
|
+
# affordance on would break `is_ours`, whose contract is that the rendered text is a PREFIX of $MSG
|
|
77
|
+
# (OBS-396). A wrapped staged entry still yields a prefix, which is all the comparison needs.
|
|
78
|
+
staged_line() {
|
|
79
|
+
herdr agent read "$TARGET" --source visible --lines 14 2>/dev/null \
|
|
80
|
+
| sed -n '/↑ to edit/{/steer/p;}' | head -1 \
|
|
81
|
+
| sed 's/^[[:space:]]*❯[[:space:]]*//' \
|
|
82
|
+
| sed 's/[[:space:]]*↑ to edit.*$//' \
|
|
83
|
+
| sed 's/[[:space:]]*$//'
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
# Is $1 a rendered prefix of our message? (first line of a wrapped draft = prefix of MSG)
|
|
58
87
|
is_ours() {
|
|
59
88
|
case "$MSG" in "$1"*) return 0 ;; *) return 1 ;; esac
|
|
@@ -107,9 +136,73 @@ fi
|
|
|
107
136
|
|
|
108
137
|
# Atomic text+Enter honouring the pane's live bracketed-paste mode — never send-text + separate
|
|
109
138
|
# Enter, which is the "sitting unsubmitted" failure by construction.
|
|
110
|
-
|
|
139
|
+
#
|
|
140
|
+
# OBS-552: the outcome of this verb was thrown away — `>/dev/null 2>&1` discarded BOTH the exit code
|
|
141
|
+
# and the reason. A refusal that wrote NOTHING then fell through to the prompt-line checks below with
|
|
142
|
+
# an empty line, which is byte-identical to a clean submit, and this script exited 0 announcing
|
|
143
|
+
# DELIVERED_SUBMITTED — or QUEUED_BEHIND_TURN when the seat read `working`, which also EXPLAINS AWAY
|
|
144
|
+
# the silence that follows. Measured 2026-08-19 against a live seat: `herdr agent prompt` refuses a
|
|
145
|
+
# detected-but-undrivable occupant with `agent_not_ready` and never touches the input box, while
|
|
146
|
+
# `pane run` delivers to that same pane. A send receipt that cannot fail is not a receipt.
|
|
147
|
+
PROMPT_ERR=$(herdr agent prompt "$TARGET" "$MSG" 2>&1 >/dev/null)
|
|
148
|
+
PROMPT_RC=$?
|
|
149
|
+
VIA=""
|
|
150
|
+
if [ "$PROMPT_RC" -ne 0 ]; then
|
|
151
|
+
case "$PROMPT_ERR" in
|
|
152
|
+
*agent_not_ready*)
|
|
153
|
+
# UNEQUIVOCALLY PRE-WRITE: herdr rejected the target before typing anything, so a second
|
|
154
|
+
# delivery cannot duplicate. This is the ONLY class that earns a fallback.
|
|
155
|
+
PANE=$(pane_of)
|
|
156
|
+
if [ -z "$PANE" ]; then
|
|
157
|
+
echo "SEND_REFUSED $TARGET — agent prompt refused pre-write and no pane id resolved: $PROMPT_ERR"
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
PANE_ERR=$(herdr pane run "$PANE" "$MSG" 2>&1 >/dev/null)
|
|
161
|
+
PANE_RC=$?
|
|
162
|
+
if [ "$PANE_RC" -ne 0 ]; then
|
|
163
|
+
echo "SEND_FAILED $TARGET — both delivery surfaces refused; nothing was typed."
|
|
164
|
+
echo " agent prompt: $PROMPT_ERR"
|
|
165
|
+
echo " pane run ($PANE): $PANE_ERR"
|
|
166
|
+
exit 1
|
|
167
|
+
fi
|
|
168
|
+
VIA=" (via pane run $PANE — agent prompt refused pre-write: $PROMPT_ERR)"
|
|
169
|
+
;;
|
|
170
|
+
*)
|
|
171
|
+
# AMBIGUOUS: this class cannot prove the write did not land, and a second delivery APPENDS to
|
|
172
|
+
# whatever did. So it never retries — it surfaces the cause and stops, which is the one outcome
|
|
173
|
+
# the discarded stderr made impossible.
|
|
174
|
+
echo "SEND_UNVERIFIED $TARGET — agent prompt exited $PROMPT_RC with an ambiguous outcome: $PROMPT_ERR"
|
|
175
|
+
echo " NOT retrying (a second send appends to anything that landed). Read the pane, then decide."
|
|
176
|
+
exit 1
|
|
177
|
+
;;
|
|
178
|
+
esac
|
|
179
|
+
fi
|
|
111
180
|
|
|
112
181
|
sleep 2
|
|
182
|
+
|
|
183
|
+
# BEFORE any emptiness verdict: a non-empty staged queue means this seat has accepted text and is not
|
|
184
|
+
# running it, so an empty ordinary box proves nothing. Fail closed — there is no delivery remedy to
|
|
185
|
+
# offer, and every one was tried against the captured instance: bare `enter` on the empty box no-ops,
|
|
186
|
+
# `up`+`enter` moves the entry to the box and puts it straight BACK in the queue, `herdr agent
|
|
187
|
+
# send-keys` cannot type kimi's own `ctrl-s` affordance (no ctrl chords), and `pane run` is a
|
|
188
|
+
# shell-pane path with no effect on a TUI. Recovery is the operator's: capture provenance
|
|
189
|
+
# (pid/ppid/pgid/lstart), TERM the CLI, then `herdr agent start … -- --auto -c` in the SAME pane —
|
|
190
|
+
# session continuation preserved 136k of context and the queue drained on restart. NEVER retry here:
|
|
191
|
+
# the entry may drain at any moment and a second send would then run twice.
|
|
192
|
+
STAGED=$(staged_line)
|
|
193
|
+
if [ -n "$STAGED" ]; then
|
|
194
|
+
if is_ours "$STAGED"; then
|
|
195
|
+
echo "SEND_UNVERIFIED $TARGET — the directive is STAGED in the seat's queue, not running: $STAGED"
|
|
196
|
+
else
|
|
197
|
+
echo "SEND_UNVERIFIED $TARGET — this seat's queue holds an entry that is not draining, so nothing sent now is running: $STAGED"
|
|
198
|
+
fi
|
|
199
|
+
echo " An empty prompt line does NOT mean submitted on a staging TUI — the box is empty BECAUSE the"
|
|
200
|
+
echo " text went to the queue. Do NOT re-send: the queue may drain at any moment and run it twice."
|
|
201
|
+
echo " No input path drains it (enter no-ops, up+enter re-queues, send-keys has no ctrl chords,"
|
|
202
|
+
echo " pane run is shell-only). Capture provenance, TERM the CLI, restart it in the SAME pane with"
|
|
203
|
+
echo " session continuation, then verify the pane REVISION moves before trusting any receipt."
|
|
204
|
+
exit 1
|
|
205
|
+
fi
|
|
113
206
|
PL=$(prompt_line)
|
|
114
207
|
if [ -n "$PL" ] && is_ours "$PL"; then
|
|
115
208
|
# The text is SITTING, not sent — the swallowed-Enter shape. Submitting the existing draft is not a
|
|
@@ -127,14 +220,14 @@ fi
|
|
|
127
220
|
|
|
128
221
|
if [ -n "$PL" ]; then
|
|
129
222
|
# Not ours: either autosuggest ghost (dim SGR — benign) or a draft that appeared under us.
|
|
130
|
-
echo "DELIVERED_SUBMITTED $TARGET — prompt line clear of our text; NOTE it now holds: $PL"
|
|
223
|
+
echo "DELIVERED_SUBMITTED $TARGET$VIA — prompt line clear of our text; NOTE it now holds: $PL"
|
|
131
224
|
echo " ANSI-verify (dim = ghost). If typed, treat per D-206: discriminate before anyone clears it."
|
|
132
225
|
exit 0
|
|
133
226
|
fi
|
|
134
227
|
|
|
135
228
|
if [ -n "$QUEUED" ]; then
|
|
136
|
-
echo "QUEUED_BEHIND_TURN $TARGET — submitted into a working seat's queue; it is READ at turn end. A freeze-class or superseding directive belongs behind TKR_INTERRUPT=1."
|
|
229
|
+
echo "QUEUED_BEHIND_TURN $TARGET$VIA — submitted into a working seat's queue; it is READ at turn end. A freeze-class or superseding directive belongs behind TKR_INTERRUPT=1."
|
|
137
230
|
else
|
|
138
|
-
echo "DELIVERED_SUBMITTED $TARGET"
|
|
231
|
+
echo "DELIVERED_SUBMITTED $TARGET$VIA"
|
|
139
232
|
fi
|
|
140
233
|
exit 0
|