tickmarkr 1.97.0 → 2.0.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 +28 -0
- package/dist/brand.js +41 -0
- package/dist/cli/commands/compile.js +32 -1
- package/dist/cli/commands/resume.js +9 -3
- package/dist/cli/commands/run.d.ts +61 -1
- package/dist/cli/commands/run.js +368 -17
- package/dist/cli/commands/status.js +145 -28
- package/dist/compile/collateral.d.ts +25 -0
- package/dist/compile/collateral.js +46 -11
- package/dist/compile/native.js +10 -0
- package/dist/drivers/herdr.d.ts +19 -13
- package/dist/drivers/herdr.js +88 -26
- package/dist/drivers/types.d.ts +2 -0
- package/dist/gates/acceptance.js +17 -7
- package/dist/gates/llm.d.ts +19 -0
- package/dist/gates/llm.js +104 -6
- package/dist/gates/run-gates.d.ts +18 -0
- package/dist/gates/run-gates.js +195 -29
- package/dist/gates/scope.d.ts +9 -1
- package/dist/gates/scope.js +22 -2
- package/dist/graph/graph.d.ts +1 -0
- package/dist/graph/graph.js +19 -2
- package/dist/report/compare.js +17 -2
- package/dist/run/daemon.d.ts +1 -8
- package/dist/run/daemon.js +231 -246
- package/dist/run/environment.d.ts +18 -1
- package/dist/run/environment.js +19 -2
- package/dist/run/journal.d.ts +50 -3
- package/dist/run/journal.js +181 -5
- package/dist/run/protocol.d.ts +4 -4
- package/dist/run/stall.d.ts +30 -0
- package/dist/run/stall.js +173 -0
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +20 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { GLYPHS, LIVE } 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";
|
|
@@ -15,8 +15,42 @@ import { readSupervision, supervisionText } from "../../run/supervision.js";
|
|
|
15
15
|
import { deriveRunCockpitData, } from "../../tui/cockpit/derive.js";
|
|
16
16
|
import { COCKPIT_COLUMN_FLOOR } from "../../tui/cockpit/layout.js";
|
|
17
17
|
import { cellWidth, fitCells, wrapCells } from "../../tui/cockpit/width.js";
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
import { version } from "./version.js";
|
|
19
|
+
// The board is a LIVE surface, so it draws through brand.ts's operator role map and never the vivid
|
|
20
|
+
// one-shot tokens. Semantic aliases can share a colour, but every board cell, gate mark, effort bar,
|
|
21
|
+
// header and footer styles through one of the five exported authorities.
|
|
22
|
+
const ok = LIVE.pass;
|
|
23
|
+
const running = LIVE.running;
|
|
24
|
+
const information = LIVE.information;
|
|
25
|
+
const fail = LIVE.failure;
|
|
26
|
+
const warn = LIVE.attention;
|
|
27
|
+
const dim = LIVE.secondaryText;
|
|
28
|
+
const title = LIVE.primaryText;
|
|
29
|
+
const legend = LIVE.secondaryText;
|
|
30
|
+
const rule = (columns) => LIVE.chrome("─".repeat(columns));
|
|
31
|
+
// Two clocks, deliberately unequal. The journal frame is cheap — a single file read and a fold — so
|
|
32
|
+
// it redraws on a 500ms cadence and the board feels live. Scraping a worker's pane is neither cheap
|
|
33
|
+
// nor bounded: it crosses the herdr socket and can hang on a wedged pane, so it keeps its own 2s
|
|
34
|
+
// budget and runs OFF the redraw path (see scheduleWorkerScrape). A slow scrape updates liveness
|
|
35
|
+
// one frame late; it never delays a frame, and it never overlaps itself.
|
|
36
|
+
// ponytail: fixed cadences; promote to config.visibility.* only when an operator asks.
|
|
37
|
+
const FRAME_MS = 500;
|
|
38
|
+
const SCRAPE_MS = 2000;
|
|
39
|
+
// The binary's own version, read ONCE when status starts (see `status`), then carried into every
|
|
40
|
+
// frame: a board redrawing twice a second must not pay a manifest read per frame, and the version
|
|
41
|
+
// it names must not change under the operator mid-run. It reads through the SAME manifest reader
|
|
42
|
+
// `tickmarkr version` prints from — one version answer for the whole CLI — and a manifest that
|
|
43
|
+
// cannot be read, cannot be parsed, or names no version PROPAGATES rather than resolving to a
|
|
44
|
+
// placeholder: a board that cannot name its own binary is a broken installation, and the board's
|
|
45
|
+
// whole job is to state what is true. A rendered `vunknown` would be a claim about the binary
|
|
46
|
+
// nobody could check.
|
|
47
|
+
const readBinaryVersion = async () => {
|
|
48
|
+
const semver = await version();
|
|
49
|
+
if (typeof semver !== "string" || semver === "") {
|
|
50
|
+
throw new Error("package.json names no version — reinstall tickmarkr");
|
|
51
|
+
}
|
|
52
|
+
return semver;
|
|
53
|
+
};
|
|
20
54
|
// The claim leads, the guidance follows: an incomparable journal is named in three words a wrapped
|
|
21
55
|
// board keeps on one row, and the remedy trails it. Both surfaces state the claim; nothing derived
|
|
22
56
|
// from such a journal — task states, gate outcomes, tip verification — may be presented as this
|
|
@@ -368,7 +402,7 @@ const gateSnapshot = (task, events, rehashAt) => {
|
|
|
368
402
|
};
|
|
369
403
|
};
|
|
370
404
|
export const gateStates = (task, events) => gateSnapshot(task, events).states;
|
|
371
|
-
//
|
|
405
|
+
// Verdict semantics only: pass uses brand teal, failure uses amethyst, and skip/open use chrome.
|
|
372
406
|
const GATE_STATE_TOKEN = { pass: ok, fail, skip: dim, open: dim };
|
|
373
407
|
// TTY cells are bare glyphs in fixed GATE_NAMES order — gate identity lives once in the frame
|
|
374
408
|
// legend, and in words on a failing row; non-TTY keeps the letter+box chips (byte-pinned surface)
|
|
@@ -493,6 +527,10 @@ const foldTaskEffort = (tasks, events) => {
|
|
|
493
527
|
return [...effort.values()].sort((left, right) => right.total - left.total
|
|
494
528
|
|| order.get(left.taskId) - order.get(right.taskId));
|
|
495
529
|
};
|
|
530
|
+
// Attention (review) and failure (park) wear the ONE amethyst, so the bar segments and their legend
|
|
531
|
+
// markers carry the distinction in SHAPE: a full block for dispatch, light shade for a review round,
|
|
532
|
+
// dark shade for a human park. Same display width, so the fitted bar keeps its measured columns.
|
|
533
|
+
const EFFORT_GLYPH = { dispatch: "\u2588", review: "\u2592", park: "\u2593" };
|
|
496
534
|
/**
|
|
497
535
|
* Prototype panel, fitted by the cockpit's display-cell authority before board-wide wrapping.
|
|
498
536
|
*
|
|
@@ -518,9 +556,9 @@ const effortPanel = (tasks, events, columns) => {
|
|
|
518
556
|
const dispatchEnd = Math.round((task.dispatches / maxTotal) * barColumns);
|
|
519
557
|
const reviewEnd = Math.round(((task.dispatches + task.reviews) / maxTotal) * barColumns);
|
|
520
558
|
const parkEnd = Math.round((task.total / maxTotal) * barColumns);
|
|
521
|
-
const stack = ok(
|
|
522
|
-
+ warn(
|
|
523
|
-
+ fail(
|
|
559
|
+
const stack = ok(EFFORT_GLYPH.dispatch.repeat(dispatchEnd))
|
|
560
|
+
+ warn(EFFORT_GLYPH.review.repeat(Math.max(0, reviewEnd - dispatchEnd)))
|
|
561
|
+
+ fail(EFFORT_GLYPH.park.repeat(Math.max(0, parkEnd - reviewEnd)));
|
|
524
562
|
return `${prefix}${fitCells(stack, barColumns)} ${counts}`;
|
|
525
563
|
});
|
|
526
564
|
return [
|
|
@@ -529,7 +567,7 @@ const effortPanel = (tasks, events, columns) => {
|
|
|
529
567
|
"",
|
|
530
568
|
...rows,
|
|
531
569
|
"",
|
|
532
|
-
` ${ok(
|
|
570
|
+
` ${ok(EFFORT_GLYPH.dispatch)} ${dim("dispatch")} ${warn(EFFORT_GLYPH.review)} ${dim("review round")} ${fail(EFFORT_GLYPH.park)} ${dim("human park")}`,
|
|
533
571
|
];
|
|
534
572
|
};
|
|
535
573
|
// VIS-11 (v1.13): a liveness header for renderFrame — last journal event age + whether the recorded
|
|
@@ -829,7 +867,10 @@ const recordedTasks = (graph, rows) => graph.tasks.filter((task) => {
|
|
|
829
867
|
|| row.parkKind !== undefined);
|
|
830
868
|
});
|
|
831
869
|
const recordedDone = (tasks, rows) => tasks.filter((task) => graphTaskStatus(rows.get(task.id)?.state, task.status) === "done").length;
|
|
832
|
-
const renderFrame = (cwd,
|
|
870
|
+
const renderFrame = (cwd,
|
|
871
|
+
// No default: the version is the binary's, resolved once by its caller, and a frame that had to
|
|
872
|
+
// invent one would be naming a version nobody can check.
|
|
873
|
+
binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(), journalRowsOnly = false, namedRunId) => {
|
|
833
874
|
const g = loadGraph(cwd);
|
|
834
875
|
const record = readRunRecord(cwd, g, namedRunId);
|
|
835
876
|
const runId = record?.runId;
|
|
@@ -975,16 +1016,18 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
975
1016
|
const tipFailed = tipPhase?.state === "failed";
|
|
976
1017
|
const anyFailed = cells.some((cell) => cell.redTier);
|
|
977
1018
|
const progressTone = anyFailed || tipFailed ? fail : tipPhase ? warn : ok;
|
|
1019
|
+
// Attention and failure wear the ONE amethyst here too, so the header leads with its own glyph:
|
|
1020
|
+
// ✗ once a verdict is in, ! while the tip is still unverified. Colour cannot carry this.
|
|
1021
|
+
const progressGlyph = anyFailed || tipFailed ? GLYPHS.fail : GLYPHS.attention;
|
|
978
1022
|
const tally = tipPhase
|
|
979
|
-
? `${progressTone(`${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
|
|
980
|
-
: done === total && total > 0 ? ok(`${done}/${total} done`) : `${done}/${total} done
|
|
1023
|
+
? `${progressTone(`${progressGlyph} ${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
|
|
1024
|
+
: done === total && total > 0 ? ok(`${done}/${total} done`) : information(`${done}/${total} done`);
|
|
981
1025
|
const live = liveness(events, now)
|
|
982
1026
|
.replace(/\bdead\b/u, fail("dead"))
|
|
983
1027
|
.replace(/\bfinished\b/u, dim("finished"))
|
|
984
|
-
.replace(/\balive\b/u,
|
|
1028
|
+
.replace(/\balive\b/u, running("alive"))
|
|
985
1029
|
.replaceAll(" · ", dot);
|
|
986
|
-
const
|
|
987
|
-
brandChip(" tickmarkr "),
|
|
1030
|
+
const facts = [
|
|
988
1031
|
title(runId ?? "no runs yet"),
|
|
989
1032
|
tally,
|
|
990
1033
|
...(events.length ? [`${events.filter((event) => event.event === "run-resume").length} restarts`] : []),
|
|
@@ -993,10 +1036,34 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
993
1036
|
...(runId && !comparable ? [warn(NOT_COMPARABLE_NOTICE)] : []),
|
|
994
1037
|
...(verify === undefined
|
|
995
1038
|
? []
|
|
996
|
-
: [verify === "verify passed"
|
|
1039
|
+
: [verify === "verify passed"
|
|
1040
|
+
? ok(verify)
|
|
1041
|
+
: tipFailed ? fail(`${GLYPHS.fail} ${verify}`) : warn(`${GLYPHS.attention} ${verify}`)]),
|
|
997
1042
|
...(runId ? [live] : []),
|
|
998
1043
|
...(journalRowsOnly ? [`zone ${localZoneLabel(zoneReference)}`] : []),
|
|
999
1044
|
].join(separator);
|
|
1045
|
+
// The brand lockup is two rows and stays two rows at every width: the muted chip, and the running
|
|
1046
|
+
// binary's version directly beneath it. The run's own facts continue to the right of both rows and
|
|
1047
|
+
// wrap under themselves — the version can never be pushed off its row by a fact that grew, which
|
|
1048
|
+
// is the whole point of wrapping the facts against their own column rather than the board's.
|
|
1049
|
+
const chipText = " tickmarkr ";
|
|
1050
|
+
const versionText = ` v${binaryVersion}`;
|
|
1051
|
+
const chipCells = cellWidth(chipText);
|
|
1052
|
+
const versionCells = cellWidth(versionText);
|
|
1053
|
+
const lockupCells = Math.max(chipCells, versionCells);
|
|
1054
|
+
const lockupGap = " ";
|
|
1055
|
+
const lockupGapCells = cellWidth(lockupGap);
|
|
1056
|
+
const factColumns = Math.max(1, boardColumns - lockupCells - lockupGapCells);
|
|
1057
|
+
// No continuation prefix: the lockup gutter below already indents every fact row, so the fact
|
|
1058
|
+
// column starts at the same cell on all of them.
|
|
1059
|
+
const factLines = wrapCells(facts, factColumns);
|
|
1060
|
+
const besideLockup = (lead, factLine) => factLine === undefined ? lead : `${lead}${lockupGap}${factLine}`;
|
|
1061
|
+
const fillLockup = (lead, cells) => `${lead}${" ".repeat(lockupCells - cells)}`;
|
|
1062
|
+
const headerRows = [
|
|
1063
|
+
besideLockup(fillLockup(LIVE.chip(chipText), chipCells), factLines[0]),
|
|
1064
|
+
besideLockup(fillLockup(dim(versionText), versionCells), factLines[1]),
|
|
1065
|
+
...factLines.slice(2).map((line) => `${" ".repeat(lockupCells + lockupGapCells)}${line}`),
|
|
1066
|
+
];
|
|
1000
1067
|
const nowLine = activity.now ? [legend(` now: ${activity.now}`)] : [];
|
|
1001
1068
|
const supervisionLegend = legend(` ${supervisionText(supervision)}`);
|
|
1002
1069
|
const taskSectionSummary = `${done} of ${total} merged · rows in graph order · gates left→right in pipeline order`;
|
|
@@ -1018,7 +1085,15 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
1018
1085
|
return [
|
|
1019
1086
|
livePhase ? `${SPINNER[animationFrame % SPINNER.length]} ${phrase ?? "running"}` : "",
|
|
1020
1087
|
!livePhase && isStarved ? "starved" : "",
|
|
1021
|
-
|
|
1088
|
+
// Attention and failure wear the ONE amethyst, so the tier word leads with its own glyph —
|
|
1089
|
+
// ! for warn, ✗ for failed. That is the distinction that survives NO_COLOR and a pipe.
|
|
1090
|
+
!livePhase && !isStarved && !merged
|
|
1091
|
+
? redTier
|
|
1092
|
+
? `${GLYPHS.fail} failed`
|
|
1093
|
+
: st === "failed" || st === "human"
|
|
1094
|
+
? `${GLYPHS.attention} ${phrase ?? (st === "failed" ? "warn" : surfaceStatusWord(st))}`
|
|
1095
|
+
: phrase ?? surfaceStatusWord(st)
|
|
1096
|
+
: "",
|
|
1022
1097
|
redTier && phrase ? phrase : "",
|
|
1023
1098
|
priorGraph ? PRIOR_GRAPH_MARKER : "",
|
|
1024
1099
|
failed.length ? `failed · ${failed.join(", ")}` : "",
|
|
@@ -1048,7 +1123,15 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
1048
1123
|
return cells.flatMap((cell) => {
|
|
1049
1124
|
const effort = effortByTask.get(cell.t.id);
|
|
1050
1125
|
const deps = cell.t.deps.length ? cell.t.deps.join(", ") : "—";
|
|
1051
|
-
const tone = cell.redTier
|
|
1126
|
+
const tone = cell.redTier
|
|
1127
|
+
? fail
|
|
1128
|
+
: cell.merged
|
|
1129
|
+
? ok
|
|
1130
|
+
: staleWorker(cell) || cell.st === "failed" || cell.st === "human"
|
|
1131
|
+
? warn
|
|
1132
|
+
: cell.livePhase || cell.st === "running"
|
|
1133
|
+
? running
|
|
1134
|
+
: dim;
|
|
1052
1135
|
const machinery = [
|
|
1053
1136
|
`deps ${deps}`,
|
|
1054
1137
|
gateMatrix(cell.states),
|
|
@@ -1078,7 +1161,15 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
1078
1161
|
wrapCells(noteFor(cell), noteWidth),
|
|
1079
1162
|
];
|
|
1080
1163
|
const height = Math.max(...columns.map((column) => column.length));
|
|
1081
|
-
const idTone = cell.redTier
|
|
1164
|
+
const idTone = cell.redTier
|
|
1165
|
+
? fail
|
|
1166
|
+
: cell.merged
|
|
1167
|
+
? ok
|
|
1168
|
+
: staleWorker(cell) || cell.st === "failed" || cell.st === "human"
|
|
1169
|
+
? warn
|
|
1170
|
+
: cell.livePhase || cell.st === "running"
|
|
1171
|
+
? running
|
|
1172
|
+
: dim;
|
|
1082
1173
|
const titleTone = cell.redTier || cell.livePhase || (effort?.parks ?? 0) >= 3 ? title : dim;
|
|
1083
1174
|
return Array.from({ length: height }, (_, index) => ` ${idTone(fitColumn(columns[0][index] ?? "", idWidth, 1))}`
|
|
1084
1175
|
+ dim(fitColumn(columns[1][index] ?? "", areaWidth))
|
|
@@ -1096,7 +1187,7 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
1096
1187
|
const effort = effortPanel(g.tasks, record && !comparable ? undefined : events, boardColumns);
|
|
1097
1188
|
return {
|
|
1098
1189
|
content: boardRows([
|
|
1099
|
-
|
|
1190
|
+
...headerRows,
|
|
1100
1191
|
...nowLine,
|
|
1101
1192
|
supervisionLegend,
|
|
1102
1193
|
"",
|
|
@@ -1142,10 +1233,12 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1142
1233
|
const namedRunId = positionalRunId(argv);
|
|
1143
1234
|
if (argv.includes("--oneline"))
|
|
1144
1235
|
return oneLine(cwd, namedRunId);
|
|
1145
|
-
//
|
|
1236
|
+
// ONE manifest read per invocation, here — every frame below names this same version.
|
|
1237
|
+
const binaryVersion = await readBinaryVersion();
|
|
1238
|
+
// The task-table frame owns its own two-row brand lockup; the four-row global banner would
|
|
1146
1239
|
// create the second header the approved replacement removes.
|
|
1147
1240
|
if (!argv.includes("--watch")) {
|
|
1148
|
-
return renderFrame(cwd, opts.now?.() ?? Date.now(), 0, undefined, false, namedRunId).content;
|
|
1241
|
+
return renderFrame(cwd, binaryVersion, opts.now?.() ?? Date.now(), 0, undefined, false, namedRunId).content;
|
|
1149
1242
|
}
|
|
1150
1243
|
const eventStream = argv.some((arg) => arg === "--events" || arg === "--jsonl" || arg === "--decision-events");
|
|
1151
1244
|
const webhookUrl = opts.webhookUrl
|
|
@@ -1173,14 +1266,21 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1173
1266
|
}
|
|
1174
1267
|
}
|
|
1175
1268
|
: undefined);
|
|
1176
|
-
|
|
1269
|
+
// Retiring a task that is no longer running is bookkeeping over data already in hand, so it stays
|
|
1270
|
+
// on the frame path where the board can rely on it every redraw.
|
|
1271
|
+
const retireInactiveWorkers = (active) => {
|
|
1177
1272
|
const activeIds = new Set(active.map((phase) => phase.taskId));
|
|
1178
1273
|
for (const taskId of workerLiveness.keys())
|
|
1179
1274
|
if (!activeIds.has(taskId))
|
|
1180
1275
|
workerLiveness.delete(taskId);
|
|
1276
|
+
};
|
|
1277
|
+
const scrapeWorkerOutput = async (active, runId, observedAt) => {
|
|
1181
1278
|
if (!readWorkerOutput || !runId)
|
|
1182
1279
|
return;
|
|
1183
|
-
|
|
1280
|
+
// allSettled, never all: Promise.all resolves the moment ONE pane read rejects, which would
|
|
1281
|
+
// clear the single-flight guard while a sibling read is still hung on its socket and let the
|
|
1282
|
+
// next budget start a second, overlapping scrape. Every read must settle before the guard drops.
|
|
1283
|
+
await Promise.allSettled(active.map(async (phase) => {
|
|
1184
1284
|
const output = await readWorkerOutput(phase.taskId, phase.attempt, runId);
|
|
1185
1285
|
if (output === undefined)
|
|
1186
1286
|
return;
|
|
@@ -1203,6 +1303,21 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1203
1303
|
prior.snapshot = snapshot;
|
|
1204
1304
|
}));
|
|
1205
1305
|
};
|
|
1306
|
+
// The scrape's own clock, and the reason a hung pane cannot freeze the board: it is STARTED here
|
|
1307
|
+
// and never awaited, so the frame loop walks straight past it. `scrapeInFlight` is what keeps a
|
|
1308
|
+
// scrape that outlives its budget from being joined by a second one — a wedged pane read would
|
|
1309
|
+
// otherwise accumulate one outstanding socket read per elapsed budget for as long as it hangs.
|
|
1310
|
+
let scrapeInFlight = false;
|
|
1311
|
+
let lastScrapeAt = Number.NEGATIVE_INFINITY;
|
|
1312
|
+
const scheduleWorkerScrape = (frame, observedAt) => {
|
|
1313
|
+
if (scrapeInFlight || observedAt - lastScrapeAt < SCRAPE_MS)
|
|
1314
|
+
return;
|
|
1315
|
+
scrapeInFlight = true;
|
|
1316
|
+
lastScrapeAt = observedAt;
|
|
1317
|
+
void scrapeWorkerOutput(frame.workerPhases, frame.runId, observedAt)
|
|
1318
|
+
.catch(() => undefined)
|
|
1319
|
+
.finally(() => { scrapeInFlight = false; });
|
|
1320
|
+
};
|
|
1206
1321
|
let decisionRunId;
|
|
1207
1322
|
let journalCursor = 0;
|
|
1208
1323
|
const consumeDecisionEvents = () => {
|
|
@@ -1265,10 +1380,10 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1265
1380
|
}
|
|
1266
1381
|
}
|
|
1267
1382
|
else {
|
|
1268
|
-
frame = renderFrame(cwd, nowMs, i, workerLiveness, true, namedRunId);
|
|
1383
|
+
frame = renderFrame(cwd, binaryVersion, nowMs, i, workerLiveness, true, namedRunId);
|
|
1269
1384
|
if (tty) {
|
|
1270
1385
|
updateTitle(frame.hotPhase, nowMs);
|
|
1271
|
-
process.stdout.write(`\x1b[2J\x1b[H${frame.content}\n${legend(` watching · refresh ${
|
|
1386
|
+
process.stdout.write(`\x1b[2J\x1b[H${frame.content}\n${legend(` watching · refresh ${FRAME_MS / 1000}s · ^C to quit`)}`);
|
|
1272
1387
|
}
|
|
1273
1388
|
else {
|
|
1274
1389
|
process.stdout.write(frame.content + sep);
|
|
@@ -1277,9 +1392,11 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1277
1392
|
frames.push(frame.content);
|
|
1278
1393
|
}
|
|
1279
1394
|
if (i + 1 < iterations) {
|
|
1280
|
-
if (frame)
|
|
1281
|
-
|
|
1282
|
-
|
|
1395
|
+
if (frame) {
|
|
1396
|
+
retireInactiveWorkers(frame.workerPhases);
|
|
1397
|
+
scheduleWorkerScrape(frame, nowMs);
|
|
1398
|
+
}
|
|
1399
|
+
await sleep(FRAME_MS);
|
|
1283
1400
|
}
|
|
1284
1401
|
}
|
|
1285
1402
|
}
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { type TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import { type Task } from "../graph/schema.js";
|
|
3
|
+
/**
|
|
4
|
+
* OBS-547: the FULL per-task collateral prediction, uncapped. ONE map is computed per run (daemon.ts,
|
|
5
|
+
* at run start) and handed whole to the scope gate, so a hit the plan display hides is still there
|
|
6
|
+
* when the gate asks about it. The lint lines below are a capped VIEW of this same map — never a
|
|
7
|
+
* second scan. Tasks with no hits are absent.
|
|
8
|
+
*/
|
|
9
|
+
export declare function collateralHits(tasks: ReadonlyArray<Pick<Task, "id" | "files">>, repoRoot: string): Map<string, string[]>;
|
|
10
|
+
/** The verbatim repair an authoring defect owes: the files[] lines the spec is missing. */
|
|
11
|
+
export declare function filesRepair(taskId: string, paths: ReadonlyArray<string>): string;
|
|
12
|
+
export interface ScopeCollateralVerdict {
|
|
13
|
+
/** every hard offender was predicted ⇒ authoring defect, repair pre-written, no attempt charged */
|
|
14
|
+
authoring: boolean;
|
|
15
|
+
/** the offenders the map had already named */
|
|
16
|
+
predicted: string[];
|
|
17
|
+
/** the offenders it had not — the lint's blind spots, recorded as evidence rather than folklore */
|
|
18
|
+
missed: string[];
|
|
19
|
+
repair: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* OBS-547: cross-reference at the RED. The gate supplies specificity (the paths actually touched),
|
|
23
|
+
* the prediction supplies classification — so there are no false positives to fear and no refusal to
|
|
24
|
+
* author. Feed this the FULL map for the task: a classifier bounded by the display cap calls a
|
|
25
|
+
* predicted 21st hit a quality failure.
|
|
26
|
+
*/
|
|
27
|
+
export declare function classifyScopeOffenders(taskId: string, hard: ReadonlyArray<string>, predicted: ReadonlyArray<string>): ScopeCollateralVerdict;
|
|
3
28
|
/**
|
|
4
29
|
* Return human-readable scope-lint lines for plan output (no `!` prefix — plan owns that).
|
|
5
30
|
* Each line names the task id and at least one missing collateral test path.
|
|
@@ -3,8 +3,12 @@ import { extname, join, relative } from "node:path";
|
|
|
3
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { criticalPathHits, DEFAULT_CONFIG, DEFAULT_REVIEW_CRITICAL_PATHS, effectiveReviewPolicy, loadConfig, } from "../config/config.js";
|
|
5
5
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
6
|
-
// Advisory
|
|
7
|
-
//
|
|
6
|
+
// Advisory scan (OBS-12/13/14/21, OBS-76). NEVER expands files[] or fails compile — a warning the
|
|
7
|
+
// author acts on. Plain-text (no AST), capped + sorted for DISPLAY only.
|
|
8
|
+
// OBS-547: the collateral prediction is no longer plan-time-only. The daemon computes one full
|
|
9
|
+
// (uncapped) map per run and hands each task's slice to its scope gate, which classifies a red
|
|
10
|
+
// against it — the prediction never causes a refusal, it only says whether a red the gate already
|
|
11
|
+
// found was foreseeable (authoring defect) or not (chargeable quality failure).
|
|
8
12
|
/** Max files walked per root (sorted walk; rest ignored). */
|
|
9
13
|
const MAX_WALK_FILES = 400;
|
|
10
14
|
/** Max collateral test paths listed per task. */
|
|
@@ -95,15 +99,17 @@ function makeReader(repoRoot) {
|
|
|
95
99
|
};
|
|
96
100
|
}
|
|
97
101
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
102
|
+
* OBS-547: the FULL per-task collateral prediction, uncapped. ONE map is computed per run (daemon.ts,
|
|
103
|
+
* at run start) and handed whole to the scope gate, so a hit the plan display hides is still there
|
|
104
|
+
* when the gate asks about it. The lint lines below are a capped VIEW of this same map — never a
|
|
105
|
+
* second scan. Tasks with no hits are absent.
|
|
100
106
|
*/
|
|
101
|
-
export function
|
|
107
|
+
export function collateralHits(tasks, repoRoot) {
|
|
108
|
+
const map = new Map();
|
|
102
109
|
const testFiles = walkCode(repoRoot, "tests");
|
|
103
110
|
if (!testFiles.length)
|
|
104
|
-
return
|
|
111
|
+
return map;
|
|
105
112
|
const read = makeReader(repoRoot);
|
|
106
|
-
const lines = [];
|
|
107
113
|
for (const t of tasks) {
|
|
108
114
|
// OBS-22: scopeGate accepts picomatch globs; advisory collateral warnings must agree.
|
|
109
115
|
const scoped = filesGlob(t.files.map((f) => f.replace(/^\.\//, "")));
|
|
@@ -122,14 +128,43 @@ export function collateralLints(tasks, repoRoot) {
|
|
|
122
128
|
if (mentions(text, needles))
|
|
123
129
|
hits.push(tf);
|
|
124
130
|
}
|
|
125
|
-
if (!hits.length)
|
|
126
|
-
continue;
|
|
127
131
|
// deterministic: walk already sorted; stable list
|
|
132
|
+
if (hits.length)
|
|
133
|
+
map.set(t.id, hits);
|
|
134
|
+
}
|
|
135
|
+
return map;
|
|
136
|
+
}
|
|
137
|
+
/** The verbatim repair an authoring defect owes: the files[] lines the spec is missing. */
|
|
138
|
+
export function filesRepair(taskId, paths) {
|
|
139
|
+
return paths.map((p) => `add ${p} to ${taskId}.files[]`).join("\n");
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* OBS-547: cross-reference at the RED. The gate supplies specificity (the paths actually touched),
|
|
143
|
+
* the prediction supplies classification — so there are no false positives to fear and no refusal to
|
|
144
|
+
* author. Feed this the FULL map for the task: a classifier bounded by the display cap calls a
|
|
145
|
+
* predicted 21st hit a quality failure.
|
|
146
|
+
*/
|
|
147
|
+
export function classifyScopeOffenders(taskId, hard, predicted) {
|
|
148
|
+
const named = new Set(predicted);
|
|
149
|
+
const hit = hard.filter((f) => named.has(f));
|
|
150
|
+
const missed = hard.filter((f) => !named.has(f));
|
|
151
|
+
return { authoring: hard.length > 0 && missed.length === 0, predicted: hit, missed, repair: filesRepair(taskId, hit) };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Return human-readable scope-lint lines for plan output (no `!` prefix — plan owns that).
|
|
155
|
+
* Each line names the task id and at least one missing collateral test path.
|
|
156
|
+
*/
|
|
157
|
+
export function collateralLints(tasks, repoRoot) {
|
|
158
|
+
const lines = [];
|
|
159
|
+
for (const [id, hits] of collateralHits(tasks, repoRoot)) {
|
|
128
160
|
const listed = hits.slice(0, MAX_HITS_PER_TASK).join(", ");
|
|
161
|
+
// OBS-547: the cap hides names, never predictions. Say so, and say where the hidden ones surface —
|
|
162
|
+
// a count with no route is exactly what left one run's victim unreadable.
|
|
129
163
|
const tail = hits.length > MAX_HITS_PER_TASK
|
|
130
|
-
? ` (${hits.length} total;
|
|
164
|
+
? ` (${hits.length} total; ${MAX_HITS_PER_TASK} shown, ${hits.length - MAX_HITS_PER_TASK} capped out of view`
|
|
165
|
+
+ ` but RETAINED for the scope gate — a matching scope red prints the hidden path with its files[] repair)`
|
|
131
166
|
: "";
|
|
132
|
-
lines.push(`${
|
|
167
|
+
lines.push(`${id}: likely collateral tests not in files[]: ${listed}${tail}`);
|
|
133
168
|
}
|
|
134
169
|
return lines;
|
|
135
170
|
}
|
package/dist/compile/native.js
CHANGED
|
@@ -728,6 +728,16 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
728
728
|
- Enumerating one axis exhaustively is what hides the others. A spec that guards PARTIAL coverage
|
|
729
729
|
site-by-site, member-by-member, can be defeated wholesale by CONDITIONAL coverage, which leaves
|
|
730
730
|
every enumeration satisfied. After you enumerate, ask what a single flag would do to the whole set.
|
|
731
|
+
- EVERY CRITERION NAMES THE PAIR IT DISCRIMINATES: the correct case that MUST PASS, and the
|
|
732
|
+
neighbouring plausible-wrong or false-clean case that MUST FAIL. Two easy examples that both pass are
|
|
733
|
+
not discrimination — they are two ways of being green, and the wrong half is the whole point: it is
|
|
734
|
+
what a reader would mistake for the mechanism, its VOCABULARY without its behaviour. Measured three
|
|
735
|
+
times in three days, each a criterion that pinned vocabulary instead of the discriminating case: a
|
|
736
|
+
suite that pinned an alarm's text while the alarm itself went unpinned; an export asserted SOMEWHERE,
|
|
737
|
+
which says nothing about the environment a process actually receives; and a gate green on "infra is
|
|
738
|
+
what execution says" while a signalled oracle still charged an attempt. Each was satisfied exactly as
|
|
739
|
+
written and each shipped the defect. If you cannot name the case that must FAIL, you have named a
|
|
740
|
+
topic, not a criterion — and the "so X fails" clause that ends a good criterion is where it goes.
|
|
731
741
|
- A criterion that names a behaviour must name the VALUE AT WHICH IT WOULD BREAK. Every criterion is a
|
|
732
742
|
claim about a variable — a status, a width, a count, an arrival time — and if it does not say which
|
|
733
743
|
value of that variable is the hard one, THE TEST WILL CHOOSE THE EASY ONE AND BE GREEN. Measured: a
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type JournalEvent } from "../run/journal.js";
|
|
1
2
|
import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
|
|
2
3
|
export declare const TRAILER_SAFE_FLOOR_COLS = 108;
|
|
3
4
|
export declare const TRAILER_WIDTH_MARGIN = 2;
|
|
@@ -22,20 +23,20 @@ export declare class DeliveryCorruptedError extends Error {
|
|
|
22
23
|
export type DriverJournal = (event: string, slotName: string, data: Record<string, unknown>) => void;
|
|
23
24
|
/** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
|
|
24
25
|
export declare function workerSplitDirection(paneCols: number | null, safeFloor?: number, margin?: number): "right" | "down";
|
|
25
|
-
export declare const
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
direction: "
|
|
29
|
-
/** herdr's split ratio is the FIRST child's share and a
|
|
30
|
-
*
|
|
31
|
-
ratio
|
|
32
|
-
/**
|
|
33
|
-
|
|
26
|
+
export declare const BOARD_HEIGHT_SHARE = 0.72;
|
|
27
|
+
export interface BoardPlacement {
|
|
28
|
+
/** Always down: the split is vertical, so the board can own the caller's FULL width. */
|
|
29
|
+
direction: "down";
|
|
30
|
+
/** herdr's split ratio is the FIRST child's share, and a down split's first child is the TOP
|
|
31
|
+
* region — the region the board occupies once it is swapped above the caller. */
|
|
32
|
+
ratio: number;
|
|
33
|
+
/** The new pane is swapped ABOVE the caller; the split alone would leave the board underneath. */
|
|
34
|
+
swap: "above";
|
|
34
35
|
}
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
export declare function boardSplitPlan(
|
|
36
|
+
/** The single approved vertical-stack record. The caller's columns are accepted and deliberately
|
|
37
|
+
* ignored: this signature is where width used to decide the arrangement, and the parameter stays
|
|
38
|
+
* so that "the plan does not depend on it" is a property a caller (and a test) can exercise. */
|
|
39
|
+
export declare function boardSplitPlan(_callerCols?: number | null): BoardPlacement;
|
|
39
40
|
export declare function taskGroupOf(name: string): string | undefined;
|
|
40
41
|
/** The operator-facing TITLE for a tab this driver creates when no stage label is supplied: a task
|
|
41
42
|
* token for a worker (`T5`, `T5↻2` on a retry) and ROLE + task for a gate pane (`REVIEW T5`) — the
|
|
@@ -63,11 +64,14 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
63
64
|
private inputBoxes;
|
|
64
65
|
private bootstraps;
|
|
65
66
|
private journalRoots;
|
|
67
|
+
private narrate?;
|
|
66
68
|
private ws;
|
|
67
69
|
private callerPane;
|
|
68
70
|
private watches;
|
|
69
71
|
constructor(bin?: string, workersPerTab?: number, time?: HerdrTimeSource, journal?: DriverJournal | undefined);
|
|
70
72
|
private appendDispatchRetry;
|
|
73
|
+
/** v1.99 T2: bind this driver's own journal writes to the run's live narration sink. */
|
|
74
|
+
narrateWith(narrate: (event: JournalEvent) => void): void;
|
|
71
75
|
private serial;
|
|
72
76
|
private deliveryQueue;
|
|
73
77
|
private reserveDispatch;
|
|
@@ -77,6 +81,7 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
77
81
|
static available(): boolean;
|
|
78
82
|
private herdr;
|
|
79
83
|
private namedPaneId;
|
|
84
|
+
private paneStillOpen;
|
|
80
85
|
private paneId;
|
|
81
86
|
private paneWidth;
|
|
82
87
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
@@ -116,6 +121,7 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
116
121
|
private closeGrouped;
|
|
117
122
|
private ownedWatchPanes;
|
|
118
123
|
private watchSlot;
|
|
124
|
+
private discardSplit;
|
|
119
125
|
narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
|
|
120
126
|
reconcile(desired: Set<string>, runId: string, opts?: {
|
|
121
127
|
spareLiveLlm?: boolean;
|