tickmarkr 1.83.0 → 1.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code.d.ts +1 -0
- package/dist/adapters/claude-code.js +57 -1
- package/dist/adapters/fake.js +9 -0
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +21 -0
- package/dist/cli/commands/status.js +160 -30
- package/dist/compile/collateral.d.ts +86 -2
- package/dist/compile/collateral.js +294 -3
- package/dist/config/config.d.ts +62 -0
- package/dist/config/config.js +157 -2
- package/dist/drivers/herdr.d.ts +20 -3
- package/dist/drivers/herdr.js +288 -105
- package/dist/gates/baseline.d.ts +1 -0
- package/dist/gates/baseline.js +91 -13
- package/dist/gates/review.d.ts +7 -0
- package/dist/gates/review.js +99 -6
- package/dist/gates/run-gates.d.ts +9 -0
- package/dist/gates/run-gates.js +285 -41
- package/dist/run/daemon.d.ts +48 -2
- package/dist/run/daemon.js +1417 -315
- package/dist/run/journal.d.ts +56 -3
- package/dist/run/journal.js +275 -1
- package/dist/run/stall.d.ts +35 -1
- package/dist/run/stall.js +118 -8
- package/dist/tui/cockpit/components.d.ts +30 -1
- package/dist/tui/cockpit/components.js +19 -3
- package/dist/tui/cockpit/derive.d.ts +29 -2
- package/dist/tui/cockpit/derive.js +219 -23
- package/dist/tui/cockpit/layout.d.ts +75 -6
- package/dist/tui/cockpit/layout.js +97 -19
- package/dist/tui/cockpit/live.d.ts +14 -1
- package/dist/tui/cockpit/live.js +223 -29
- package/dist/tui/cockpit/pointer.d.ts +261 -0
- package/dist/tui/cockpit/pointer.js +610 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +36 -5
- package/dist/tui/cockpit/run-cockpit.js +270 -51
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ export declare const CLAUDE_ALIAS_IDENTITY_STAMPS: {
|
|
|
8
8
|
export type ClaudeAlias = keyof typeof CLAUDE_ALIAS_IDENTITY_STAMPS;
|
|
9
9
|
export type ClaudeAliasIdentityProbe = (cwd: string, alias: ClaudeAlias) => string | undefined;
|
|
10
10
|
export declare const CLAUDE_TRUST_DIALOG: TrustDialog;
|
|
11
|
+
export declare const CLAUDE_INPUT_BOX: import("./types.js").InputBox;
|
|
11
12
|
export declare function claudeSlug(real: string): string;
|
|
12
13
|
export declare function readClaudeAliasIdentity(cwd: string, alias: ClaudeAlias): string | undefined;
|
|
13
14
|
export declare function probeClaudeAliasIdentity(cwd: string, alias: ClaudeAlias): string | undefined;
|
|
@@ -3,7 +3,7 @@ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { parseWorkerResult } from "./prompt.js";
|
|
6
|
-
import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
|
|
6
|
+
import { channelsFromConfig, declareInputBox, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
|
|
7
7
|
// SPEND-01/SPEND-11: claude writes a per-session JSONL to ~/.claude/projects/<slug>/ where slug is the
|
|
8
8
|
// realpath'd cwd with every non-alphanumeric char replaced by "-" (verified 114/114 — 36-DIAGNOSIS.md).
|
|
9
9
|
// The old `/`-only formula missed the "." in `.tickmarkr/worktrees/…` — ENOENT on every worktree dispatch.
|
|
@@ -40,6 +40,61 @@ export const CLAUDE_TRUST_DIALOG = {
|
|
|
40
40
|
fingerprint: "Quick safety check: Is this a project you created or one you trust?",
|
|
41
41
|
key: "Enter",
|
|
42
42
|
};
|
|
43
|
+
// OBS-201/262 nudge deliverability: claude-code is the ONLY member of NUDGEABLE_ADAPTERS, and the
|
|
44
|
+
// driver's deliverTyped pincer (readiness stable-frame → type → read-back → verified submit) needs a
|
|
45
|
+
// declared input box for any delivery whose command is not the adapter's own launch line — which a
|
|
46
|
+
// nudge never is. Without this declaration `requireInputBox` was false, readiness fell back to
|
|
47
|
+
// "the pane no longer echoes the command", and submission fell through to the positional fallback:
|
|
48
|
+
// the rescue that OBS-262 widened the gate for could not be verifiably delivered to the one adapter
|
|
49
|
+
// allowed to receive it.
|
|
50
|
+
//
|
|
51
|
+
// CAPTURED, not guessed (claude 2.1.220, tmux 120x30, 2026-08-03 — the state-to-PIXELS law):
|
|
52
|
+
// ──────────────────────────────────────────── ← full-width rule
|
|
53
|
+
// ❯ You are still assigned this task… ← caret, NBSP, then the typed turn
|
|
54
|
+
// ──────────────────────────────────────────── ← full-width rule
|
|
55
|
+
// Two findings the kimi-shaped guess would have got wrong, and both are fatal on their own:
|
|
56
|
+
// 1. claude's editor is NOT a bordered ╭─╮ box — it is one row FENCED BY TWO RULES, so a
|
|
57
|
+
// `│ > ` fingerprint or a border-adjacency walk never fires.
|
|
58
|
+
// 2. the caret is padded with U+00A0, not a space. A `"❯ "` fingerprint with an ASCII space
|
|
59
|
+
// matches nothing, ever — the OBS-152 failure mode exactly (six versions of "kimi is broken"
|
|
60
|
+
// were all anchored matchers meeting a TUI that renders differently than assumed).
|
|
61
|
+
// `match` means THE EDITOR IS PAINTED and is deliberately true for an empty editor (readiness);
|
|
62
|
+
// `emptyMatch` is the stricter "painted AND carrying nothing", which is what proves a submit
|
|
63
|
+
// registered. Callers deciding submission must test emptyMatch first — see submissionRegistered.
|
|
64
|
+
const CLAUDE_ANSI_SGR_RE = /\u001B\[[0-9;]*m/g;
|
|
65
|
+
const CLAUDE_RULE_RE = /^─{8,}$/;
|
|
66
|
+
const CLAUDE_CARET_RE = /^❯\u00A0/;
|
|
67
|
+
const CLAUDE_CARET_EMPTY_RE = /^❯\u00A0\s*$/;
|
|
68
|
+
// A wrapped or multi-line turn grows the editor downward before the closing rule.
|
|
69
|
+
// ponytail: a fixed window, not a parser — raise it if a real capture ever shows a taller editor.
|
|
70
|
+
const CLAUDE_MAX_EDITOR_ROWS = 8;
|
|
71
|
+
function matchesClaudeEditor(paneText, empty) {
|
|
72
|
+
// Trim ASCII margins ONLY: String.trim() eats U+00A0, which would erase the very byte that
|
|
73
|
+
// distinguishes claude's caret padding from an ordinary prompt line.
|
|
74
|
+
const lines = paneText.replace(CLAUDE_ANSI_SGR_RE, "").split("\n").map((l) => l.replace(/^[ \t]+|[ \t]+$/g, ""));
|
|
75
|
+
const caret = empty ? CLAUDE_CARET_EMPTY_RE : CLAUDE_CARET_RE;
|
|
76
|
+
return lines.some((line, i) => {
|
|
77
|
+
if (!caret.test(line))
|
|
78
|
+
return false;
|
|
79
|
+
if (i === 0 || !CLAUDE_RULE_RE.test(lines[i - 1]))
|
|
80
|
+
return false;
|
|
81
|
+
for (let below = i + 1; below < lines.length && below <= i + CLAUDE_MAX_EDITOR_ROWS; below++) {
|
|
82
|
+
if (CLAUDE_RULE_RE.test(lines[below]))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export const CLAUDE_INPUT_BOX = declareInputBox("claude-code", {
|
|
89
|
+
fingerprint: "❯\u00A0",
|
|
90
|
+
match: (paneText) => matchesClaudeEditor(paneText, false),
|
|
91
|
+
emptyMatch: (paneText) => matchesClaudeEditor(paneText, true),
|
|
92
|
+
// The launch line settles on a clean shell line; the editor paints after it. Both interactive
|
|
93
|
+
// entry points count — a resumed session re-enters the same TUI (OBS-142's launch distinction).
|
|
94
|
+
launchCommand: (command) => command.startsWith("claude --model ") || command.startsWith("claude -r "),
|
|
95
|
+
// The 2026-08-03 capture reached a painted editor ~10s after the trust answer on a warm install.
|
|
96
|
+
readinessTimeoutMs: 30_000,
|
|
97
|
+
});
|
|
43
98
|
export function claudeSlug(real) {
|
|
44
99
|
return real.replace(/[^A-Za-z0-9]/g, "-");
|
|
45
100
|
}
|
|
@@ -168,6 +223,7 @@ export const claudeCode = {
|
|
|
168
223
|
// the daemon safely answers only the exact adapter-declared dialog once per slot.
|
|
169
224
|
interactiveCommand: (promptFile, model) => `claude --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
|
|
170
225
|
trustDialog: CLAUDE_TRUST_DIALOG,
|
|
226
|
+
inputBox: CLAUDE_INPUT_BOX,
|
|
171
227
|
resumeCommand: (sessionId, promptFile, model) => `claude -r ${shq(sessionId)} --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
|
|
172
228
|
invoke(task, _cwd, a, ctx) {
|
|
173
229
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
package/dist/adapters/fake.js
CHANGED
|
@@ -35,14 +35,23 @@ export class FakeAdapter {
|
|
|
35
35
|
// contains TICKMARKR-JUDGE — a review/consult prompt never touches it (research Pitfall 3).
|
|
36
36
|
let prompt = "";
|
|
37
37
|
let isJudge = false;
|
|
38
|
+
// v1.85 T4: the role THIS prompt is, when it can be told. The three scratch files are shared per
|
|
39
|
+
// script path, so a build that writes a role it is not can clobber a concurrently-dispatched
|
|
40
|
+
// sibling's file between that sibling's build and its `cat` — which is exactly what a round that
|
|
41
|
+
// launches judge and review together does. Writing only the matching role removes the race: the
|
|
42
|
+
// other roles' `grep` guards below can never fire for this prompt, so their files are never read.
|
|
43
|
+
let role;
|
|
38
44
|
try {
|
|
39
45
|
prompt = readFileSync(promptFile, "utf8");
|
|
40
46
|
isJudge = /TICKMARKR-(?:JUDGE|SCOPE)/.test(prompt);
|
|
47
|
+
role = isJudge ? "judge" : /TICKMARKR-REVIEW/.test(prompt) ? "review" : /TICKMARKR-CONSULT/.test(prompt) ? "consult" : undefined;
|
|
41
48
|
}
|
|
42
49
|
catch {
|
|
43
50
|
// unreadable promptFile: can't detect role; serve static values (legacy headless-call behavior)
|
|
44
51
|
}
|
|
45
52
|
const serve = (key) => {
|
|
53
|
+
if (role !== undefined && key !== role)
|
|
54
|
+
return join(dirname(this.scriptPath), `${key}.json`);
|
|
46
55
|
let val = this.script[key];
|
|
47
56
|
if (key === "judge" && isJudge && Array.isArray(val)) {
|
|
48
57
|
// array served sequentially per judge prompt, clamped to last (steps[min(n,len-1)] idiom)
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export interface InputBox {
|
|
|
86
86
|
fingerprint: string;
|
|
87
87
|
match?(paneText: string): boolean;
|
|
88
88
|
emptyMatch?(paneText: string): boolean;
|
|
89
|
+
occupiedMatch?(paneText: string): boolean;
|
|
89
90
|
launchCommand?(command: string): boolean;
|
|
90
91
|
readinessTimeoutMs?: number;
|
|
91
92
|
}
|
|
@@ -93,6 +94,8 @@ export declare function declareInputBox(adapterId: string, inputBox: InputBox):
|
|
|
93
94
|
export declare function declaredInputBoxForWorkerName(workerName: string): InputBox | undefined;
|
|
94
95
|
export declare function matchesInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
95
96
|
export declare function matchesEmptyInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
97
|
+
export declare function matchesOccupiedInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
98
|
+
export declare function missingInputStateDeclarations(inputBox: InputBox | undefined): string[];
|
|
96
99
|
export interface WorkerAdapter {
|
|
97
100
|
id: string;
|
|
98
101
|
vendor: string;
|
package/dist/adapters/types.js
CHANGED
|
@@ -45,6 +45,27 @@ export function matchesInputBox(paneText, inputBox) {
|
|
|
45
45
|
export function matchesEmptyInputBox(paneText, inputBox) {
|
|
46
46
|
return inputBox.emptyMatch?.(paneText) === true;
|
|
47
47
|
}
|
|
48
|
+
// The OCCUPIED state: the adapter's own pin when it has one, else painted-and-not-empty, which two
|
|
49
|
+
// adapter declarations decide between them. Either way the answer comes from the adapter, never from
|
|
50
|
+
// where the prompt happens to sit in the transcript.
|
|
51
|
+
export function matchesOccupiedInputBox(paneText, inputBox) {
|
|
52
|
+
if (inputBox.occupiedMatch)
|
|
53
|
+
return inputBox.occupiedMatch(paneText) === true;
|
|
54
|
+
return matchesInputBox(paneText, inputBox) && !matchesEmptyInputBox(paneText, inputBox);
|
|
55
|
+
}
|
|
56
|
+
// v1.85 T5: which input-state declarations a typed delivery needs and this adapter has not made.
|
|
57
|
+
// Empty is always required (it is the only positive submission evidence); occupied needs either its
|
|
58
|
+
// own matcher or the `match` that derives it. A non-empty result is a fail-closed refusal, named.
|
|
59
|
+
export function missingInputStateDeclarations(inputBox) {
|
|
60
|
+
if (!inputBox)
|
|
61
|
+
return ["inputBox"];
|
|
62
|
+
const missing = [];
|
|
63
|
+
if (!inputBox.emptyMatch)
|
|
64
|
+
missing.push("emptyMatch");
|
|
65
|
+
if (!inputBox.occupiedMatch && !inputBox.match)
|
|
66
|
+
missing.push("occupiedMatch");
|
|
67
|
+
return missing;
|
|
68
|
+
}
|
|
48
69
|
export function channelsFromConfig(adapterId, cfg) {
|
|
49
70
|
const e = cfg.tiers[adapterId];
|
|
50
71
|
if (!e)
|
|
@@ -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,52 @@ 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";
|
|
9
12
|
// ponytail: fixed 2s refresh; promote to config.visibility.* only when an operator asks.
|
|
10
13
|
const REFRESH_MS = 2000;
|
|
11
14
|
const NOT_COMPARABLE_NOTICE = "graph recompiled since this run — task states not comparable; resume with `--graph-changed` to audit this recompile";
|
|
12
15
|
const PRIOR_GRAPH_MARKER = "prior graph";
|
|
16
|
+
// One compact host-zone label per screen. Each event below is still converted from its own
|
|
17
|
+
// complete UTC instant, so the zone's historical rules — including a transition within one frame
|
|
18
|
+
// — stay intact rather than being rendered through this label's single sampled offset.
|
|
19
|
+
const localZoneLabel = (reference = new Date()) => {
|
|
20
|
+
const offset = -reference.getTimezoneOffset();
|
|
21
|
+
if (offset === 0)
|
|
22
|
+
return "UTC";
|
|
23
|
+
const sign = offset < 0 ? "-" : "+";
|
|
24
|
+
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0");
|
|
25
|
+
const minutes = Math.abs(offset) % 60;
|
|
26
|
+
return minutes === 0
|
|
27
|
+
? `${sign}${hours}`
|
|
28
|
+
: `${sign}${hours}:${String(minutes).padStart(2, "0")}`;
|
|
29
|
+
};
|
|
30
|
+
const localClockText = (content, reference = new Date()) => {
|
|
31
|
+
return content.replace(/\b([01]\d|2[0-3]):([0-5]\d):([0-5]\d)\b/gu, (_whole, h, m, s) => {
|
|
32
|
+
const instant = new Date(Date.UTC(reference.getUTCFullYear(), reference.getUTCMonth(), reference.getUTCDate(), Number(h), Number(m), Number(s)));
|
|
33
|
+
return [instant.getHours(), instant.getMinutes(), instant.getSeconds()]
|
|
34
|
+
.map((part) => String(part).padStart(2, "0"))
|
|
35
|
+
.join(":");
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
/** The complete journal instant behind a folded clock phrase, not a screen-wide offset sample. */
|
|
39
|
+
const taskClockReference = (events, taskId, content, fallback) => {
|
|
40
|
+
const utcClock = content.match(/\b(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\b/u)?.[0];
|
|
41
|
+
if (utcClock === undefined)
|
|
42
|
+
return fallback;
|
|
43
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
const event = events[index];
|
|
45
|
+
if (event.taskId !== taskId || event.ts.slice(11, 19) !== utcClock)
|
|
46
|
+
continue;
|
|
47
|
+
const instant = new Date(event.ts);
|
|
48
|
+
if (Number.isFinite(instant.getTime()))
|
|
49
|
+
return instant;
|
|
50
|
+
}
|
|
51
|
+
return fallback;
|
|
52
|
+
};
|
|
53
|
+
// Every attempt number a cell phrase carries is the engagement's own ladder (foldActivity counts
|
|
54
|
+
// dispatch labels from 1). Name that ruler wherever a bare one appears, not only in the one phrase
|
|
55
|
+
// shipping today — a phrase added upstream must not reach the operator unruled.
|
|
56
|
+
const labelAttemptRuler = (text) => text.replace(/(?<!engagement |run )\battempt (?=\d)/gu, "engagement attempt ");
|
|
13
57
|
// The timer must keep the process ALIVE: an unref'd timer here let the event loop drain after the
|
|
14
58
|
// first frame, so a live `--watch` printed once and exited 0 (OBS-11). Never unref this.
|
|
15
59
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -196,6 +240,25 @@ export const taskBox = (status) => {
|
|
|
196
240
|
return "[!]";
|
|
197
241
|
return "[ ]";
|
|
198
242
|
};
|
|
243
|
+
const graphTaskStatus = (state, fallback) => {
|
|
244
|
+
if (state === "completed" || state === "done")
|
|
245
|
+
return "done";
|
|
246
|
+
if (state === "interrupted")
|
|
247
|
+
return "failed";
|
|
248
|
+
return state ?? fallback;
|
|
249
|
+
};
|
|
250
|
+
const surfaceStatusWord = (state) => {
|
|
251
|
+
if (state === "human")
|
|
252
|
+
return "parked";
|
|
253
|
+
return state;
|
|
254
|
+
};
|
|
255
|
+
const surfaceTaskBox = (state, merged) => {
|
|
256
|
+
if (merged)
|
|
257
|
+
return "[x]";
|
|
258
|
+
if (state === "failed" || state === "human")
|
|
259
|
+
return "[!]";
|
|
260
|
+
return "[ ]";
|
|
261
|
+
};
|
|
199
262
|
export const gateBox = (state, unicode) => {
|
|
200
263
|
if (unicode) {
|
|
201
264
|
// shared glyph vocabulary: pass/fail verdicts, dash for skip, dim circle for not-yet-run
|
|
@@ -366,7 +429,7 @@ const tipVerifyText = (phase) => {
|
|
|
366
429
|
return "tip-verify: pending";
|
|
367
430
|
const gates = phase.gates.length ? phase.gates.join(", ") : "gate unknown";
|
|
368
431
|
const failed = `tip-verify: FAILED (${gates})`;
|
|
369
|
-
const reverify = phase.state === "re-verifying" ? ` → re-verifying (attempt ${phase.attempt})` : "";
|
|
432
|
+
const reverify = phase.state === "re-verifying" ? ` → re-verifying (run attempt ${phase.attempt})` : "";
|
|
370
433
|
const fingerprints = phase.fingerprints === undefined
|
|
371
434
|
? ""
|
|
372
435
|
: ` · ${phase.fingerprints} fingerprint${phase.fingerprints === 1 ? "" : "s"}`;
|
|
@@ -412,11 +475,24 @@ const statusEngagement = (events, loadedHash) => {
|
|
|
412
475
|
}
|
|
413
476
|
return { comparable: baseline.comparable };
|
|
414
477
|
};
|
|
415
|
-
|
|
478
|
+
// The journal's own reader rule (src/run/journal.ts readJsonl), applied to bytes already in hand:
|
|
479
|
+
// skip blanks, drop a line that will not parse (a torn trailing write after a crash), keep the rest.
|
|
480
|
+
const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
|
|
481
|
+
if (!line.trim())
|
|
482
|
+
return [];
|
|
483
|
+
try {
|
|
484
|
+
return [JSON.parse(line)];
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness = new Map(), journalRowsOnly = false) => {
|
|
416
491
|
const g = loadGraph(cwd);
|
|
417
492
|
const runId = Journal.latestRunId(cwd, { withJournal: true });
|
|
418
493
|
const assignments = new Map();
|
|
419
494
|
let replayed = null;
|
|
495
|
+
let cockpit;
|
|
420
496
|
let events = [];
|
|
421
497
|
const contexts = new Map();
|
|
422
498
|
let comparable = false;
|
|
@@ -424,7 +500,19 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
424
500
|
let supersededBy; // v1.53 T5: this run is dead — a newer run replaced it
|
|
425
501
|
if (runId) {
|
|
426
502
|
const j = Journal.open(cwd, runId);
|
|
427
|
-
|
|
503
|
+
// ONE byte snapshot per frame. The cockpit fold and every decoration below — foldActivity,
|
|
504
|
+
// livePhases, tipVerifyPhase, gateSnapshot, the failure and liveness readings — fold THESE
|
|
505
|
+
// bytes and nothing else, so a line the daemon appends mid-frame can never pair newer task
|
|
506
|
+
// rows against older activity, phases, gates or tip state. `Journal.open` refuses a run
|
|
507
|
+
// without a journal, so the file is there to read.
|
|
508
|
+
const raw = readFileSync(join(j.dir, "journal.jsonl"), "utf8");
|
|
509
|
+
events = parseJournalSnapshot(raw);
|
|
510
|
+
// The daemon creates journal.jsonl with lock-reclaimed before run-start and a reader may also
|
|
511
|
+
// catch an empty/torn first write. Those snapshots own no task fold yet; once run-start exists,
|
|
512
|
+
// however, a fold failure is unexpected and must surface instead of becoming a false empty run.
|
|
513
|
+
if (journalRowsOnly && events.some((event) => event.event === "run-start")) {
|
|
514
|
+
cockpit = deriveRunCockpitData({ fileName: `${runId}.journal.jsonl`, raw }, "status", { graph: g });
|
|
515
|
+
}
|
|
428
516
|
const sup = [...events].reverse().find((e) => e.event === "superseded" && typeof e.data.by === "string");
|
|
429
517
|
supersededBy = sup?.data.by;
|
|
430
518
|
// The resume comparator is the fail-closed baseline; a matching graph-rehash is the daemon's
|
|
@@ -433,12 +521,14 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
433
521
|
comparable = engagement.comparable;
|
|
434
522
|
rehashAt = engagement.rehashAt;
|
|
435
523
|
if (comparable) {
|
|
436
|
-
|
|
524
|
+
if (!journalRowsOnly)
|
|
525
|
+
replayed = j.replayStatuses();
|
|
437
526
|
for (const e of events) {
|
|
438
|
-
if (e.event === "task-dispatch" && e.taskId) {
|
|
527
|
+
if (!journalRowsOnly && e.event === "task-dispatch" && e.taskId) {
|
|
439
528
|
const a = e.data.assignment;
|
|
440
|
-
if (typeof a.adapter === "string" && typeof a.model === "string")
|
|
529
|
+
if (typeof a.adapter === "string" && typeof a.model === "string") {
|
|
441
530
|
assignments.set(e.taskId, `${a.adapter}:${a.model}`);
|
|
531
|
+
}
|
|
442
532
|
}
|
|
443
533
|
if (e.event === "context-sample" && e.taskId && typeof e.data.tokens === "number" && Number.isFinite(e.data.tokens)) {
|
|
444
534
|
contexts.set(e.taskId, e.data.tokens); // last write wins
|
|
@@ -446,7 +536,34 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
446
536
|
}
|
|
447
537
|
}
|
|
448
538
|
}
|
|
449
|
-
const
|
|
539
|
+
const taskRows = new Map(cockpit?.taskRows.map((row) => [row.taskId, row]) ?? []);
|
|
540
|
+
const clockFallback = new Date(now);
|
|
541
|
+
const recordedClock = new Date(events.at(-1)?.ts ?? now);
|
|
542
|
+
const zoneReference = Number.isFinite(recordedClock.getTime()) ? recordedClock : clockFallback;
|
|
543
|
+
const effective = {
|
|
544
|
+
...g,
|
|
545
|
+
tasks: g.tasks.map((task) => ({
|
|
546
|
+
...task,
|
|
547
|
+
status: comparable
|
|
548
|
+
? journalRowsOnly
|
|
549
|
+
? graphTaskStatus(taskRows.get(task.id)?.state, task.status)
|
|
550
|
+
: replayed?.get(task.id) ?? task.status
|
|
551
|
+
: task.status,
|
|
552
|
+
})),
|
|
553
|
+
};
|
|
554
|
+
const renderedTasks = journalRowsOnly
|
|
555
|
+
? cockpit !== undefined && comparable
|
|
556
|
+
? g.tasks.filter((task) => {
|
|
557
|
+
const row = taskRows.get(task.id);
|
|
558
|
+
return row !== undefined && (row.state !== undefined
|
|
559
|
+
|| row.lastEventTime !== undefined
|
|
560
|
+
|| row.attempts !== undefined
|
|
561
|
+
|| row.actor !== undefined
|
|
562
|
+
|| row.merged === true
|
|
563
|
+
|| row.parkKind !== undefined);
|
|
564
|
+
})
|
|
565
|
+
: []
|
|
566
|
+
: g.tasks;
|
|
450
567
|
const starved = new Set(blockedTasks(effective).map((t) => t.id));
|
|
451
568
|
// OBS-104: ONE activity fold feeds both surfaces — never re-derived here. Comparable events only
|
|
452
569
|
// (a recompiled graph's journal must not animate the wrong tasks); with no or stale journal the
|
|
@@ -455,7 +572,10 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
455
572
|
const unicode = visual();
|
|
456
573
|
const divider = unicode ? " · " : " / ";
|
|
457
574
|
const width = process.stdout.columns ?? 120;
|
|
458
|
-
const done =
|
|
575
|
+
const done = journalRowsOnly
|
|
576
|
+
? renderedTasks.filter((task) => graphTaskStatus(taskRows.get(task.id)?.state, task.status) === "done").length
|
|
577
|
+
: effective.tasks.filter((task) => task.status === "done").length;
|
|
578
|
+
const total = g.tasks.length;
|
|
459
579
|
const ended = comparable && runHasEnded(events);
|
|
460
580
|
const tipPhase = comparable ? tipVerifyPhase(events) : undefined;
|
|
461
581
|
const taskIds = new Set(g.tasks.map((task) => task.id));
|
|
@@ -464,8 +584,10 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
464
584
|
if (!taskIds.has(taskId))
|
|
465
585
|
phases.delete(taskId);
|
|
466
586
|
const hotPhase = [...phases.values()].sort((a, b) => a.order - b.order).at(-1);
|
|
467
|
-
const cells =
|
|
468
|
-
const
|
|
587
|
+
const cells = renderedTasks.map((t) => {
|
|
588
|
+
const folded = journalRowsOnly && comparable ? taskRows.get(t.id) : undefined;
|
|
589
|
+
const st = folded?.state ?? replayed?.get(t.id) ?? t.status;
|
|
590
|
+
const merged = journalRowsOnly ? folded?.merged === true : st === "done" || st === "completed";
|
|
469
591
|
const failureKind = comparable ? recordedTaskFailureKind(events, t.id) : undefined;
|
|
470
592
|
const terminal = st === "failed" || st === "human";
|
|
471
593
|
// Unknown legacy task-failed events stay red. Typed availability noise is warn-tier only while
|
|
@@ -475,28 +597,34 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
475
597
|
|| (st === "failed" && failureKind === undefined));
|
|
476
598
|
const livePhase = phases.get(t.id);
|
|
477
599
|
const isStarved = !livePhase && starved.has(t.id);
|
|
478
|
-
const
|
|
600
|
+
const rawPhrase = livePhase ? phaseDetail(livePhase, now, workerLiveness.get(t.id)) : isStarved ? undefined : activity.cells.get(t.id);
|
|
601
|
+
const phrase = rawPhrase === undefined
|
|
602
|
+
? undefined
|
|
603
|
+
: journalRowsOnly
|
|
604
|
+
? labelAttemptRuler(localClockText(rawPhrase, taskClockReference(events, t.id, rawPhrase, clockFallback)))
|
|
605
|
+
: rawPhrase;
|
|
479
606
|
const label = isStarved ? " starved" : phrase ? ` ${phrase}` : "";
|
|
480
|
-
const channel = assignments.get(t.id) ?? "-";
|
|
607
|
+
const channel = journalRowsOnly ? folded?.actor ?? "-" : assignments.get(t.id) ?? "-";
|
|
481
608
|
const ctx = contexts.get(t.id);
|
|
482
609
|
const assignCol = ctx !== undefined ? `${channel}${divider}ctx ${ctx}` : channel;
|
|
483
610
|
const gates = comparable
|
|
484
611
|
? gateSnapshot(t, events, rehashAt)
|
|
485
612
|
: { states: defaultGateStates(t), priorGraph: false };
|
|
486
|
-
return { t, st, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, ...gates };
|
|
613
|
+
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, ...gates };
|
|
487
614
|
});
|
|
488
615
|
if (!unicode) {
|
|
489
616
|
// machine/CI surface — journals without phase-start stay byte-identical; new phase-aware frames
|
|
490
617
|
// use an ASCII spinner so pipes never receive terminal-only braille/ANSI.
|
|
491
|
-
const rows = cells.map(({ t, st, label, assignCol, livePhase, states, priorGraph }) => {
|
|
618
|
+
const rows = cells.map(({ t, st, merged, label, assignCol, livePhase, states, priorGraph }) => {
|
|
492
619
|
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" :
|
|
620
|
+
const prefix = livePhase ? ` ${ASCII_SPINNER[animationFrame % ASCII_SPINNER.length]} ${t.id} ` : ` ${surfaceTaskBox(st, merged)} ${t.id} `;
|
|
621
|
+
const suffix = ` ${chain}${priorGraph ? ` ${PRIOR_GRAPH_MARKER}` : ""} ${livePhase ? "running" : surfaceStatusWord(st)}${label} ${assignCol}`;
|
|
495
622
|
return `${prefix}${shortGoal(t.goal, Math.max(0, width - prefix.length - suffix.length))}${suffix}`;
|
|
496
623
|
});
|
|
624
|
+
const zone = journalRowsOnly ? `${divider}zone ${localZoneLabel(zoneReference)}` : "";
|
|
497
625
|
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}/${
|
|
626
|
+
? `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`}`
|
|
627
|
+
: `tickmarkr status${zone}${divider}no runs yet${divider}${done}/${total} done`;
|
|
500
628
|
const legendLine = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
501
629
|
return {
|
|
502
630
|
content: [header, legendLine, ...rows].join("\n"),
|
|
@@ -512,7 +640,7 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
512
640
|
const dot = dim(" · ");
|
|
513
641
|
const anyFailed = cells.some((c) => c.redTier);
|
|
514
642
|
const gaugeCells = 10;
|
|
515
|
-
const fill =
|
|
643
|
+
const fill = total ? Math.round((done / total) * gaugeCells) : 0;
|
|
516
644
|
const tipFailed = tipPhase?.state === "failed";
|
|
517
645
|
const progressTone = anyFailed || tipFailed ? fail : tipPhase ? warn : ok;
|
|
518
646
|
const gauge = (fill ? progressTone("█".repeat(fill)) : "") + (fill < gaugeCells ? dim("░".repeat(gaugeCells - fill)) : "");
|
|
@@ -522,16 +650,17 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
522
650
|
.replace(/\balive\b/, ok("alive"))
|
|
523
651
|
.replaceAll(" · ", dot);
|
|
524
652
|
const tally = tipPhase
|
|
525
|
-
? `${progressTone(`${done}/${
|
|
526
|
-
: `${done}/${
|
|
653
|
+
? `${progressTone(`${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
|
|
654
|
+
: `${done}/${total} done`;
|
|
527
655
|
const tipStatus = tipPhase
|
|
528
656
|
? `${tipFailed ? fail(tipVerifyText(tipPhase)) : warn(tipVerifyText(tipPhase))}${dot}`
|
|
529
657
|
: "";
|
|
530
|
-
const
|
|
658
|
+
const zone = journalRowsOnly ? `zone ${localZoneLabel(zoneReference)}${dot}` : "";
|
|
659
|
+
const header = ` ${title(runId ? `run ${runId}` : "tickmarkr")}${dot}${zone}` +
|
|
531
660
|
(runId
|
|
532
661
|
? `${supersededBy ? `${warn(`superseded by ${supersededBy}`)}${dot}` : ""}${!comparable ? `${warn(NOT_COMPARABLE_NOTICE)}${dot}` : ""}${live}${dot}${tipStatus}`
|
|
533
662
|
: `no runs yet${dot}`) +
|
|
534
|
-
`${gauge} ${tipPhase ? tally : done ===
|
|
663
|
+
`${gauge} ${tipPhase ? tally : done === total && total > 0 ? ok(tally) : tally}`;
|
|
535
664
|
const hr = rule(Math.min(width, 100));
|
|
536
665
|
// OBS-104 run-level now line: names the most recent journal event. TTY frame only — the non-TTY
|
|
537
666
|
// machine surface is byte-pinned (status-brand golden) and must not drift. Rendered BELOW the task
|
|
@@ -542,25 +671,26 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
542
671
|
// id, goal at full width, and only SHORT status words. Line 2 carries the machinery, dim and
|
|
543
672
|
// aligned under the goal: gate chain, live activity phrase (or channel), ctx. Long channel names
|
|
544
673
|
// and activity phrases live on line 2 only, so they can never squeeze the goal or wrap line 1.
|
|
545
|
-
const taskVerdict = (c) => c.
|
|
546
|
-
const statusWord = (c) => c.livePhase ? "running" : c.redTier ? "failed" : c.st === "failed" ? "warn" :
|
|
674
|
+
const taskVerdict = (c) => c.merged ? "pass" : c.redTier ? "fail" : c.st === "failed" || c.st === "human" ? "warn" : "neutral";
|
|
675
|
+
const statusWord = (c) => c.livePhase ? "running" : c.redTier ? "failed" : c.st === "failed" ? "warn" : surfaceStatusWord(c.st);
|
|
547
676
|
const idW = Math.max(...cells.map((c) => c.t.id.length), 2);
|
|
548
677
|
// plain-text status suffixes for width math only — starved / failed gate names / approval hint
|
|
549
|
-
const suffixPlain = (c) => (c.isStarved ? " · starved" : "") + failedSuffix(c.states)
|
|
678
|
+
const suffixPlain = (c) => (c.isStarved ? " · starved" : "") + failedSuffix(c.states)
|
|
679
|
+
+ humanGateSuffix(c.t, graphTaskStatus(c.st, c.t.status), c.states);
|
|
550
680
|
const stW = Math.max(...cells.map((c) => statusWord(c).length + suffixPlain(c).length));
|
|
551
681
|
const avail = Math.max(8, width - (5 + idW) - 2 - stW);
|
|
552
682
|
const goals = cells.map((c) => shortGoal(c.t.goal, avail));
|
|
553
683
|
const goalW = Math.max(8, ...goals.map((s) => s.length));
|
|
554
684
|
const indent = " ".repeat(idW + 5); // line 2 starts under the goal column
|
|
555
685
|
const rows = cells.map((c, i) => {
|
|
556
|
-
const { t, st, failureKind, redTier, states, priorGraph, isStarved, phrase, channel, ctx, livePhase } = c;
|
|
686
|
+
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, channel, ctx, livePhase } = c;
|
|
557
687
|
const word = statusWord(c);
|
|
558
688
|
const staleWorker = livePhase?.phase === "worker"
|
|
559
689
|
&& workerOutputAge(livePhase, now, workerLiveness.get(t.id)) >= 60_000;
|
|
560
|
-
const stWord = staleWorker ? warn(word) :
|
|
690
|
+
const stWord = staleWorker ? warn(word) : merged ? ok(word) : redTier ? fail(word) : st === "failed" || st === "human" ? warn(word) : word;
|
|
561
691
|
// a fail names its gate in words right here — the one moment gate identity is needed on a row
|
|
562
692
|
const f = failedGates(states);
|
|
563
|
-
const human = humanGateSuffix(t, st, states);
|
|
693
|
+
const human = humanGateSuffix(t, graphTaskStatus(st, t.status), states);
|
|
564
694
|
const statusCell = stWord +
|
|
565
695
|
(isStarved ? dot + fail("starved") : "") +
|
|
566
696
|
(f.length ? dot + fail(f.join(", ")) : "") +
|
|
@@ -709,7 +839,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
709
839
|
}
|
|
710
840
|
}
|
|
711
841
|
else {
|
|
712
|
-
frame = renderFrame(cwd, nowMs, i, workerLiveness);
|
|
842
|
+
frame = renderFrame(cwd, nowMs, i, workerLiveness, true);
|
|
713
843
|
if (tty) {
|
|
714
844
|
updateTitle(frame.hotPhase, nowMs);
|
|
715
845
|
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[];
|