tickmarkr 2.6.1 → 2.6.2
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 +14 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +1 -0
- package/dist/adapters/prompt.js +14 -6
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +12 -4
- package/dist/adapters/types.js +6 -0
- package/dist/cli/commands/approve.d.ts +5 -1
- package/dist/cli/commands/approve.js +66 -23
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +45 -7
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +24 -19
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +9 -2
- package/dist/config/config.d.ts +20 -0
- package/dist/config/config.js +47 -8
- package/dist/config/fleet-overlay.d.ts +1 -0
- package/dist/config/fleet-overlay.js +56 -0
- package/dist/drivers/orca.d.ts +26 -1
- package/dist/drivers/orca.js +193 -60
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +2 -1
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +12 -1
- package/dist/gates/baseline.js +11 -4
- package/dist/gates/llm.d.ts +5 -4
- package/dist/gates/llm.js +13 -13
- package/dist/gates/review.d.ts +8 -0
- package/dist/gates/review.js +40 -4
- package/dist/gates/run-gates.d.ts +2 -1
- package/dist/gates/run-gates.js +27 -13
- package/dist/gates/test-manifest.d.ts +3 -1
- package/dist/gates/test-manifest.js +9 -2
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/router.js +30 -15
- package/dist/run/consult.d.ts +13 -1
- package/dist/run/consult.js +14 -5
- package/dist/run/daemon.d.ts +37 -2
- package/dist/run/daemon.js +579 -141
- package/dist/run/git.d.ts +8 -0
- package/dist/run/git.js +14 -0
- package/dist/run/journal.d.ts +126 -3
- package/dist/run/journal.js +410 -37
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +31 -1
- package/dist/run/protocol.js +3 -1
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/tui/cockpit/board.js +3 -3
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.js +13 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +26 -1
- package/dist/tui/cockpit/run-view.d.ts +9 -3
- package/dist/tui/cockpit/run-view.js +60 -7
- package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
- package/dist/tui/cockpit/setup-cockpit.js +6 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +2 -1
- package/schema/config.schema.json +818 -0
- package/skills/tickmarkr-loop/SKILL.md +8 -2
- package/skills/tickmarkr-overseer/SKILL.md +42 -0
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
|
@@ -5,7 +5,7 @@ import { loadConfig } from "../../config/config.js";
|
|
|
5
5
|
import { integrationBranch } from "../../run/merge.js";
|
|
6
6
|
import { separabilityErrors, surfaceErrors, taskBudgetErrors } from "../../compile/collateral.js";
|
|
7
7
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
8
|
-
import { applyScopeAmendments, engagementComparable, engagementReleased, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
8
|
+
import { applyScopeAmendments, bindingToken, engagementComparable, engagementReleased, failedGateBeforePark, parseBindingToken, physicalLine, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
9
9
|
export const APPROVAL_DISPOSITIONS = ["dispatch", "waive-gate", "re-dispatch", "fund-fixed-attempt", "fresh-budget"];
|
|
10
10
|
/**
|
|
11
11
|
* What each disposition's release actually buys — the clause every operator-facing sentence about
|
|
@@ -56,9 +56,9 @@ sourceIndexes) {
|
|
|
56
56
|
const kind = typeof e.data.kind === "string" ? e.data.kind : undefined;
|
|
57
57
|
const reason = typeof e.data.reason === "string" ? e.data.reason : undefined;
|
|
58
58
|
return {
|
|
59
|
-
index: i, line:
|
|
59
|
+
index: i, line: sourceIndexes?.[i] === undefined ? physicalLine(events, i) : sourceIndexes[i] + 1, ts: typeof e.ts === "string" ? e.ts : undefined, kind, reason,
|
|
60
60
|
approveCommand: typeof e.data.approveCommand === "string" ? e.data.approveCommand : undefined,
|
|
61
|
-
failedGate:
|
|
61
|
+
failedGate: failedGateBeforePark(events, taskId, i), tombstone: isTombstonePark(kind, reason),
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
return undefined;
|
|
@@ -71,6 +71,17 @@ export function readJournalEvents(journal) {
|
|
|
71
71
|
sourceIndexes: tracked.map((row) => row.sourceIndex),
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/** OBS-1178: the token a park is decided by — what status, park notices and the cockpit print. */
|
|
75
|
+
export const parkToken = (park) => park.ts === undefined ? undefined : bindingToken({ line: park.line, ts: park.ts });
|
|
76
|
+
/** OBS-1178: the newest task-failed row for a task — the failure a failed-task recheck binds to. */
|
|
77
|
+
export function newestFailure(events, taskId, sourceIndexes) {
|
|
78
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
79
|
+
const e = events[i];
|
|
80
|
+
if (e.event === "task-failed" && e.taskId === taskId && typeof e.ts === "string")
|
|
81
|
+
return { line: sourceIndexes?.[i] === undefined ? physicalLine(events, i) : sourceIndexes[i] + 1, ts: e.ts };
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
74
85
|
/**
|
|
75
86
|
* FINAL §3.3's decision menu as data: human-gate/attempt-cap/other non-gate parks → approve; infra →
|
|
76
87
|
* approve or recheck; review gate-fail → waive/uphold/recheck; other gate-fail → waive/recheck; a
|
|
@@ -137,13 +148,17 @@ export function approvalEnactment(token, run) {
|
|
|
137
148
|
}
|
|
138
149
|
/** The production command registered in COMMANDS; its returned bytes are what the CLI prints. */
|
|
139
150
|
export async function approve(argv, cwd = process.cwd()) {
|
|
140
|
-
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling, files } = parseArgs(argv);
|
|
151
|
+
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling, files, park: namedPark, gate: namedGate } = parseArgs(argv);
|
|
141
152
|
const decisions = [waive, uphold, recheck].filter(Boolean).length;
|
|
142
153
|
if (decisions > 1)
|
|
143
154
|
throw new Error("--waive, --uphold and --recheck are different decisions — pass one");
|
|
155
|
+
// OBS-1178: the decision binds at COMMAND time — to the named --park, else to the park (or, for a
|
|
156
|
+
// failed task's recheck, the failure) open now — and is revalidated under serialization below. A
|
|
157
|
+
// decision queued behind another approval can therefore never land on a park that opened meanwhile.
|
|
158
|
+
// Journal.open throws `no journal for <runId> at <dir>` on an unknown run — that IS the refusal.
|
|
159
|
+
const bound = namedPark ?? openDecision(Journal.open(cwd, runId), taskId);
|
|
144
160
|
const serialization = await acquireApprovalSerialization(cwd, runId);
|
|
145
161
|
try {
|
|
146
|
-
// Journal.open throws `no journal for <runId> at <dir>` on an unknown run — that IS the refusal.
|
|
147
162
|
const journal = Journal.open(cwd, runId);
|
|
148
163
|
const status = journal.replayStatuses().get(taskId);
|
|
149
164
|
if (status === undefined) {
|
|
@@ -153,13 +168,17 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
153
168
|
// integration base owns verified-able work; --recheck re-gates that tree with no worker. Any other
|
|
154
169
|
// verb on a failed task, or a recheck over nothing landed, is refused naming why.
|
|
155
170
|
if (status === "failed" && recheck) {
|
|
171
|
+
if (namedGate !== undefined)
|
|
172
|
+
throw new Error(`--gate names a parked failed gate; task ${taskId} is failed — bind its recheck with --park <failure token>`);
|
|
173
|
+
const { events: failedEvents, sourceIndexes: failedIndexes } = readJournalEvents(journal);
|
|
174
|
+
const failure = requireBound(runId, taskId, bound, newestFailure(failedEvents, taskId, failedIndexes), "failure");
|
|
156
175
|
const ahead = failedTaskCommitsAhead(cwd, journal, runId, taskId);
|
|
157
176
|
if (ahead.count === 0) {
|
|
158
177
|
throw new Error(`task ${taskId} is failed and ${ahead.ref} carries no commits ahead of ${ahead.base} — nothing to re-gate; use resume --retry-failed to fund a worker`);
|
|
159
178
|
}
|
|
160
179
|
journal.append("task-approved", taskId, {
|
|
161
180
|
by, ...(reason ? { reason } : {}), via: "cli", release: RECHECK_RELEASE,
|
|
162
|
-
recheckedRef: ahead.ref, commitsAhead: ahead.count,
|
|
181
|
+
recheckedRef: ahead.ref, commitsAhead: ahead.count, failure,
|
|
163
182
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
164
183
|
});
|
|
165
184
|
return disposition(cwd, runId, "re-dispatch", `re-checking failed ${taskId} in ${runId} — by ${by}; ${ahead.count} commit(s) on ${ahead.ref} ahead of ${ahead.base}; no worker funded`, serialization.contended);
|
|
@@ -178,6 +197,10 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
178
197
|
const infraPark = park?.kind === "infra" || park?.kind === "diff-cap";
|
|
179
198
|
const authoringRunnerPark = park?.kind === "authoring" && isToolGate(park.failedGate);
|
|
180
199
|
const failedGate = gateFailPark || authoringRunnerPark ? park?.failedGate : undefined;
|
|
200
|
+
const parkBinding = requireBound(runId, taskId, bound, park?.ts === undefined ? undefined : { line: park.line, ts: park.ts }, "park");
|
|
201
|
+
if (namedGate !== undefined && namedGate !== park?.failedGate) {
|
|
202
|
+
throw new Error(`refusing mismatched gate for ${taskId}: --gate ${namedGate} but park ${bindingToken(parkBinding)} failed ${park?.failedGate ?? "no gate"} — nothing appended`);
|
|
203
|
+
}
|
|
181
204
|
if ((park?.kind === "scope-request" && !decisions) || files !== undefined) {
|
|
182
205
|
if (park?.kind !== "scope-request")
|
|
183
206
|
throw new Error("--files requires a scope-request park");
|
|
@@ -209,7 +232,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
209
232
|
+ `remedy: close the run, split the task in its spec, recompile, then \`tickmarkr resume ${runId} --graph-changed\``);
|
|
210
233
|
}
|
|
211
234
|
journal.append("task-approved", taskId, {
|
|
212
|
-
by, ...(reason ? { reason } : {}), via: "cli", release: "scope-request",
|
|
235
|
+
by, ...(reason ? { reason } : {}), via: "cli", release: "scope-request", park: parkBinding,
|
|
213
236
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }), // OBS-1083
|
|
214
237
|
amendment: { from, to: graphDefinitionHash(amended), beforeFiles: task.files, files: amendedFiles, parkLine: park.line, definition: taskDefinitionFingerprint(task) },
|
|
215
238
|
});
|
|
@@ -234,6 +257,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
234
257
|
via: "cli",
|
|
235
258
|
release: REVIEW_UPHELD_RELEASE,
|
|
236
259
|
gate: "review",
|
|
260
|
+
park: parkBinding,
|
|
237
261
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
238
262
|
});
|
|
239
263
|
return disposition(cwd, runId, "fund-fixed-attempt", `upheld the reviewer for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
@@ -247,6 +271,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
247
271
|
...(reason ? { reason } : {}),
|
|
248
272
|
via: "cli",
|
|
249
273
|
release: RECHECK_RELEASE,
|
|
274
|
+
park: parkBinding,
|
|
250
275
|
// OBS-1084: failedGate names the runner report for every recheck. Authoring rechecks additionally
|
|
251
276
|
// carry gate so their durable release row identifies the red tool gate that made recheck admissible.
|
|
252
277
|
...(failedGate ? { failedGate } : {}),
|
|
@@ -265,6 +290,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
265
290
|
via: "cli",
|
|
266
291
|
release: GATE_SATISFIED_RELEASE,
|
|
267
292
|
gate: failedGate,
|
|
293
|
+
park: parkBinding,
|
|
268
294
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
269
295
|
});
|
|
270
296
|
return disposition(cwd, runId, "waive-gate", `waived failed gate ${failedGate} for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
@@ -292,6 +318,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
292
318
|
via: "cli",
|
|
293
319
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
294
320
|
...(capPark ? { release: ATTEMPT_CAP_RELEASE } : {}),
|
|
321
|
+
park: parkBinding,
|
|
295
322
|
});
|
|
296
323
|
const token = capPark ? "fresh-budget" : "dispatch";
|
|
297
324
|
return disposition(cwd, runId, token, `approved ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
@@ -300,6 +327,23 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
300
327
|
serialization.release();
|
|
301
328
|
}
|
|
302
329
|
}
|
|
330
|
+
/** The row a decision on this task answers right now: its newest failure when failed, else its newest park. */
|
|
331
|
+
function openDecision(journal, taskId) {
|
|
332
|
+
const { events, sourceIndexes } = readJournalEvents(journal);
|
|
333
|
+
if (journal.replayStatuses().get(taskId) === "failed")
|
|
334
|
+
return newestFailure(events, taskId, sourceIndexes);
|
|
335
|
+
const park = newestPark(events, taskId, sourceIndexes);
|
|
336
|
+
return park?.ts === undefined ? undefined : { line: park.line, ts: park.ts };
|
|
337
|
+
}
|
|
338
|
+
/** OBS-1178: under serialization, the decision's command-time binding must still name the open row. */
|
|
339
|
+
function requireBound(runId, taskId, bound, open, what) {
|
|
340
|
+
if (open === undefined)
|
|
341
|
+
throw new Error(`task ${taskId} has no ${what} row with a timestamp to bind this decision to — refusing an unbound release`);
|
|
342
|
+
if (bound === undefined || bound.line !== open.line || bound.ts !== open.ts) {
|
|
343
|
+
throw new Error(`refusing stale decision for ${taskId}: bound to ${bound === undefined ? "no open row" : `${what} ${bindingToken(bound)}`} but the open ${what} is ${bindingToken(open)} — read \`tickmarkr status ${runId}\` and decide the open ${what}; nothing appended`);
|
|
344
|
+
}
|
|
345
|
+
return open;
|
|
346
|
+
}
|
|
303
347
|
/**
|
|
304
348
|
* OBS-1022: what a failed task has landed. A preserved snapshot is authoritative only when it contains
|
|
305
349
|
* the current task-branch tip; an older preservation can remain in the journal after a later retry has
|
|
@@ -365,12 +409,14 @@ function disposition(cwd, runId, token, message, contended) {
|
|
|
365
409
|
};
|
|
366
410
|
return `${out}\nTICKMARKR_APPROVAL ${JSON.stringify(record)}`;
|
|
367
411
|
}
|
|
368
|
-
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--files <glob,…>] [--waive|--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
412
|
+
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--park <line>@<ts>] [--gate <gate>] [--files <glob,…>] [--waive|--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
369
413
|
// hand-parsed argv — no CLI framework (house style). Positionals are runId then taskId; decision,
|
|
370
414
|
// ceiling, actor and reason are flags. Throws usage on missing positionals (mirrors resume.ts/unlock.ts).
|
|
371
415
|
function parseArgs(argv) {
|
|
372
416
|
const positionals = [];
|
|
373
417
|
let files;
|
|
418
|
+
let park;
|
|
419
|
+
let gate;
|
|
374
420
|
let by;
|
|
375
421
|
let reason;
|
|
376
422
|
let waive = false;
|
|
@@ -388,6 +434,17 @@ function parseArgs(argv) {
|
|
|
388
434
|
throw new Error("--files requires non-empty repository-relative globs");
|
|
389
435
|
}
|
|
390
436
|
}
|
|
437
|
+
else if (a === "--park") {
|
|
438
|
+
const value = argv[++i];
|
|
439
|
+
park = value === undefined ? undefined : parseBindingToken(value);
|
|
440
|
+
if (!park)
|
|
441
|
+
throw new Error("--park requires the <line>@<ts> token `tickmarkr status` prints for the park");
|
|
442
|
+
}
|
|
443
|
+
else if (a === "--gate") {
|
|
444
|
+
gate = argv[++i];
|
|
445
|
+
if (!gate || !GATE_NAMES.includes(gate))
|
|
446
|
+
throw new Error(`--gate requires one of ${GATE_NAMES.join(", ")}`);
|
|
447
|
+
}
|
|
391
448
|
else if (a === "--by") {
|
|
392
449
|
by = argv[++i];
|
|
393
450
|
if (!by)
|
|
@@ -424,19 +481,5 @@ function parseArgs(argv) {
|
|
|
424
481
|
if (!runId || !taskId) {
|
|
425
482
|
throw new Error(USAGE);
|
|
426
483
|
}
|
|
427
|
-
return { runId, taskId, by: by ?? userInfo().username, reason, waive, uphold, recheck, reviewRoundCeiling, files };
|
|
428
|
-
}
|
|
429
|
-
function failedGateForNewestPark(events, taskId, lastHumanIndex) {
|
|
430
|
-
for (let i = lastHumanIndex - 1; i >= 0; i -= 1) {
|
|
431
|
-
const event = events[i];
|
|
432
|
-
if (event.taskId !== taskId)
|
|
433
|
-
continue;
|
|
434
|
-
if (event.event === "task-approved" || event.event === "task-human" || event.event === "task-dispatch")
|
|
435
|
-
return undefined;
|
|
436
|
-
if (event.event === "gate-result" && event.data.pass === false
|
|
437
|
-
&& typeof event.data.gate === "string" && GATE_NAMES.includes(event.data.gate)) {
|
|
438
|
-
return event.data.gate;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return undefined;
|
|
484
|
+
return { runId, taskId, by: by ?? userInfo().username, reason, waive, uphold, recheck, reviewRoundCeiling, files, park, gate };
|
|
442
485
|
}
|
|
@@ -2,8 +2,9 @@ import { isAbsolute, join } from "node:path";
|
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { collateralLints, sourceScopeFindings, sourceScopeLints, } from "../../compile/collateral.js";
|
|
4
4
|
import { CompileError } from "../../compile/common.js";
|
|
5
|
+
import { readDoctor } from "../../adapters/registry.js";
|
|
5
6
|
import { loadConfig } from "../../config/config.js";
|
|
6
|
-
import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
|
|
7
|
+
import { deadPoolEntries, deadPoolEntryLine, denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
|
|
7
8
|
import { compileSource } from "../../compile/index.js";
|
|
8
9
|
import { retiredLiteralErrors } from "../../compile/retired-literals.js";
|
|
9
10
|
import { clearCompileRefusal, saveCompileRefusal, saveGraph, stateDirName } from "../../graph/graph.js";
|
|
@@ -88,11 +89,20 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
88
89
|
// that uses it; plan already said so, compile now refuses the seal before any state write so a dry
|
|
89
90
|
// run reaches the same refusal. Pool only: a partial deny leaves live members, and pin/prefer
|
|
90
91
|
// collisions on the default map stay doctor's and resume's business. Scoped to the graph's shapes.
|
|
91
|
-
|
|
92
|
+
// OBS-1143: aliases are judged by doctor's CACHED identity — compile never spends a probe.
|
|
93
|
+
const routingCfg = loadConfig(cwd);
|
|
94
|
+
const shapes = g.tasks.map((task) => task.shape);
|
|
95
|
+
const health = readDoctor(cwd);
|
|
96
|
+
const deadPools = denyPreferCollisions(routingCfg, shapes, health)
|
|
92
97
|
.filter((collision) => collision.kind === "pool");
|
|
93
98
|
if (deadPools.length > 0) {
|
|
94
99
|
throw new CompileError(`${src} routes a task shape to a fully denied pool:\n${deadPools.map((collision) => ` - ${denyPreferCollisionLine(collision)}`).join("\n")}${diagnostics}`);
|
|
95
100
|
}
|
|
101
|
+
// OBS-1144: a partial deny compiles, as the router routes it — but each skipped entry is named.
|
|
102
|
+
const skippedPoolEntries = deadPoolEntries(routingCfg, shapes, health).map(deadPoolEntryLine);
|
|
103
|
+
const poolNotes = skippedPoolEntries.length
|
|
104
|
+
? `\npool notes:\n${skippedPoolEntries.map((line) => ` ! ${line}`).join("\n")}`
|
|
105
|
+
: "";
|
|
96
106
|
// One bounded read supplies both cross-run surfaces: unresolved findings below and merge facts for
|
|
97
107
|
// the ancestry check. Neither fact mutates the compiled graph; status and every readiness predicate
|
|
98
108
|
// remain the source compiler's answer.
|
|
@@ -121,7 +131,7 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
121
131
|
const mergeHistory = mergedPending.length
|
|
122
132
|
? `\nmerge history:\n${mergedPending.map((line) => ` ${line}`).join("\n")}`
|
|
123
133
|
: "";
|
|
124
|
-
return `${harnessLine(resolveHarness(harnessFrom))}\n${summary}${diagnostics}${priorFindings}${mergeHistory}`;
|
|
134
|
+
return `${harnessLine(resolveHarness(harnessFrom))}\n${summary}${diagnostics}${poolNotes}${priorFindings}${mergeHistory}`;
|
|
125
135
|
}
|
|
126
136
|
catch (error) {
|
|
127
137
|
// A dry run is a pure validation query. A real authoring refusal records the negative result
|
|
@@ -162,6 +162,7 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
162
162
|
modelOverrides?: Record<string, {
|
|
163
163
|
vendor?: string | undefined;
|
|
164
164
|
channel?: "sub" | "api" | undefined;
|
|
165
|
+
effort?: "low" | "medium" | "high" | undefined;
|
|
165
166
|
}> | undefined;
|
|
166
167
|
windows?: Record<string, number> | undefined;
|
|
167
168
|
}>;
|
|
@@ -286,6 +287,7 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
286
287
|
modelOverrides?: Record<string, {
|
|
287
288
|
vendor?: string | undefined;
|
|
288
289
|
channel?: "sub" | "api" | undefined;
|
|
290
|
+
effort?: "low" | "medium" | "high" | undefined;
|
|
289
291
|
}> | undefined;
|
|
290
292
|
windows?: Record<string, number> | undefined;
|
|
291
293
|
}>;
|
|
@@ -14,10 +14,11 @@ import { loadConfig, overlayPreferShapes } from "../../config/config.js";
|
|
|
14
14
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
15
15
|
import { ORCA_FIXTURE_VERSION, parseEnvelope, resolveOrcaCliBinary } from "../../drivers/orca.js";
|
|
16
16
|
import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
|
|
17
|
-
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
17
|
+
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, observedSeat, preferRanks } from "../../route/preference.js";
|
|
18
18
|
import { CATALOG_REFRESH_TIMEOUT_MS, LIVEBENCH_TABLE_DATE, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
19
19
|
import { sh } from "../../run/git.js";
|
|
20
20
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
21
|
+
import { readReviewNoVerdictHistory, reviewNoVerdictRows } from "../../run/journal.js";
|
|
21
22
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
22
23
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
23
24
|
export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
@@ -670,6 +671,13 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
670
671
|
rows.push(alignedStatusRow("warn", demotion.reviewer, `${demotion.count} review seat${demotion.count === 1 ? "" : "s"} demoted · cause ${causes}`));
|
|
671
672
|
}
|
|
672
673
|
}
|
|
674
|
+
// OBS-1052(3): measured, never inferred — an unreadable journal renders unknown, not a clean zero.
|
|
675
|
+
const noVerdictRows = reviewNoVerdictRows(readReviewNoVerdictHistory(cwd));
|
|
676
|
+
if (noVerdictRows.length) {
|
|
677
|
+
rows.push(legend("review no-verdict history:"));
|
|
678
|
+
for (const row of noVerdictRows)
|
|
679
|
+
rows.push(alignedStatusRow(row.verdict, row.channel, row.value));
|
|
680
|
+
}
|
|
673
681
|
if (existsSync(graphPath(cwd))) {
|
|
674
682
|
try {
|
|
675
683
|
const graph = loadGraph(cwd);
|
|
@@ -780,7 +788,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
780
788
|
const excluded = excludedChannels(cfg, adapters, health);
|
|
781
789
|
if (excluded.length)
|
|
782
790
|
lintRows.push(attentionRow(exclusionLine(excluded)));
|
|
783
|
-
lintRows.push(...denyPreferCollisions(cfg).map((c) => attentionRow(denyPreferCollisionLine(c))));
|
|
791
|
+
lintRows.push(...denyPreferCollisions(cfg, undefined, health).map((c) => attentionRow(denyPreferCollisionLine(c))));
|
|
784
792
|
const aliasExcluded = modelAliasExclusions(cfg, adapters, health);
|
|
785
793
|
if (aliasExcluded.length)
|
|
786
794
|
lintRows.push(attentionRow(modelAliasLine(aliasExcluded)));
|
|
@@ -857,7 +865,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
857
865
|
: v.authed
|
|
858
866
|
? `${ok("authed")}${probed}`
|
|
859
867
|
: `${fail("unauthed:")} ${trunc(v.reason ?? "probe failed", 40)} (${dateOf(v.probedAt)})`;
|
|
860
|
-
const d = disallowedBy(
|
|
868
|
+
const d = disallowedBy(observedSeat(health, a.id, m), cfg.routing);
|
|
861
869
|
const denied = d?.by === "deny" ? d.entry : "—";
|
|
862
870
|
const pref = preferRanks({ adapter: a.id, model: m }, cfg).map((p) => `${p.shape}#${p.rank}`).join(",") || "—";
|
|
863
871
|
const window = declaredModelWindow(cfg, a.id, m);
|
|
@@ -5,16 +5,16 @@ import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthE
|
|
|
5
5
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
6
6
|
import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
7
7
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
8
|
-
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, overlayBytesLoadError, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
|
|
8
|
+
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, lowerLayerModelOverrides, overlayBytesLoadError, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
9
|
import { exclusionReason, projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
10
10
|
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
11
11
|
import { doctor } from "./doctor.js";
|
|
12
12
|
import { candidateRow, costSignal, shapeCandidates } from "./fleet-picker.js";
|
|
13
13
|
import { route } from "../../route/router.js";
|
|
14
14
|
import { pickRole } from "../../route/role-pick.js";
|
|
15
|
-
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, entryMatchesChannel, exclusionCollector } from "../../route/preference.js";
|
|
15
|
+
import { deadPoolEntries, deadPoolEntryLine, denyPreferCollisionLine, denyPreferCollisions, disallowedBy, entryMatchesChannel, exclusionCollector } from "../../route/preference.js";
|
|
16
16
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
17
|
-
import { loadRoutingProfile } from "../../run/journal.js";
|
|
17
|
+
import { loadRoutingProfile, readReviewNoVerdictHistory, reviewNoVerdictRows } from "../../run/journal.js";
|
|
18
18
|
const initialFetch = globalThis.fetch;
|
|
19
19
|
const NON_TTY_MSG = "tickmarkr fleet: interactive fleet editor requires a TTY — use `tickmarkr fleet --print` for non-interactive output";
|
|
20
20
|
const QUIT = "fleet: quit without writing";
|
|
@@ -581,7 +581,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
581
581
|
captions.push(`task hint: routing.map.${shape}.prefer ranks ${entry.prefer.join(", ")} — not manageable here, edit it in Shapes`);
|
|
582
582
|
}
|
|
583
583
|
// 8. deny∩prefer collision — a standing lint on this shape's declaration, never a fleet toggle.
|
|
584
|
-
for (const collision of denyPreferCollisions(cfgPreview, [shape])) {
|
|
584
|
+
for (const collision of denyPreferCollisions(cfgPreview, [shape], health)) {
|
|
585
585
|
captions.push(denyPreferCollisionLine(collision));
|
|
586
586
|
}
|
|
587
587
|
// 9. first-match provenance — the per-channel ledger below lists EVERY collector scope.
|
|
@@ -594,7 +594,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
594
594
|
const channelLedger = (shape, map, cfgPreview, offered) => {
|
|
595
595
|
const entry = map[shape];
|
|
596
596
|
const floor = cfgPreview.routing.floors[shape];
|
|
597
|
-
const collisions = denyPreferCollisions(cfgPreview, [shape]);
|
|
597
|
+
const collisions = denyPreferCollisions(cfgPreview, [shape], health);
|
|
598
598
|
const discovered = new Set(previewChannels.map((c) => `${c.adapter}:${c.model}`));
|
|
599
599
|
// T2 review (D-225): discovery already dropped a channel whose model probe failed — it is
|
|
600
600
|
// rebuilt as a structured channel from its adapter's own declaration and walks the SAME
|
|
@@ -616,7 +616,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
616
616
|
// never greys beside itself; its pin/pool/floor/prefer captions stay in excludedNote
|
|
617
617
|
const ledgerChannels = [...previewChannels.map((c) => ({ ...c, authReason: undefined })), ...failedProbes, ...unauthedCliChannels]
|
|
618
618
|
.filter((c) => !offered.has(`${c.adapter}:${c.model}`));
|
|
619
|
-
|
|
619
|
+
const lines = ledgerChannels.flatMap((c) => {
|
|
620
620
|
const key = `${c.adapter}:${c.model}`;
|
|
621
621
|
const reasons = exclusionCollector(c, cfgPreview.routing, "worker").map(exclusionReason);
|
|
622
622
|
// OBS-1065: a greyed picker row carries its reach beside its reasons, like the models view
|
|
@@ -645,8 +645,19 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
645
645
|
if (c.authReason)
|
|
646
646
|
reasons.push(c.authReason);
|
|
647
647
|
// labelled like the models view's row (adapter/model), so a ledger row never reads as a candidate
|
|
648
|
-
return reasons.length ? [`${c.adapter}/${c.model} — reach: ${reach} — ${reasons.join("; ")}`] : [];
|
|
648
|
+
return reasons.length ? [{ key, line: `${c.adapter}/${c.model} — reach: ${reach} — ${reasons.join("; ")}` }] : [];
|
|
649
649
|
});
|
|
650
|
+
// OBS-1145 add.1: every carried pool member the picker does not offer leads the ledger in chain
|
|
651
|
+
// order — one no reason above names still gets its own row — so the picker renders its ordinal
|
|
652
|
+
// and Space can drop it; a chain member with no row is hidden AND irremovable
|
|
653
|
+
const carried = entry?.pool?.channels.filter((key) => !offered.has(key)) ?? [];
|
|
654
|
+
const named = new Set(lines.map((row) => row.key));
|
|
655
|
+
const unnamed = carried.filter((key) => !named.has(key)).map((key) => ({
|
|
656
|
+
key,
|
|
657
|
+
line: `${key.replace(":", "/")} — ${discovered.has(key) ? `not offered for ${shape}` : "not served by an installed adapter"} — carried in routing.map.${shape}.pool`,
|
|
658
|
+
}));
|
|
659
|
+
const order = (key) => (carried.includes(key) ? carried.indexOf(key) : carried.length);
|
|
660
|
+
return [...lines, ...unnamed].sort((a, b) => order(a.key) - order(b.key)).map((row) => row.line);
|
|
650
661
|
};
|
|
651
662
|
const candidatesForShape = (shape, mode, map, deny) => {
|
|
652
663
|
const preview = previewCfg(mode, map, deny);
|
|
@@ -677,6 +688,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
677
688
|
review: cfg.review.prefer?.slice(),
|
|
678
689
|
consult: cfg.consult.prefer?.slice(),
|
|
679
690
|
};
|
|
691
|
+
// OBS-1052(3): shown on the Steering view beside review.prefer; the pickers above stay untouched.
|
|
692
|
+
const reviewAdvisories = reviewNoVerdictRows(readReviewNoVerdictHistory(cwd))
|
|
693
|
+
.filter((row) => row.verdict === "warn")
|
|
694
|
+
.map((row) => `review history: ${row.channel} — ${row.value}`);
|
|
680
695
|
const steeringOptionsFor = (which, current) => {
|
|
681
696
|
const discovered = which === "review" ? [...reviewAdapters, ...seats] : seats;
|
|
682
697
|
return [...discovered, ...current.filter((entry) => !discovered.includes(entry))];
|
|
@@ -685,6 +700,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
685
700
|
// discovered seats universe plus a keep-default row, and a write is staged only on a real change.
|
|
686
701
|
const initialJudge = `${cfg.judge.adapter}:${cfg.judge.model}`;
|
|
687
702
|
let pendingWrite = null;
|
|
703
|
+
// OBS-1182: the layers under the repo overlay, read raw, so clearing an effort masks an inherited
|
|
704
|
+
// one even when those layers only validate beside repo fields.
|
|
705
|
+
const lowerOverrides = lowerLayerModelOverrides({ globalDir });
|
|
688
706
|
const reviewOverlay = (state) => {
|
|
689
707
|
const staged = structuredClone(initial);
|
|
690
708
|
// OBS-994/FL-1, OBS-1099 add.1: every schema-enumerated deny scope rides the same review/write funnel
|
|
@@ -692,6 +710,12 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
692
710
|
staged[scope.key] = state[scope.key];
|
|
693
711
|
staged.allowOut = state.allowOut ?? initial.allowOut;
|
|
694
712
|
staged.map = state.map;
|
|
713
|
+
// OBS-1182: staged efforts replace the loaded ones; none staged anywhere ⇒ no key, as loaded
|
|
714
|
+
const efforts = Object.fromEntries(Object.entries(state.efforts ?? {}).filter(([, models]) => Object.keys(models).length));
|
|
715
|
+
if (Object.keys(efforts).length)
|
|
716
|
+
staged.efforts = efforts;
|
|
717
|
+
else
|
|
718
|
+
delete staged.efforts;
|
|
695
719
|
const today = new Date().toISOString().slice(0, 10);
|
|
696
720
|
for (const classification of state.classifications) {
|
|
697
721
|
staged.tiers[classification.adapter] ??= {};
|
|
@@ -717,6 +741,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
717
741
|
initial,
|
|
718
742
|
edited: staged,
|
|
719
743
|
universe,
|
|
744
|
+
lowerOverrides,
|
|
720
745
|
...(modeChanged ? { mode: state.selectedMode } : {}),
|
|
721
746
|
...(judgeChanged && judgeSeat ? { judge: judgeSeat } : {}),
|
|
722
747
|
steering: { initial: initialSteering, edited: state.steering },
|
|
@@ -727,12 +752,23 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
727
752
|
return { kind: "empty" };
|
|
728
753
|
}
|
|
729
754
|
pendingWrite = write;
|
|
755
|
+
// OBS-1144: the review names every dead pool entry the written config carries, with its reason and
|
|
756
|
+
// what still routes — before y. Unloadable bytes carry no notes: the reload guard refuses them anyway.
|
|
757
|
+
let poolNotes = [];
|
|
758
|
+
try {
|
|
759
|
+
const written = loadConfigWithMode(cwd, { globalDir, repoOverlayText: after }).cfg;
|
|
760
|
+
poolNotes = deadPoolEntries(written, undefined, health).map(deadPoolEntryLine);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
poolNotes = [];
|
|
764
|
+
}
|
|
730
765
|
return {
|
|
731
766
|
kind: "diff",
|
|
732
767
|
before,
|
|
733
768
|
after,
|
|
734
769
|
diff: unifiedYamlDiff(before, after, path),
|
|
735
770
|
path,
|
|
771
|
+
...(poolNotes.length ? { notes: poolNotes } : {}),
|
|
736
772
|
};
|
|
737
773
|
};
|
|
738
774
|
const reloadGuard = io.reloadGuard
|
|
@@ -743,6 +779,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
743
779
|
health,
|
|
744
780
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [initialDenyPropOf(scope), editable[scope.key] ?? []])),
|
|
745
781
|
initialAllowOut: editable.allowOut,
|
|
782
|
+
initialEfforts: editable.efforts,
|
|
746
783
|
modelGroups,
|
|
747
784
|
initialMode: rm.mode.mode,
|
|
748
785
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
@@ -756,6 +793,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
756
793
|
],
|
|
757
794
|
initialSteering,
|
|
758
795
|
steeringOptionsFor,
|
|
796
|
+
reviewAdvisories,
|
|
759
797
|
reviewOverlay,
|
|
760
798
|
reloadGuard,
|
|
761
799
|
stagedRouting,
|
|
@@ -11,7 +11,7 @@ import { estimateCosts } from "../../report/cost.js";
|
|
|
11
11
|
import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChannelTokens, formatTokenUsage as fields, totalTokens as total, } from "../../report/operator-record.js";
|
|
12
12
|
import { cellsOf, cellSummary } from "../../route/profile.js";
|
|
13
13
|
import { Journal, loadRoutingProfile } from "../../run/journal.js";
|
|
14
|
-
import { formatTipProof, runEndTipProof } from "../../run/daemon.js";
|
|
14
|
+
import { baselineProvenanceOf, fingerprintsOf, forgivenFingerprints, forgivenGateRow, formatBaselineProvenance, formatFingerprints, formatForgiven, formatTipProof, runEndTipProof, } from "../../run/daemon.js";
|
|
15
15
|
import { deriveRunCockpitData } from "../../tui/cockpit/derive.js";
|
|
16
16
|
const n = (x) => x.toLocaleString("en-US"); // explicit locale — CI/darwin flake guard
|
|
17
17
|
const EM = "—";
|
|
@@ -170,6 +170,10 @@ const VERIFICATION_READING = {
|
|
|
170
170
|
failed: "FAILED — the run did not verify its own tip",
|
|
171
171
|
absent: "absent — no tip verification recorded: neither passed nor failed",
|
|
172
172
|
};
|
|
173
|
+
function forgivenLines(events) {
|
|
174
|
+
const forgiven = forgivenFingerprints(closedCycle(events));
|
|
175
|
+
return forgiven.length ? ["- **forgiven vs baseline:**", ...forgiven.map((f) => ` - ${formatForgiven(f)}`)] : [];
|
|
176
|
+
}
|
|
173
177
|
function verificationReading(runId, events) {
|
|
174
178
|
const state = verificationOf(runId, events);
|
|
175
179
|
const proof = runEndTipProof(closedCycle(events));
|
|
@@ -179,6 +183,16 @@ function verificationReading(runId, events) {
|
|
|
179
183
|
}
|
|
180
184
|
return VERIFICATION_READING[state];
|
|
181
185
|
}
|
|
186
|
+
// OBS-1123: a battery row that carried baseline reds names them apart from any red it introduced, so a
|
|
187
|
+
// new regression can never read as forgiven. Rows without the structured fields render as before.
|
|
188
|
+
const fingerprintClauses = (data) => {
|
|
189
|
+
const fresh = fingerprintsOf(data.freshFingerprints) ?? [];
|
|
190
|
+
const forgiven = fingerprintsOf(data.forgivenFingerprints);
|
|
191
|
+
if (!forgiven && !forgivenGateRow(data))
|
|
192
|
+
return "";
|
|
193
|
+
return `${fresh.length ? `; new red (not in baseline): ${formatFingerprints(fresh)}` : ""}`
|
|
194
|
+
+ `; forgiven vs baseline: ${formatFingerprints(forgiven)} — ${formatBaselineProvenance(baselineProvenanceOf(data.baselineProvenance))}`;
|
|
195
|
+
};
|
|
182
196
|
// VIS-07 / REC-01: derived only from the run journal, telemetry, and local configuration.
|
|
183
197
|
export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
184
198
|
const runStart = events.find((e) => e.event === "run-start");
|
|
@@ -216,6 +230,8 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
216
230
|
`- **failed:** ${count("failed")}`,
|
|
217
231
|
`- **human:** ${count("human")}`,
|
|
218
232
|
`- **verification:** ${verificationReading(runId, events)}`,
|
|
233
|
+
// OBS-1123: the same fold the run-end record states, over the same closed cycle as verification.
|
|
234
|
+
...forgivenLines(events),
|
|
219
235
|
"",
|
|
220
236
|
"## Usage & efficiency",
|
|
221
237
|
"",
|
|
@@ -280,7 +296,7 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
280
296
|
}
|
|
281
297
|
const resolved = gate === "review" && g.data.pass === true && Array.isArray(g.data.resolved)
|
|
282
298
|
? g.data.resolved.filter((id) => typeof id === "string") : [];
|
|
283
|
-
lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}${resolved.length ? `; resolved: ${resolved.join(", ")}` : ""}`);
|
|
299
|
+
lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}${resolved.length ? `; resolved: ${resolved.join(", ")}` : ""}${fingerprintClauses(g.data)}`);
|
|
284
300
|
}
|
|
285
301
|
for (const row of leg2) {
|
|
286
302
|
const pass = row.data.pass === true ? "pass" : row.data.pass === false ? "fail" : EM;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
+
import { readDoctor } from "../../adapters/registry.js";
|
|
2
3
|
import { loadConfig } from "../../config/config.js";
|
|
3
4
|
import { classifyHost, parseDriverOverride, pickDriver, preflightHostDriver } from "../../drivers/index.js";
|
|
4
5
|
import { loadGraph } from "../../graph/graph.js";
|
|
@@ -31,9 +32,10 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
31
32
|
// v1.87 T3 (OBS-162, twice-carried workaround): the preflight runs AFTER the graph is read and
|
|
32
33
|
// sees only the shapes the resumed graph carries. A deny∩prefer collision on a shape no resumed
|
|
33
34
|
// task uses is a config fact the run would never resolve — it must not refuse the only
|
|
34
|
-
// crash-recovery path. doctor still walks the whole map.
|
|
35
|
+
// crash-recovery path. doctor still walks the whole map. OBS-1143: aliases are judged by the
|
|
36
|
+
// identity doctor cached — the same one the daemon's discovery routes with.
|
|
35
37
|
const graph = loadGraph(cwd);
|
|
36
|
-
const collisions = denyPreferCollisions(cfg, graph.tasks.map((t) => t.shape));
|
|
38
|
+
const collisions = denyPreferCollisions(cfg, graph.tasks.map((t) => t.shape), readDoctor(cwd));
|
|
37
39
|
if (collisions.length) {
|
|
38
40
|
throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
|
|
39
41
|
}
|
|
@@ -12,7 +12,7 @@ import { authorsNote, doneAuthors } from "../../run/operator-state.js";
|
|
|
12
12
|
import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
13
13
|
import { trackJournalRows } from "../../run/protocol.js";
|
|
14
14
|
import { newestPark, permittedDecisionVerbs } from "./approve.js";
|
|
15
|
-
import { Journal, formatJournalNarration, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
15
|
+
import { Journal, bindingToken, effectiveEvents, formatJournalNarration, parseJournalText, physicalLine, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
16
16
|
import { isPidLive, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
17
17
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
18
18
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
@@ -167,7 +167,7 @@ export const decisionEventsFromJournal = (events, runId, stateDir = ".tickmarkr"
|
|
|
167
167
|
...base,
|
|
168
168
|
type: "human-decision-required",
|
|
169
169
|
tier: "decision",
|
|
170
|
-
approvalCommand: `tickmarkr approve ${runId} ${event.taskId}`,
|
|
170
|
+
approvalCommand: `tickmarkr approve ${runId} ${event.taskId} --park ${bindingToken({ line: physicalLine(events, index), ts: event.ts })}`,
|
|
171
171
|
...(typeof event.data.kind === "string" ? { kind: event.data.kind } : {}),
|
|
172
172
|
...(typeof event.data.reason === "string" ? { reason: event.data.reason } : {}),
|
|
173
173
|
}];
|
|
@@ -841,18 +841,6 @@ const statusEngagement = (events, loadedHash) => {
|
|
|
841
841
|
}
|
|
842
842
|
return { comparable: true };
|
|
843
843
|
};
|
|
844
|
-
// The journal's own reader rule (src/run/journal.ts readJsonl), applied to bytes already in hand:
|
|
845
|
-
// skip blanks, drop a line that will not parse (a torn trailing write after a crash), keep the rest.
|
|
846
|
-
const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
|
|
847
|
-
if (!line.trim())
|
|
848
|
-
return [];
|
|
849
|
-
try {
|
|
850
|
-
return [JSON.parse(line)];
|
|
851
|
-
}
|
|
852
|
-
catch {
|
|
853
|
-
return [];
|
|
854
|
-
}
|
|
855
|
-
});
|
|
856
844
|
// runLockOwner is the single lock payload/liveness reader. Its current path helper also initializes
|
|
857
845
|
// .tickmarkr metadata; an engine-written lock necessarily passed through that initializer already, so
|
|
858
846
|
// status avoids invoking it in stripped reader-purity fixtures where no engine lock can exist.
|
|
@@ -892,7 +880,7 @@ const readRunRecord = (cwd, graph, namedRunId) => {
|
|
|
892
880
|
if (!runId)
|
|
893
881
|
return undefined;
|
|
894
882
|
const raw = readRunJournalRaw(cwd, runId, lockedRunId === runId);
|
|
895
|
-
const events =
|
|
883
|
+
const events = parseJournalText(raw);
|
|
896
884
|
// The resume comparator is the fail-closed baseline; a matching graph-rehash is the daemon's
|
|
897
885
|
// append-only audit that authorizes this status replay after stop-amend-resume.
|
|
898
886
|
const engagement = statusEngagement(events, graphDefinitionHash(graph));
|
|
@@ -918,7 +906,8 @@ const recordTaskRows = (record, graph, isDaemonAlive) => record.comparable && re
|
|
|
918
906
|
const recordProjection = (record, graph, isDaemonAlive) => {
|
|
919
907
|
const rows = record ? recordTaskRows(record, graph, isDaemonAlive) : new Map();
|
|
920
908
|
const events = record?.comparable ? record.events : [];
|
|
921
|
-
|
|
909
|
+
// OBS-1178: a refused or unsound decision released nothing, so the activity fold never reads it.
|
|
910
|
+
const activity = projectActivity(record?.runId ?? "", trackJournalRows(record?.runId ?? "", effectiveEvents(events).map((raw, sourceIndex) => ({ raw, sourceIndex }))), graph.tasks);
|
|
922
911
|
const tasks = graph.tasks.map(task => {
|
|
923
912
|
const row = rows.get(task.id);
|
|
924
913
|
const evidence = events.filter(event => event.taskId === task.id);
|
|
@@ -1068,12 +1057,28 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1068
1057
|
// OBS-738: recovery facts stay on the journal's two established reducers. The prefix fold keeps an
|
|
1069
1058
|
// older journal equally readable by asking upheldFeedbackByTask what was active at that exact
|
|
1070
1059
|
// restore boundary; current resume-restore rows additionally carry the same task for narration.
|
|
1060
|
+
// OBS-1178: both folds read decisions through the one decision fold over the whole journal.
|
|
1061
|
+
const decided = effectiveEvents(events);
|
|
1071
1062
|
const preservedRefs = preservedRefsByTask(events);
|
|
1072
|
-
const restoredUpheldFeedback = new Set(
|
|
1073
|
-
&& upheldFeedbackByTask(
|
|
1063
|
+
const restoredUpheldFeedback = new Set(decided.flatMap((event, index) => event.event === "resume-restore" && event.taskId
|
|
1064
|
+
&& upheldFeedbackByTask(decided.slice(0, index)).has(event.taskId)
|
|
1074
1065
|
? [event.taskId]
|
|
1075
1066
|
: []));
|
|
1067
|
+
// OBS-1178: a failed task's newest task-failed row is the token its recheck binds to. Only an
|
|
1068
|
+
// effective decision consumes it — a refused or unsound recheck leaves the task failed and the token shown.
|
|
1069
|
+
const failureTokens = new Map();
|
|
1070
|
+
decided.forEach((event, index) => {
|
|
1071
|
+
if (!event.taskId || !["task-dispatch", "task-done", "task-failed", "task-human", "task-approved"].includes(event.event))
|
|
1072
|
+
return;
|
|
1073
|
+
if (event.event === "task-failed")
|
|
1074
|
+
failureTokens.set(event.taskId, bindingToken({ line: physicalLine(decided, index), ts: event.ts }));
|
|
1075
|
+
else
|
|
1076
|
+
failureTokens.delete(event.taskId);
|
|
1077
|
+
});
|
|
1076
1078
|
const recoveryLinesForTask = (taskId) => [
|
|
1079
|
+
...(failureTokens.has(taskId) && runId
|
|
1080
|
+
? [`failed — ${taskId} — failure ${failureTokens.get(taskId)} — re-gate landed work with \`tickmarkr approve ${runId} ${taskId} --recheck --park ${failureTokens.get(taskId)}\``]
|
|
1081
|
+
: []),
|
|
1077
1082
|
...(preservedRefs.get(taskId) ?? []).flatMap(({ ref, diffCommand }) => [
|
|
1078
1083
|
`preserved worktree — ${taskId} — ${ref}`,
|
|
1079
1084
|
` ${diffCommand}`,
|
|
@@ -1501,7 +1506,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1501
1506
|
decisionRunId = runId;
|
|
1502
1507
|
journalCursor = 0;
|
|
1503
1508
|
}
|
|
1504
|
-
const journalEvents =
|
|
1509
|
+
const journalEvents = parseJournalText(readRunJournalRaw(cwd, runId, lockedRunId === runId));
|
|
1505
1510
|
if (journalEvents.length < journalCursor)
|
|
1506
1511
|
journalCursor = 0;
|
|
1507
1512
|
const fresh = decisionEventsFromJournal(journalEvents, runId, stateDirName(cwd))
|