tickmarkr 1.84.0 → 1.86.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/README.md +4 -2
- package/dist/adapters/catalog-remote.d.ts +64 -0
- package/dist/adapters/catalog-remote.js +287 -0
- package/dist/adapters/catalog.d.ts +96 -0
- package/dist/adapters/catalog.js +176 -0
- package/dist/adapters/claude-code.d.ts +1 -0
- package/dist/adapters/claude-code.js +59 -1
- package/dist/adapters/fake.js +42 -4
- package/dist/adapters/model-lints.d.ts +25 -5
- package/dist/adapters/model-lints.js +184 -50
- package/dist/adapters/model-windows.d.ts +31 -0
- package/dist/adapters/model-windows.js +69 -0
- package/dist/adapters/prompt.d.ts +5 -1
- package/dist/adapters/prompt.js +13 -4
- package/dist/adapters/registry.d.ts +25 -26
- package/dist/adapters/registry.js +173 -110
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +36 -3
- package/dist/brand.d.ts +5 -1
- package/dist/brand.js +18 -2
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +43 -21
- package/dist/cli/commands/fleet.d.ts +7 -0
- package/dist/cli/commands/fleet.js +94 -74
- package/dist/cli/commands/init.js +118 -5
- package/dist/cli/commands/status.js +202 -46
- package/dist/compile/collateral.d.ts +86 -2
- package/dist/compile/collateral.js +294 -3
- package/dist/compile/gsd.d.ts +2 -1
- package/dist/compile/gsd.js +68 -2
- package/dist/compile/native.d.ts +14 -0
- package/dist/compile/native.js +161 -12
- package/dist/config/config.d.ts +82 -5
- package/dist/config/config.js +253 -66
- package/dist/config/fleet-overlay.d.ts +25 -20
- package/dist/config/fleet-overlay.js +195 -77
- package/dist/config/fleet-why.d.ts +23 -0
- package/dist/config/fleet-why.js +42 -0
- package/dist/drivers/herdr.d.ts +21 -3
- package/dist/drivers/herdr.js +344 -110
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +1 -0
- package/dist/gates/baseline.js +91 -13
- package/dist/gates/llm.d.ts +0 -1
- package/dist/gates/llm.js +5 -30
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +105 -10
- package/dist/gates/run-gates.d.ts +9 -0
- package/dist/gates/run-gates.js +285 -41
- package/dist/gates/verdict-cause.d.ts +4 -0
- package/dist/gates/verdict-cause.js +63 -0
- package/dist/graph/schema.d.ts +6 -0
- package/dist/graph/schema.js +8 -5
- package/dist/route/router.d.ts +0 -5
- package/dist/route/router.js +16 -20
- package/dist/run/consult.d.ts +6 -0
- package/dist/run/consult.js +35 -25
- package/dist/run/daemon.d.ts +48 -2
- package/dist/run/daemon.js +1488 -330
- package/dist/run/journal.d.ts +56 -3
- package/dist/run/journal.js +358 -4
- package/dist/run/stall.d.ts +35 -1
- package/dist/run/stall.js +118 -8
- package/dist/tui/cockpit/capture.d.ts +12 -0
- package/dist/tui/cockpit/capture.js +37 -1
- package/dist/tui/cockpit/components.d.ts +2 -0
- package/dist/tui/cockpit/components.js +8 -8
- package/dist/tui/cockpit/derive.d.ts +29 -2
- package/dist/tui/cockpit/derive.js +219 -23
- package/dist/tui/cockpit/run-cockpit.js +128 -27
- package/dist/tui/cockpit/theme.d.ts +32 -26
- package/dist/tui/cockpit/theme.js +11 -5
- package/dist/tui/ink/components.d.ts +0 -15
- package/dist/tui/ink/components.js +0 -17
- package/dist/tui/ink/fleet-app.d.ts +4 -1
- package/dist/tui/ink/fleet-app.js +134 -13
- package/fixtures/sample.native.md +1 -1
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +354 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
- package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
- package/dist/tui/ink/studio-app.d.ts +0 -59
- package/dist/tui/ink/studio-app.js +0 -320
- package/dist/tui/save.d.ts +0 -38
- package/dist/tui/save.js +0 -96
- package/dist/tui/staging.d.ts +0 -29
- package/dist/tui/staging.js +0 -78
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { BANNER, GLYPHS, dim, fail, legend, ok, rule, statusRow, title, warn } from "../../brand.js";
|
|
2
4
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
3
5
|
import { formatOwnedName, } from "../../drivers/types.js";
|
|
@@ -6,10 +8,53 @@ import { GATE_NAMES } from "../../graph/schema.js";
|
|
|
6
8
|
import { foldActivity } from "../../run/activity.js";
|
|
7
9
|
import { Journal, engagementComparable, isQualityFailureParkKind, recordedTaskFailureKind, runHasEnded, } from "../../run/journal.js";
|
|
8
10
|
import { normalizeStallSnapshot } from "../../run/stall.js";
|
|
11
|
+
import { deriveRunCockpitData, } from "../../tui/cockpit/derive.js";
|
|
12
|
+
import { cellWidth, fitCells } from "../../tui/cockpit/width.js";
|
|
9
13
|
// ponytail: fixed 2s refresh; promote to config.visibility.* only when an operator asks.
|
|
10
14
|
const REFRESH_MS = 2000;
|
|
11
15
|
const NOT_COMPARABLE_NOTICE = "graph recompiled since this run — task states not comparable; resume with `--graph-changed` to audit this recompile";
|
|
12
16
|
const PRIOR_GRAPH_MARKER = "prior graph";
|
|
17
|
+
// One compact host-zone label per screen. Each event below is still converted from its own
|
|
18
|
+
// complete UTC instant, so the zone's historical rules — including a transition within one frame
|
|
19
|
+
// — stay intact rather than being rendered through this label's single sampled offset.
|
|
20
|
+
const localZoneLabel = (reference = new Date()) => {
|
|
21
|
+
const offset = -reference.getTimezoneOffset();
|
|
22
|
+
if (offset === 0)
|
|
23
|
+
return "UTC";
|
|
24
|
+
const sign = offset < 0 ? "-" : "+";
|
|
25
|
+
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0");
|
|
26
|
+
const minutes = Math.abs(offset) % 60;
|
|
27
|
+
return minutes === 0
|
|
28
|
+
? `${sign}${hours}`
|
|
29
|
+
: `${sign}${hours}:${String(minutes).padStart(2, "0")}`;
|
|
30
|
+
};
|
|
31
|
+
const localClockText = (content, reference = new Date()) => {
|
|
32
|
+
return content.replace(/\b([01]\d|2[0-3]):([0-5]\d):([0-5]\d)\b/gu, (_whole, h, m, s) => {
|
|
33
|
+
const instant = new Date(Date.UTC(reference.getUTCFullYear(), reference.getUTCMonth(), reference.getUTCDate(), Number(h), Number(m), Number(s)));
|
|
34
|
+
return [instant.getHours(), instant.getMinutes(), instant.getSeconds()]
|
|
35
|
+
.map((part) => String(part).padStart(2, "0"))
|
|
36
|
+
.join(":");
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
/** The complete journal instant behind a folded clock phrase, not a screen-wide offset sample. */
|
|
40
|
+
const taskClockReference = (events, taskId, content, fallback) => {
|
|
41
|
+
const utcClock = content.match(/\b(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\b/u)?.[0];
|
|
42
|
+
if (utcClock === undefined)
|
|
43
|
+
return fallback;
|
|
44
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
45
|
+
const event = events[index];
|
|
46
|
+
if (event.taskId !== taskId || event.ts.slice(11, 19) !== utcClock)
|
|
47
|
+
continue;
|
|
48
|
+
const instant = new Date(event.ts);
|
|
49
|
+
if (Number.isFinite(instant.getTime()))
|
|
50
|
+
return instant;
|
|
51
|
+
}
|
|
52
|
+
return fallback;
|
|
53
|
+
};
|
|
54
|
+
// Every attempt number a cell phrase carries is the engagement's own ladder (foldActivity counts
|
|
55
|
+
// dispatch labels from 1). Name that ruler wherever a bare one appears, not only in the one phrase
|
|
56
|
+
// shipping today — a phrase added upstream must not reach the operator unruled.
|
|
57
|
+
const labelAttemptRuler = (text) => text.replace(/(?<!engagement |run )\battempt (?=\d)/gu, "engagement attempt ");
|
|
13
58
|
// The timer must keep the process ALIVE: an unref'd timer here let the event loop drain after the
|
|
14
59
|
// first frame, so a live `--watch` printed once and exited 0 (OBS-11). Never unref this.
|
|
15
60
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -88,6 +133,20 @@ const optionValue = (argv, name) => {
|
|
|
88
133
|
const value = index >= 0 ? argv[index + 1] : undefined;
|
|
89
134
|
return value && !value.startsWith("-") ? value : undefined;
|
|
90
135
|
};
|
|
136
|
+
// The one positional this command takes: an explicit run id. Everything else is a flag or a flag's
|
|
137
|
+
// value (`--webhook <url>`), so the first bare token that is not a --webhook value is the run id.
|
|
138
|
+
const positionalRunId = (argv) => {
|
|
139
|
+
for (let i = 0; i < argv.length; i++) {
|
|
140
|
+
const arg = argv[i];
|
|
141
|
+
if (arg === "--webhook") {
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!arg.startsWith("-"))
|
|
146
|
+
return arg;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
};
|
|
91
150
|
const defaultPostWebhook = (url, event) => fetch(url, {
|
|
92
151
|
method: "POST",
|
|
93
152
|
headers: { "content-type": "application/json" },
|
|
@@ -196,6 +255,25 @@ export const taskBox = (status) => {
|
|
|
196
255
|
return "[!]";
|
|
197
256
|
return "[ ]";
|
|
198
257
|
};
|
|
258
|
+
const graphTaskStatus = (state, fallback) => {
|
|
259
|
+
if (state === "completed" || state === "done")
|
|
260
|
+
return "done";
|
|
261
|
+
if (state === "interrupted")
|
|
262
|
+
return "failed";
|
|
263
|
+
return state ?? fallback;
|
|
264
|
+
};
|
|
265
|
+
const surfaceStatusWord = (state) => {
|
|
266
|
+
if (state === "human")
|
|
267
|
+
return "parked";
|
|
268
|
+
return state;
|
|
269
|
+
};
|
|
270
|
+
const surfaceTaskBox = (state, merged) => {
|
|
271
|
+
if (merged)
|
|
272
|
+
return "[x]";
|
|
273
|
+
if (state === "failed" || state === "human")
|
|
274
|
+
return "[!]";
|
|
275
|
+
return "[ ]";
|
|
276
|
+
};
|
|
199
277
|
export const gateBox = (state, unicode) => {
|
|
200
278
|
if (unicode) {
|
|
201
279
|
// shared glyph vocabulary: pass/fail verdicts, dash for skip, dim circle for not-yet-run
|
|
@@ -244,13 +322,18 @@ const failedSuffix = (states) => {
|
|
|
244
322
|
// cell when present: a post-approval park is not awaiting the designed gate. Plain text for column
|
|
245
323
|
// math; rendered with a dim dot + warn words. TTY-only — the non-TTY surface stays byte-pinned.
|
|
246
324
|
export const humanGateSuffix = (t, st, states) => st === "human" && t.humanGate && failedGates(states).length === 0 ? " · awaiting approval" : "";
|
|
325
|
+
// Task titles are graph-authored data. Remove terminal controls and row-breaking whitespace before
|
|
326
|
+
// either width measurement or rendering so zero-cell ECMA-48 bytes cannot escape the task column.
|
|
327
|
+
const ECMA_48_CONTROL = /(?:\u001B\][\s\S]*?(?:\u0007|\u001B\\)|\u009D[\s\S]*?(?:\u0007|\u009C)|\u001B[P^_X][\s\S]*?\u001B\\|[\u0090\u0098\u009E\u009F][\s\S]*?\u009C|(?:\u001B\[|\u009B)[0-?]*[ -/]*[@-~]|\u001B[@-_])/gu;
|
|
328
|
+
const sanitizeTaskText = (text) => text.replace(ECMA_48_CONTROL, "").replace(/\p{Cc}/gu, " ").replace(/\s+/gu, " ").trim();
|
|
247
329
|
export const shortGoal = (goal, max) => {
|
|
248
|
-
const clause = goal.split(/[,;.?!]/, 1)[0].trim();
|
|
249
|
-
|
|
330
|
+
const clause = sanitizeTaskText(goal).split(/[,;.?!]/, 1)[0].trim();
|
|
331
|
+
const cells = Math.max(0, Math.floor(max));
|
|
332
|
+
if (cellWidth(clause) <= cells)
|
|
250
333
|
return clause;
|
|
251
|
-
if (
|
|
252
|
-
return clause
|
|
253
|
-
return `${clause
|
|
334
|
+
if (cells <= 3)
|
|
335
|
+
return fitCells(clause, cells).trimEnd();
|
|
336
|
+
return `${fitCells(clause, cells - 3).trimEnd()}...`;
|
|
254
337
|
};
|
|
255
338
|
// VIS-11 (v1.13): a liveness header for renderFrame — last journal event age + whether the recorded
|
|
256
339
|
// daemon pid is still alive. Honest about unknowns: a pre-v1.13 journal with no pid renders "unknown",
|
|
@@ -366,7 +449,7 @@ const tipVerifyText = (phase) => {
|
|
|
366
449
|
return "tip-verify: pending";
|
|
367
450
|
const gates = phase.gates.length ? phase.gates.join(", ") : "gate unknown";
|
|
368
451
|
const failed = `tip-verify: FAILED (${gates})`;
|
|
369
|
-
const reverify = phase.state === "re-verifying" ? ` → re-verifying (attempt ${phase.attempt})` : "";
|
|
452
|
+
const reverify = phase.state === "re-verifying" ? ` → re-verifying (run attempt ${phase.attempt})` : "";
|
|
370
453
|
const fingerprints = phase.fingerprints === undefined
|
|
371
454
|
? ""
|
|
372
455
|
: ` · ${phase.fingerprints} fingerprint${phase.fingerprints === 1 ? "" : "s"}`;
|
|
@@ -412,11 +495,26 @@ const statusEngagement = (events, loadedHash) => {
|
|
|
412
495
|
}
|
|
413
496
|
return { comparable: baseline.comparable };
|
|
414
497
|
};
|
|
415
|
-
|
|
498
|
+
// The journal's own reader rule (src/run/journal.ts readJsonl), applied to bytes already in hand:
|
|
499
|
+
// skip blanks, drop a line that will not parse (a torn trailing write after a crash), keep the rest.
|
|
500
|
+
const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
|
|
501
|
+
if (!line.trim())
|
|
502
|
+
return [];
|
|
503
|
+
try {
|
|
504
|
+
return [JSON.parse(line)];
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return [];
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness = new Map(), journalRowsOnly = false, namedRunId) => {
|
|
416
511
|
const g = loadGraph(cwd);
|
|
417
|
-
|
|
512
|
+
// An explicit <runId> is a resolution, not a hint: Journal.open below refuses an id without a
|
|
513
|
+
// readable journal, so status fails loudly naming that id instead of rendering any other run.
|
|
514
|
+
const runId = namedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
418
515
|
const assignments = new Map();
|
|
419
516
|
let replayed = null;
|
|
517
|
+
let cockpit;
|
|
420
518
|
let events = [];
|
|
421
519
|
const contexts = new Map();
|
|
422
520
|
let comparable = false;
|
|
@@ -424,7 +522,19 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
424
522
|
let supersededBy; // v1.53 T5: this run is dead — a newer run replaced it
|
|
425
523
|
if (runId) {
|
|
426
524
|
const j = Journal.open(cwd, runId);
|
|
427
|
-
|
|
525
|
+
// ONE byte snapshot per frame. The cockpit fold and every decoration below — foldActivity,
|
|
526
|
+
// livePhases, tipVerifyPhase, gateSnapshot, the failure and liveness readings — fold THESE
|
|
527
|
+
// bytes and nothing else, so a line the daemon appends mid-frame can never pair newer task
|
|
528
|
+
// rows against older activity, phases, gates or tip state. `Journal.open` refuses a run
|
|
529
|
+
// without a journal, so the file is there to read.
|
|
530
|
+
const raw = readFileSync(join(j.dir, "journal.jsonl"), "utf8");
|
|
531
|
+
events = parseJournalSnapshot(raw);
|
|
532
|
+
// The daemon creates journal.jsonl with lock-reclaimed before run-start and a reader may also
|
|
533
|
+
// catch an empty/torn first write. Those snapshots own no task fold yet; once run-start exists,
|
|
534
|
+
// however, a fold failure is unexpected and must surface instead of becoming a false empty run.
|
|
535
|
+
if (journalRowsOnly && events.some((event) => event.event === "run-start")) {
|
|
536
|
+
cockpit = deriveRunCockpitData({ fileName: `${runId}.journal.jsonl`, raw }, "status", { graph: g });
|
|
537
|
+
}
|
|
428
538
|
const sup = [...events].reverse().find((e) => e.event === "superseded" && typeof e.data.by === "string");
|
|
429
539
|
supersededBy = sup?.data.by;
|
|
430
540
|
// The resume comparator is the fail-closed baseline; a matching graph-rehash is the daemon's
|
|
@@ -433,12 +543,14 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
433
543
|
comparable = engagement.comparable;
|
|
434
544
|
rehashAt = engagement.rehashAt;
|
|
435
545
|
if (comparable) {
|
|
436
|
-
|
|
546
|
+
if (!journalRowsOnly)
|
|
547
|
+
replayed = j.replayStatuses();
|
|
437
548
|
for (const e of events) {
|
|
438
|
-
if (e.event === "task-dispatch" && e.taskId) {
|
|
549
|
+
if (!journalRowsOnly && e.event === "task-dispatch" && e.taskId) {
|
|
439
550
|
const a = e.data.assignment;
|
|
440
|
-
if (typeof a.adapter === "string" && typeof a.model === "string")
|
|
551
|
+
if (typeof a.adapter === "string" && typeof a.model === "string") {
|
|
441
552
|
assignments.set(e.taskId, `${a.adapter}:${a.model}`);
|
|
553
|
+
}
|
|
442
554
|
}
|
|
443
555
|
if (e.event === "context-sample" && e.taskId && typeof e.data.tokens === "number" && Number.isFinite(e.data.tokens)) {
|
|
444
556
|
contexts.set(e.taskId, e.data.tokens); // last write wins
|
|
@@ -446,7 +558,34 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
446
558
|
}
|
|
447
559
|
}
|
|
448
560
|
}
|
|
449
|
-
const
|
|
561
|
+
const taskRows = new Map(cockpit?.taskRows.map((row) => [row.taskId, row]) ?? []);
|
|
562
|
+
const clockFallback = new Date(now);
|
|
563
|
+
const recordedClock = new Date(events.at(-1)?.ts ?? now);
|
|
564
|
+
const zoneReference = Number.isFinite(recordedClock.getTime()) ? recordedClock : clockFallback;
|
|
565
|
+
const effective = {
|
|
566
|
+
...g,
|
|
567
|
+
tasks: g.tasks.map((task) => ({
|
|
568
|
+
...task,
|
|
569
|
+
status: comparable
|
|
570
|
+
? journalRowsOnly
|
|
571
|
+
? graphTaskStatus(taskRows.get(task.id)?.state, task.status)
|
|
572
|
+
: replayed?.get(task.id) ?? task.status
|
|
573
|
+
: task.status,
|
|
574
|
+
})),
|
|
575
|
+
};
|
|
576
|
+
const renderedTasks = journalRowsOnly
|
|
577
|
+
? cockpit !== undefined && comparable
|
|
578
|
+
? g.tasks.filter((task) => {
|
|
579
|
+
const row = taskRows.get(task.id);
|
|
580
|
+
return row !== undefined && (row.state !== undefined
|
|
581
|
+
|| row.lastEventTime !== undefined
|
|
582
|
+
|| row.attempts !== undefined
|
|
583
|
+
|| row.actor !== undefined
|
|
584
|
+
|| row.merged === true
|
|
585
|
+
|| row.parkKind !== undefined);
|
|
586
|
+
})
|
|
587
|
+
: []
|
|
588
|
+
: g.tasks;
|
|
450
589
|
const starved = new Set(blockedTasks(effective).map((t) => t.id));
|
|
451
590
|
// OBS-104: ONE activity fold feeds both surfaces — never re-derived here. Comparable events only
|
|
452
591
|
// (a recompiled graph's journal must not animate the wrong tasks); with no or stale journal the
|
|
@@ -455,7 +594,10 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
455
594
|
const unicode = visual();
|
|
456
595
|
const divider = unicode ? " · " : " / ";
|
|
457
596
|
const width = process.stdout.columns ?? 120;
|
|
458
|
-
const done =
|
|
597
|
+
const done = journalRowsOnly
|
|
598
|
+
? renderedTasks.filter((task) => graphTaskStatus(taskRows.get(task.id)?.state, task.status) === "done").length
|
|
599
|
+
: effective.tasks.filter((task) => task.status === "done").length;
|
|
600
|
+
const total = g.tasks.length;
|
|
459
601
|
const ended = comparable && runHasEnded(events);
|
|
460
602
|
const tipPhase = comparable ? tipVerifyPhase(events) : undefined;
|
|
461
603
|
const taskIds = new Set(g.tasks.map((task) => task.id));
|
|
@@ -464,8 +606,10 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
464
606
|
if (!taskIds.has(taskId))
|
|
465
607
|
phases.delete(taskId);
|
|
466
608
|
const hotPhase = [...phases.values()].sort((a, b) => a.order - b.order).at(-1);
|
|
467
|
-
const cells =
|
|
468
|
-
const
|
|
609
|
+
const cells = renderedTasks.map((t) => {
|
|
610
|
+
const folded = journalRowsOnly && comparable ? taskRows.get(t.id) : undefined;
|
|
611
|
+
const st = folded?.state ?? replayed?.get(t.id) ?? t.status;
|
|
612
|
+
const merged = journalRowsOnly ? folded?.merged === true : st === "done" || st === "completed";
|
|
469
613
|
const failureKind = comparable ? recordedTaskFailureKind(events, t.id) : undefined;
|
|
470
614
|
const terminal = st === "failed" || st === "human";
|
|
471
615
|
// Unknown legacy task-failed events stay red. Typed availability noise is warn-tier only while
|
|
@@ -475,28 +619,34 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
475
619
|
|| (st === "failed" && failureKind === undefined));
|
|
476
620
|
const livePhase = phases.get(t.id);
|
|
477
621
|
const isStarved = !livePhase && starved.has(t.id);
|
|
478
|
-
const
|
|
622
|
+
const rawPhrase = livePhase ? phaseDetail(livePhase, now, workerLiveness.get(t.id)) : isStarved ? undefined : activity.cells.get(t.id);
|
|
623
|
+
const phrase = rawPhrase === undefined
|
|
624
|
+
? undefined
|
|
625
|
+
: journalRowsOnly
|
|
626
|
+
? labelAttemptRuler(localClockText(rawPhrase, taskClockReference(events, t.id, rawPhrase, clockFallback)))
|
|
627
|
+
: rawPhrase;
|
|
479
628
|
const label = isStarved ? " starved" : phrase ? ` ${phrase}` : "";
|
|
480
|
-
const channel = assignments.get(t.id) ?? "-";
|
|
629
|
+
const channel = journalRowsOnly ? folded?.actor ?? "-" : assignments.get(t.id) ?? "-";
|
|
481
630
|
const ctx = contexts.get(t.id);
|
|
482
631
|
const assignCol = ctx !== undefined ? `${channel}${divider}ctx ${ctx}` : channel;
|
|
483
632
|
const gates = comparable
|
|
484
633
|
? gateSnapshot(t, events, rehashAt)
|
|
485
634
|
: { states: defaultGateStates(t), priorGraph: false };
|
|
486
|
-
return { t, st, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, ...gates };
|
|
635
|
+
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, ...gates };
|
|
487
636
|
});
|
|
488
637
|
if (!unicode) {
|
|
489
638
|
// machine/CI surface — journals without phase-start stay byte-identical; new phase-aware frames
|
|
490
639
|
// use an ASCII spinner so pipes never receive terminal-only braille/ANSI.
|
|
491
|
-
const rows = cells.map(({ t, st, label, assignCol, livePhase, states, priorGraph }) => {
|
|
640
|
+
const rows = cells.map(({ t, st, merged, label, assignCol, livePhase, states, priorGraph }) => {
|
|
492
641
|
const chain = gateChain(states, false);
|
|
493
|
-
const prefix = livePhase ? ` ${ASCII_SPINNER[animationFrame % ASCII_SPINNER.length]} ${t.id} ` : ` ${
|
|
494
|
-
const suffix = ` ${chain}${priorGraph ? ` ${PRIOR_GRAPH_MARKER}` : ""} ${livePhase ? "running" :
|
|
495
|
-
return `${prefix}${shortGoal(t.
|
|
642
|
+
const prefix = livePhase ? ` ${ASCII_SPINNER[animationFrame % ASCII_SPINNER.length]} ${t.id} ` : ` ${surfaceTaskBox(st, merged)} ${t.id} `;
|
|
643
|
+
const suffix = ` ${chain}${priorGraph ? ` ${PRIOR_GRAPH_MARKER}` : ""} ${livePhase ? "running" : surfaceStatusWord(st)}${label} ${assignCol}`;
|
|
644
|
+
return `${prefix}${shortGoal(t.title, Math.max(0, width - prefix.length - suffix.length))}${suffix}`;
|
|
496
645
|
});
|
|
646
|
+
const zone = journalRowsOnly ? `${divider}zone ${localZoneLabel(zoneReference)}` : "";
|
|
497
647
|
const header = runId
|
|
498
|
-
? `tickmarkr status${divider}run ${runId}${supersededBy ? `${divider}superseded by ${supersededBy}` : ""}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events, now).replaceAll(" · ", divider)}${tipPhase ? `${divider}${tipVerifyText(tipPhase)}` : ""}${divider}${tipPhase ? `${done}/${
|
|
499
|
-
: `tickmarkr status${divider}no runs yet${divider}${done}/${
|
|
648
|
+
? `tickmarkr status${divider}run ${runId}${zone}${supersededBy ? `${divider}superseded by ${supersededBy}` : ""}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events, now).replaceAll(" · ", divider)}${tipPhase ? `${divider}${tipVerifyText(tipPhase)}` : ""}${divider}${tipPhase ? `${done}/${total} tasks done${divider}run not verified` : `${done}/${total} done`}`
|
|
649
|
+
: `tickmarkr status${zone}${divider}no runs yet${divider}${done}/${total} done`;
|
|
500
650
|
const legendLine = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
501
651
|
return {
|
|
502
652
|
content: [header, legendLine, ...rows].join("\n"),
|
|
@@ -512,7 +662,7 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
512
662
|
const dot = dim(" · ");
|
|
513
663
|
const anyFailed = cells.some((c) => c.redTier);
|
|
514
664
|
const gaugeCells = 10;
|
|
515
|
-
const fill =
|
|
665
|
+
const fill = total ? Math.round((done / total) * gaugeCells) : 0;
|
|
516
666
|
const tipFailed = tipPhase?.state === "failed";
|
|
517
667
|
const progressTone = anyFailed || tipFailed ? fail : tipPhase ? warn : ok;
|
|
518
668
|
const gauge = (fill ? progressTone("█".repeat(fill)) : "") + (fill < gaugeCells ? dim("░".repeat(gaugeCells - fill)) : "");
|
|
@@ -522,16 +672,17 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
522
672
|
.replace(/\balive\b/, ok("alive"))
|
|
523
673
|
.replaceAll(" · ", dot);
|
|
524
674
|
const tally = tipPhase
|
|
525
|
-
? `${progressTone(`${done}/${
|
|
526
|
-
: `${done}/${
|
|
675
|
+
? `${progressTone(`${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
|
|
676
|
+
: `${done}/${total} done`;
|
|
527
677
|
const tipStatus = tipPhase
|
|
528
678
|
? `${tipFailed ? fail(tipVerifyText(tipPhase)) : warn(tipVerifyText(tipPhase))}${dot}`
|
|
529
679
|
: "";
|
|
530
|
-
const
|
|
680
|
+
const zone = journalRowsOnly ? `zone ${localZoneLabel(zoneReference)}${dot}` : "";
|
|
681
|
+
const header = ` ${title(runId ? `run ${runId}` : "tickmarkr")}${dot}${zone}` +
|
|
531
682
|
(runId
|
|
532
683
|
? `${supersededBy ? `${warn(`superseded by ${supersededBy}`)}${dot}` : ""}${!comparable ? `${warn(NOT_COMPARABLE_NOTICE)}${dot}` : ""}${live}${dot}${tipStatus}`
|
|
533
684
|
: `no runs yet${dot}`) +
|
|
534
|
-
`${gauge} ${tipPhase ? tally : done ===
|
|
685
|
+
`${gauge} ${tipPhase ? tally : done === total && total > 0 ? ok(tally) : tally}`;
|
|
535
686
|
const hr = rule(Math.min(width, 100));
|
|
536
687
|
// OBS-104 run-level now line: names the most recent journal event. TTY frame only — the non-TTY
|
|
537
688
|
// machine surface is byte-pinned (status-brand golden) and must not drift. Rendered BELOW the task
|
|
@@ -539,33 +690,35 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
539
690
|
const nowLine = activity.now ? [legend(` now: ${activity.now}`)] : [];
|
|
540
691
|
const gatesLegend = legend(` gates: ${GATE_NAMES.join(" · ")}`);
|
|
541
692
|
// Two-line card per task (operator request, v1.67): line 1 carries identity + verdict — glyph,
|
|
542
|
-
// id,
|
|
543
|
-
// aligned under the
|
|
544
|
-
// and activity phrases live on line 2 only, so they can never squeeze the
|
|
545
|
-
const taskVerdict = (c) => c.
|
|
546
|
-
const statusWord = (c) => c.livePhase ? "running" : c.redTier ? "failed" : c.st === "failed" ? "warn" :
|
|
693
|
+
// id, title at full width, and only SHORT status words. Line 2 carries the machinery, dim and
|
|
694
|
+
// aligned under the title: gate chain, live activity phrase (or channel), ctx. Long channel names
|
|
695
|
+
// and activity phrases live on line 2 only, so they can never squeeze the title or wrap line 1.
|
|
696
|
+
const taskVerdict = (c) => c.merged ? "pass" : c.redTier ? "fail" : c.st === "failed" || c.st === "human" ? "warn" : "neutral";
|
|
697
|
+
const statusWord = (c) => c.livePhase ? "running" : c.redTier ? "failed" : c.st === "failed" ? "warn" : surfaceStatusWord(c.st);
|
|
547
698
|
const idW = Math.max(...cells.map((c) => c.t.id.length), 2);
|
|
548
699
|
// plain-text status suffixes for width math only — starved / failed gate names / approval hint
|
|
549
|
-
const suffixPlain = (c) => (c.isStarved ? " · starved" : "") + failedSuffix(c.states)
|
|
700
|
+
const suffixPlain = (c) => (c.isStarved ? " · starved" : "") + failedSuffix(c.states)
|
|
701
|
+
+ humanGateSuffix(c.t, graphTaskStatus(c.st, c.t.status), c.states);
|
|
550
702
|
const stW = Math.max(...cells.map((c) => statusWord(c).length + suffixPlain(c).length));
|
|
551
703
|
const avail = Math.max(8, width - (5 + idW) - 2 - stW);
|
|
552
|
-
const
|
|
553
|
-
const
|
|
554
|
-
const indent = " ".repeat(idW + 5); // line 2 starts under the
|
|
704
|
+
const titles = cells.map((c) => shortGoal(c.t.title, avail));
|
|
705
|
+
const titleW = Math.max(8, ...titles.map(cellWidth));
|
|
706
|
+
const indent = " ".repeat(idW + 5); // line 2 starts under the title column
|
|
555
707
|
const rows = cells.map((c, i) => {
|
|
556
|
-
const { t, st, failureKind, redTier, states, priorGraph, isStarved, phrase, channel, ctx, livePhase } = c;
|
|
708
|
+
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, channel, ctx, livePhase } = c;
|
|
557
709
|
const word = statusWord(c);
|
|
558
710
|
const staleWorker = livePhase?.phase === "worker"
|
|
559
711
|
&& workerOutputAge(livePhase, now, workerLiveness.get(t.id)) >= 60_000;
|
|
560
|
-
const stWord = staleWorker ? warn(word) :
|
|
712
|
+
const stWord = staleWorker ? warn(word) : merged ? ok(word) : redTier ? fail(word) : st === "failed" || st === "human" ? warn(word) : word;
|
|
561
713
|
// a fail names its gate in words right here — the one moment gate identity is needed on a row
|
|
562
714
|
const f = failedGates(states);
|
|
563
|
-
const human = humanGateSuffix(t, st, states);
|
|
715
|
+
const human = humanGateSuffix(t, graphTaskStatus(st, t.status), states);
|
|
564
716
|
const statusCell = stWord +
|
|
565
717
|
(isStarved ? dot + fail("starved") : "") +
|
|
566
718
|
(f.length ? dot + fail(f.join(", ")) : "") +
|
|
567
719
|
(human ? dot + warn("awaiting approval") : "");
|
|
568
|
-
const
|
|
720
|
+
const taskTitle = titles[i];
|
|
721
|
+
const taskLabel = `${t.id.padEnd(idW)} ${taskTitle}${" ".repeat(titleW - cellWidth(taskTitle))} ${statusCell}`;
|
|
569
722
|
const line1 = livePhase
|
|
570
723
|
? ` ${(staleWorker ? warn : dim)(SPINNER[animationFrame % SPINNER.length])} ${taskLabel}`
|
|
571
724
|
: ` ${statusRow(taskVerdict(c), taskLabel)}`;
|
|
@@ -588,9 +741,10 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
588
741
|
};
|
|
589
742
|
};
|
|
590
743
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
744
|
+
const namedRunId = positionalRunId(argv);
|
|
591
745
|
// cockpit surface: banner + frame on a TTY (doctor's pattern); pipes get the bare frame
|
|
592
746
|
if (!argv.includes("--watch")) {
|
|
593
|
-
const { content } = renderFrame(cwd, opts.now?.() ?? Date.now());
|
|
747
|
+
const { content } = renderFrame(cwd, opts.now?.() ?? Date.now(), 0, undefined, false, namedRunId);
|
|
594
748
|
return visual() ? BANNER + content : content;
|
|
595
749
|
}
|
|
596
750
|
const eventStream = argv.some((arg) => arg === "--events" || arg === "--jsonl" || arg === "--decision-events");
|
|
@@ -652,7 +806,9 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
652
806
|
let decisionRunId;
|
|
653
807
|
let journalCursor = 0;
|
|
654
808
|
const consumeDecisionEvents = () => {
|
|
655
|
-
|
|
809
|
+
// A named run is followed, never re-resolved: --watch <runId> keeps reporting that run even as
|
|
810
|
+
// newer runs start. Only the no-argument form tracks latest, as before.
|
|
811
|
+
const runId = namedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
656
812
|
if (!runId)
|
|
657
813
|
return [];
|
|
658
814
|
if (decisionRunId !== runId) {
|
|
@@ -709,7 +865,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
709
865
|
}
|
|
710
866
|
}
|
|
711
867
|
else {
|
|
712
|
-
frame = renderFrame(cwd, nowMs, i, workerLiveness);
|
|
868
|
+
frame = renderFrame(cwd, nowMs, i, workerLiveness, true, namedRunId);
|
|
713
869
|
if (tty) {
|
|
714
870
|
updateTitle(frame.hotPhase, nowMs);
|
|
715
871
|
process.stdout.write(`\x1b[2J\x1b[H${BANNER}${frame.content}\n${legend(` watching · refresh ${REFRESH_MS / 1000}s · ^C to quit`)}`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TickmarkrConfig } from "../config/config.js";
|
|
1
2
|
import { type Task } from "../graph/schema.js";
|
|
2
3
|
/**
|
|
3
4
|
* Return human-readable scope-lint lines for plan output (no `!` prefix — plan owns that).
|
|
@@ -28,5 +29,88 @@ export declare function separabilityErrors(tasks: ReadonlyArray<Pick<Task, "id"
|
|
|
28
29
|
* diff at the 130k cap; it took 28 dispatches and never passed review, while T3 (4 items) took 5.
|
|
29
30
|
*/
|
|
30
31
|
export declare function taskBudgetErrors(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>): string[];
|
|
31
|
-
/**
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Max acceptance × files[] surface per task (OVERSEER-RULING-20260731-velocity, R2:
|
|
34
|
+
* `surface = acceptance.length × files.length > 24` fails compile). EVERY criterion counts —
|
|
35
|
+
* a judge item is still a thing one worker must satisfy and one reviewer must verify in the
|
|
36
|
+
* same pass, and counting only test oracles let judge-heavy tasks bypass the bound (the first
|
|
37
|
+
* T6 attempt was rejected for exactly that narrowing). The calibration corpus pins the bound:
|
|
38
|
+
* v1.84's original T1 measures 6×5=30 and fails; v1.79's tasks pass — its widest, T5 at
|
|
39
|
+
* 4×7=28, rides the recorded exception below rather than redefining the metric.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MAX_TASK_SURFACE = 24;
|
|
42
|
+
/**
|
|
43
|
+
* Max goal words per acceptance criterion (v1.84's T1 measured 75). Gameable only by adding
|
|
44
|
+
* criteria — which is the point: every law migrated from goal prose to a criterion moves its
|
|
45
|
+
* enforcement from the 40-minute review round to the 2-minute judge.
|
|
46
|
+
*/
|
|
47
|
+
export declare const MAX_GOAL_WORDS_PER_CRITERION = 60;
|
|
48
|
+
/**
|
|
49
|
+
* A recorded surface exception — an explicit contract amendment, never a silent bypass. Each
|
|
50
|
+
* entry matches exactly ONE historical task shape: task id, acceptance count, and the full
|
|
51
|
+
* files[] list must all be identical, so nothing an author writes later can ride one (matching
|
|
52
|
+
* would require copying a recorded files[] verbatim — an audible spec edit any reviewer sees).
|
|
53
|
+
*/
|
|
54
|
+
export interface SurfaceException {
|
|
55
|
+
/** Task id as authored in the spec the exception was recorded for. */
|
|
56
|
+
id: string;
|
|
57
|
+
/** Exact acceptance-criterion count of the excepted task. */
|
|
58
|
+
acceptance: number;
|
|
59
|
+
/** Exact files[] of the excepted task (order-insensitive; sorted at recording time). */
|
|
60
|
+
files: string[];
|
|
61
|
+
/** Where the amendment is recorded and why it exists. */
|
|
62
|
+
reason: string;
|
|
63
|
+
}
|
|
64
|
+
export declare const SURFACE_CONTRACT_EXCEPTIONS: ReadonlyArray<SurfaceException>;
|
|
65
|
+
/**
|
|
66
|
+
* R2 surface bound: acceptance criteria × files[] patterns, one error per offending task.
|
|
67
|
+
* `exceptions` defaults to the recorded contract amendments; tests pass [] to prove an
|
|
68
|
+
* exception is load-bearing.
|
|
69
|
+
*/
|
|
70
|
+
export declare function surfaceErrors(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, exceptions?: ReadonlyArray<SurfaceException>): string[];
|
|
71
|
+
/** R2 goal-density bound: goal words per acceptance criterion, one error per offending task. */
|
|
72
|
+
export declare function goalDensityErrors(tasks: ReadonlyArray<Pick<Task, "id" | "goal" | "acceptance">>): string[];
|
|
73
|
+
/**
|
|
74
|
+
* R2 symbol ownership: a code identifier named in a judge or test criterion that resolves uniquely
|
|
75
|
+
* to a definition OUTSIDE the task's files[] is unsatisfiable by construction — the v1.84 T1
|
|
76
|
+
* planFrame defect, which burned two review rounds because the enabling symbol lived in another
|
|
77
|
+
* task's file. Zero definition sites (plain-language false positive, external symbol, or a file
|
|
78
|
+
* the task itself creates) and ambiguous tokens (multiple definition sites) stay silent. The index
|
|
79
|
+
* must be COMPLETE: any code path that cannot be read is its own error — the rule fails closed
|
|
80
|
+
* rather than trust a partial scan.
|
|
81
|
+
*/
|
|
82
|
+
export declare function symbolOwnershipErrors(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, repoRoot: string): string[];
|
|
83
|
+
/**
|
|
84
|
+
* The participation half of the config — everything the review-policy rules read. `byShape` rides
|
|
85
|
+
* along because `gates.byShape.<shape>.review: false` is a second, NON-monotone participation switch:
|
|
86
|
+
* review.policy can only raise a task's policy, but a shape override can omit the review gate outright
|
|
87
|
+
* (src/gates/run-gates.ts `enabled`). Path-keyed participation is only true if both are read together.
|
|
88
|
+
*/
|
|
89
|
+
export type ReviewParticipation = TickmarkrConfig["review"] & {
|
|
90
|
+
byShape?: TickmarkrConfig["gates"]["byShape"];
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* R3 review participation (OVERSEER-RULING-20260731-velocity; OBS-186). The compiler assigns
|
|
94
|
+
* `reviewPolicy` from the DECLARED files[] — `judge-only` only when every declared path is provably
|
|
95
|
+
* docs/CHANGELOG/RELEASING/version-mirror leaf work — and the operator's floor may raise it. A task
|
|
96
|
+
* that would SKIP review while touching `review.criticalPaths` is the exact shape OBS-186 caught in
|
|
97
|
+
* the field: the riskiest task in the bundle silently declining the one gate that exists for it. That
|
|
98
|
+
* cannot be a warning, because nothing downstream would stop it — the gate would honour the policy.
|
|
99
|
+
*/
|
|
100
|
+
export declare function reviewParticipationErrors(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "shape">>, review: ReviewParticipation): string[];
|
|
101
|
+
/**
|
|
102
|
+
* Every Task Unit Contract violation in one pass, ready to throw. `repoRoot` backs the
|
|
103
|
+
* symbol-ownership lint and the participation config, and defaults to the invocation directory —
|
|
104
|
+
* correct for the CLI/daemon, which compile from inside the target repo.
|
|
105
|
+
*
|
|
106
|
+
* KNOWN GAP (R2 review, T6/T7 scope): the compile seam in src/compile/index.ts
|
|
107
|
+
* (`enforceTaskUnitContract`) does not thread `compileSource`'s `root` argument into this call, so a
|
|
108
|
+
* PROGRAMMATIC compile whose target repo differs from process.cwd() resolves the wrong root and can
|
|
109
|
+
* miss that repo's own `review.criticalPaths`. Threading it is one line in src/compile/index.ts,
|
|
110
|
+
* outside this task's file scope. Two things bound the exposure meanwhile, both live: the critical
|
|
111
|
+
* set here is the UNION with the shipped defaults, so a wrong root can never lower enforcement below
|
|
112
|
+
* the shipped floor; and the review GATE — which is handed the run's real config — refuses to skip a
|
|
113
|
+
* critical path itself (src/gates/review.ts), so a lint this seam misses costs a late verdict rather
|
|
114
|
+
* than an unreviewed one. The compile lint is the early warning; the gate is the fail-closed backstop.
|
|
115
|
+
*/
|
|
116
|
+
export declare function taskUnitContractErrors(tasks: ReadonlyArray<Pick<Task, "id" | "goal" | "files" | "deps" | "acceptance" | "shape">>, repoRoot?: string, review?: ReviewParticipation): string[];
|