tickmarkr 2.6.1 → 2.6.3
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 +16 -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 +11 -4
- package/dist/cli/commands/approve.js +82 -27
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +87 -11
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +2 -1
- package/dist/cli/commands/report.js +74 -8
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +43 -20
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +9 -2
- package/dist/compile/native.js +7 -0
- package/dist/config/config.d.ts +35 -2
- package/dist/config/config.js +86 -10
- package/dist/config/fleet-overlay.d.ts +13 -2
- package/dist/config/fleet-overlay.js +60 -0
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +35 -2
- package/dist/drivers/orca.js +222 -67
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- 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 +9 -1
- package/dist/gates/acceptance.js +31 -4
- package/dist/gates/baseline.d.ts +32 -2
- package/dist/gates/baseline.js +111 -24
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +11 -4
- package/dist/gates/llm.js +40 -21
- package/dist/gates/review.d.ts +14 -1
- package/dist/gates/review.js +160 -34
- package/dist/gates/run-gates.d.ts +56 -4
- package/dist/gates/run-gates.js +358 -58
- package/dist/gates/test-manifest.d.ts +45 -1
- package/dist/gates/test-manifest.js +78 -12
- 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.d.ts +12 -1
- package/dist/route/router.js +56 -24
- package/dist/run/consult.d.ts +15 -1
- package/dist/run/consult.js +18 -7
- package/dist/run/daemon.d.ts +38 -2
- package/dist/run/daemon.js +895 -192
- package/dist/run/git.d.ts +8 -0
- package/dist/run/git.js +14 -0
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +152 -3
- package/dist/run/journal.js +551 -50
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- 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 +46 -1
- package/dist/run/protocol.js +14 -2
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- 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 +27 -2
- package/dist/tui/cockpit/run-view.d.ts +9 -2
- package/dist/tui/cockpit/run-view.js +66 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +6 -0
- package/dist/tui/cockpit/setup-cockpit.js +10 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +3 -1
- package/schema/config.schema.json +825 -0
- package/skills/tickmarkr-loop/SKILL.md +15 -3
- package/skills/tickmarkr-overseer/SKILL.md +42 -0
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +91 -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,10 @@ 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
|
+
...(kind === "stall" && typeof e.data.reapFailure === "string" ? { reapFailure: e.data.reapFailure } : {}),
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
return undefined;
|
|
@@ -71,9 +72,21 @@ export function readJournalEvents(journal) {
|
|
|
71
72
|
sourceIndexes: tracked.map((row) => row.sourceIndex),
|
|
72
73
|
};
|
|
73
74
|
}
|
|
75
|
+
/** OBS-1178: the token a park is decided by — what status, park notices and the cockpit print. */
|
|
76
|
+
export const parkToken = (park) => park.ts === undefined ? undefined : bindingToken({ line: park.line, ts: park.ts });
|
|
77
|
+
/** OBS-1178: the newest task-failed row for a task — the failure a failed-task recheck binds to. */
|
|
78
|
+
export function newestFailure(events, taskId, sourceIndexes) {
|
|
79
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
80
|
+
const e = events[i];
|
|
81
|
+
if (e.event === "task-failed" && e.taskId === taskId && typeof e.ts === "string")
|
|
82
|
+
return { line: sourceIndexes?.[i] === undefined ? physicalLine(events, i) : sourceIndexes[i] + 1, ts: e.ts };
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
74
86
|
/**
|
|
75
|
-
* FINAL §3.3's decision menu as data: human-gate/attempt-cap/other non-gate parks → approve; infra
|
|
76
|
-
*
|
|
87
|
+
* FINAL §3.3's decision menu as data: human-gate/attempt-cap/other non-gate parks → approve; infra, and
|
|
88
|
+
* a stall park that recorded a reapFailure (OBS-1202), → approve or recheck; review gate-fail →
|
|
89
|
+
* waive/uphold/recheck; other gate-fail → waive/recheck; a
|
|
77
90
|
* gate-fail park with no failed-gate evidence, or a tombstone → nothing (a diagnostic, never a
|
|
78
91
|
* fabricated verb). The refusals in `approve` below enforce the same table; this is the one place a
|
|
79
92
|
* surface may read it from, so what a menu offers and what the command accepts cannot drift.
|
|
@@ -88,6 +101,10 @@ export function permittedDecisionVerbs(park) {
|
|
|
88
101
|
}
|
|
89
102
|
if (park.kind === "infra")
|
|
90
103
|
return ["approve", "recheck"];
|
|
104
|
+
// OBS-1202: an unreadable reap census parks finished work; recheck re-verifies that census and gates the
|
|
105
|
+
// harvested commits with no worker. An ordinary stall recorded no census failure: approve alone.
|
|
106
|
+
if (park.kind === "stall" && park.reapFailure !== undefined)
|
|
107
|
+
return ["approve", "recheck"];
|
|
91
108
|
// OBS-1084: an authoring park whose newest gate row is a red tool gate is a runner report (a load
|
|
92
109
|
// flake in an unowned suite), so the battery re-runs on the preserved ref with no worker.
|
|
93
110
|
if (park.kind === "authoring" && isToolGate(park.failedGate))
|
|
@@ -137,13 +154,17 @@ export function approvalEnactment(token, run) {
|
|
|
137
154
|
}
|
|
138
155
|
/** The production command registered in COMMANDS; its returned bytes are what the CLI prints. */
|
|
139
156
|
export async function approve(argv, cwd = process.cwd()) {
|
|
140
|
-
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling, files } = parseArgs(argv);
|
|
157
|
+
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling, files, park: namedPark, gate: namedGate } = parseArgs(argv);
|
|
141
158
|
const decisions = [waive, uphold, recheck].filter(Boolean).length;
|
|
142
159
|
if (decisions > 1)
|
|
143
160
|
throw new Error("--waive, --uphold and --recheck are different decisions — pass one");
|
|
161
|
+
// OBS-1178: the decision binds at COMMAND time — to the named --park, else to the park (or, for a
|
|
162
|
+
// failed task's recheck, the failure) open now — and is revalidated under serialization below. A
|
|
163
|
+
// decision queued behind another approval can therefore never land on a park that opened meanwhile.
|
|
164
|
+
// Journal.open throws `no journal for <runId> at <dir>` on an unknown run — that IS the refusal.
|
|
165
|
+
const bound = namedPark ?? openDecision(Journal.open(cwd, runId), taskId);
|
|
144
166
|
const serialization = await acquireApprovalSerialization(cwd, runId);
|
|
145
167
|
try {
|
|
146
|
-
// Journal.open throws `no journal for <runId> at <dir>` on an unknown run — that IS the refusal.
|
|
147
168
|
const journal = Journal.open(cwd, runId);
|
|
148
169
|
const status = journal.replayStatuses().get(taskId);
|
|
149
170
|
if (status === undefined) {
|
|
@@ -153,13 +174,17 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
153
174
|
// integration base owns verified-able work; --recheck re-gates that tree with no worker. Any other
|
|
154
175
|
// verb on a failed task, or a recheck over nothing landed, is refused naming why.
|
|
155
176
|
if (status === "failed" && recheck) {
|
|
177
|
+
if (namedGate !== undefined)
|
|
178
|
+
throw new Error(`--gate names a parked failed gate; task ${taskId} is failed — bind its recheck with --park <failure token>`);
|
|
179
|
+
const { events: failedEvents, sourceIndexes: failedIndexes } = readJournalEvents(journal);
|
|
180
|
+
const failure = requireBound(runId, taskId, bound, newestFailure(failedEvents, taskId, failedIndexes), "failure");
|
|
156
181
|
const ahead = failedTaskCommitsAhead(cwd, journal, runId, taskId);
|
|
157
182
|
if (ahead.count === 0) {
|
|
158
183
|
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
184
|
}
|
|
160
185
|
journal.append("task-approved", taskId, {
|
|
161
186
|
by, ...(reason ? { reason } : {}), via: "cli", release: RECHECK_RELEASE,
|
|
162
|
-
recheckedRef: ahead.ref, commitsAhead: ahead.count,
|
|
187
|
+
recheckedRef: ahead.ref, commitsAhead: ahead.count, failure,
|
|
163
188
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
164
189
|
});
|
|
165
190
|
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);
|
|
@@ -177,7 +202,12 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
177
202
|
const gateFailPark = park?.kind === "gate-fail";
|
|
178
203
|
const infraPark = park?.kind === "infra" || park?.kind === "diff-cap";
|
|
179
204
|
const authoringRunnerPark = park?.kind === "authoring" && isToolGate(park.failedGate);
|
|
205
|
+
const reapFailurePark = park?.kind === "stall" && park.reapFailure !== undefined;
|
|
180
206
|
const failedGate = gateFailPark || authoringRunnerPark ? park?.failedGate : undefined;
|
|
207
|
+
const parkBinding = requireBound(runId, taskId, bound, park?.ts === undefined ? undefined : { line: park.line, ts: park.ts }, "park");
|
|
208
|
+
if (namedGate !== undefined && namedGate !== park?.failedGate) {
|
|
209
|
+
throw new Error(`refusing mismatched gate for ${taskId}: --gate ${namedGate} but park ${bindingToken(parkBinding)} failed ${park?.failedGate ?? "no gate"} — nothing appended`);
|
|
210
|
+
}
|
|
181
211
|
if ((park?.kind === "scope-request" && !decisions) || files !== undefined) {
|
|
182
212
|
if (park?.kind !== "scope-request")
|
|
183
213
|
throw new Error("--files requires a scope-request park");
|
|
@@ -209,7 +239,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
209
239
|
+ `remedy: close the run, split the task in its spec, recompile, then \`tickmarkr resume ${runId} --graph-changed\``);
|
|
210
240
|
}
|
|
211
241
|
journal.append("task-approved", taskId, {
|
|
212
|
-
by, ...(reason ? { reason } : {}), via: "cli", release: "scope-request",
|
|
242
|
+
by, ...(reason ? { reason } : {}), via: "cli", release: "scope-request", park: parkBinding,
|
|
213
243
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }), // OBS-1083
|
|
214
244
|
amendment: { from, to: graphDefinitionHash(amended), beforeFiles: task.files, files: amendedFiles, parkLine: park.line, definition: taskDefinitionFingerprint(task) },
|
|
215
245
|
});
|
|
@@ -234,12 +264,16 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
234
264
|
via: "cli",
|
|
235
265
|
release: REVIEW_UPHELD_RELEASE,
|
|
236
266
|
gate: "review",
|
|
267
|
+
park: parkBinding,
|
|
237
268
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
238
269
|
});
|
|
239
270
|
return disposition(cwd, runId, "fund-fixed-attempt", `upheld the reviewer for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
240
271
|
}
|
|
241
272
|
if (recheck) {
|
|
242
|
-
if (
|
|
273
|
+
if (park?.kind === "stall" && !reapFailurePark) {
|
|
274
|
+
throw new Error(`--recheck applies to a stall park only when it recorded a reapFailure (census recovery); ${taskId}'s stall park recorded none — plain approve dispatches a worker; refusing`);
|
|
275
|
+
}
|
|
276
|
+
if ((!gateFailPark || !failedGate) && !infraPark && !authoringRunnerPark && !reapFailurePark) {
|
|
243
277
|
throw new Error(`--recheck applies to a gate-fail, infra, diff-cap or red-tool-gate authoring park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
244
278
|
}
|
|
245
279
|
journal.append("task-approved", taskId, {
|
|
@@ -247,13 +281,16 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
247
281
|
...(reason ? { reason } : {}),
|
|
248
282
|
via: "cli",
|
|
249
283
|
release: RECHECK_RELEASE,
|
|
284
|
+
park: parkBinding,
|
|
250
285
|
// OBS-1084: failedGate names the runner report for every recheck. Authoring rechecks additionally
|
|
251
286
|
// carry gate so their durable release row identifies the red tool gate that made recheck admissible.
|
|
252
287
|
...(failedGate ? { failedGate } : {}),
|
|
253
288
|
...(authoringRunnerPark && failedGate ? { gate: failedGate } : {}),
|
|
289
|
+
// OBS-1202: the daemon re-verifies the parked attempt's owned census before any gate runs.
|
|
290
|
+
...(reapFailurePark ? { reapFailure: park.reapFailure } : {}),
|
|
254
291
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
255
292
|
});
|
|
256
|
-
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` : `${park.kind} park`}; no gate marked satisfied`, serialization.contended);
|
|
293
|
+
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` : reapFailurePark ? "reapFailure stall park; the owned census is re-verified before the gates" : `${park.kind} park`}; no gate marked satisfied`, serialization.contended);
|
|
257
294
|
}
|
|
258
295
|
if (waive) {
|
|
259
296
|
if (!gateFailPark || !failedGate) {
|
|
@@ -265,6 +302,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
265
302
|
via: "cli",
|
|
266
303
|
release: GATE_SATISFIED_RELEASE,
|
|
267
304
|
gate: failedGate,
|
|
305
|
+
park: parkBinding,
|
|
268
306
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
269
307
|
});
|
|
270
308
|
return disposition(cwd, runId, "waive-gate", `waived failed gate ${failedGate} for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
@@ -292,6 +330,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
292
330
|
via: "cli",
|
|
293
331
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
294
332
|
...(capPark ? { release: ATTEMPT_CAP_RELEASE } : {}),
|
|
333
|
+
park: parkBinding,
|
|
295
334
|
});
|
|
296
335
|
const token = capPark ? "fresh-budget" : "dispatch";
|
|
297
336
|
return disposition(cwd, runId, token, `approved ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
@@ -300,6 +339,23 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
300
339
|
serialization.release();
|
|
301
340
|
}
|
|
302
341
|
}
|
|
342
|
+
/** The row a decision on this task answers right now: its newest failure when failed, else its newest park. */
|
|
343
|
+
function openDecision(journal, taskId) {
|
|
344
|
+
const { events, sourceIndexes } = readJournalEvents(journal);
|
|
345
|
+
if (journal.replayStatuses().get(taskId) === "failed")
|
|
346
|
+
return newestFailure(events, taskId, sourceIndexes);
|
|
347
|
+
const park = newestPark(events, taskId, sourceIndexes);
|
|
348
|
+
return park?.ts === undefined ? undefined : { line: park.line, ts: park.ts };
|
|
349
|
+
}
|
|
350
|
+
/** OBS-1178: under serialization, the decision's command-time binding must still name the open row. */
|
|
351
|
+
function requireBound(runId, taskId, bound, open, what) {
|
|
352
|
+
if (open === undefined)
|
|
353
|
+
throw new Error(`task ${taskId} has no ${what} row with a timestamp to bind this decision to — refusing an unbound release`);
|
|
354
|
+
if (bound === undefined || bound.line !== open.line || bound.ts !== open.ts) {
|
|
355
|
+
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`);
|
|
356
|
+
}
|
|
357
|
+
return open;
|
|
358
|
+
}
|
|
303
359
|
/**
|
|
304
360
|
* OBS-1022: what a failed task has landed. A preserved snapshot is authoritative only when it contains
|
|
305
361
|
* the current task-branch tip; an older preservation can remain in the journal after a later retry has
|
|
@@ -365,12 +421,14 @@ function disposition(cwd, runId, token, message, contended) {
|
|
|
365
421
|
};
|
|
366
422
|
return `${out}\nTICKMARKR_APPROVAL ${JSON.stringify(record)}`;
|
|
367
423
|
}
|
|
368
|
-
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--files <glob,…>] [--waive|--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
424
|
+
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
425
|
// hand-parsed argv — no CLI framework (house style). Positionals are runId then taskId; decision,
|
|
370
426
|
// ceiling, actor and reason are flags. Throws usage on missing positionals (mirrors resume.ts/unlock.ts).
|
|
371
427
|
function parseArgs(argv) {
|
|
372
428
|
const positionals = [];
|
|
373
429
|
let files;
|
|
430
|
+
let park;
|
|
431
|
+
let gate;
|
|
374
432
|
let by;
|
|
375
433
|
let reason;
|
|
376
434
|
let waive = false;
|
|
@@ -388,6 +446,17 @@ function parseArgs(argv) {
|
|
|
388
446
|
throw new Error("--files requires non-empty repository-relative globs");
|
|
389
447
|
}
|
|
390
448
|
}
|
|
449
|
+
else if (a === "--park") {
|
|
450
|
+
const value = argv[++i];
|
|
451
|
+
park = value === undefined ? undefined : parseBindingToken(value);
|
|
452
|
+
if (!park)
|
|
453
|
+
throw new Error("--park requires the <line>@<ts> token `tickmarkr status` prints for the park");
|
|
454
|
+
}
|
|
455
|
+
else if (a === "--gate") {
|
|
456
|
+
gate = argv[++i];
|
|
457
|
+
if (!gate || !GATE_NAMES.includes(gate))
|
|
458
|
+
throw new Error(`--gate requires one of ${GATE_NAMES.join(", ")}`);
|
|
459
|
+
}
|
|
391
460
|
else if (a === "--by") {
|
|
392
461
|
by = argv[++i];
|
|
393
462
|
if (!by)
|
|
@@ -424,19 +493,5 @@ function parseArgs(argv) {
|
|
|
424
493
|
if (!runId || !taskId) {
|
|
425
494
|
throw new Error(USAGE);
|
|
426
495
|
}
|
|
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;
|
|
496
|
+
return { runId, taskId, by: by ?? userInfo().username, reason, waive, uphold, recheck, reviewRoundCeiling, files, park, gate };
|
|
442
497
|
}
|
|
@@ -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
|
}>;
|
|
@@ -172,6 +173,8 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
172
173
|
tipTest?: string | undefined;
|
|
173
174
|
lint?: string | undefined;
|
|
174
175
|
diffCap?: number | undefined;
|
|
176
|
+
evidenceQuotaBytes?: number | undefined;
|
|
177
|
+
repairSelection?: boolean | undefined;
|
|
175
178
|
byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
|
|
176
179
|
[x: string]: unknown;
|
|
177
180
|
acceptance?: boolean | undefined;
|
|
@@ -205,8 +208,8 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
205
208
|
};
|
|
206
209
|
executionPolicy?: {
|
|
207
210
|
boundedInfrastructure: boolean;
|
|
208
|
-
repairSelection: boolean;
|
|
209
211
|
taskExecutionLimitMs: number;
|
|
212
|
+
repairSelection?: boolean | undefined;
|
|
210
213
|
} | undefined;
|
|
211
214
|
setup?: string | undefined;
|
|
212
215
|
cost?: {
|
|
@@ -286,6 +289,7 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
286
289
|
modelOverrides?: Record<string, {
|
|
287
290
|
vendor?: string | undefined;
|
|
288
291
|
channel?: "sub" | "api" | undefined;
|
|
292
|
+
effort?: "low" | "medium" | "high" | undefined;
|
|
289
293
|
}> | undefined;
|
|
290
294
|
windows?: Record<string, number> | undefined;
|
|
291
295
|
}>;
|
|
@@ -296,6 +300,8 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
296
300
|
tipTest?: string | undefined;
|
|
297
301
|
lint?: string | undefined;
|
|
298
302
|
diffCap?: number | undefined;
|
|
303
|
+
evidenceQuotaBytes?: number | undefined;
|
|
304
|
+
repairSelection?: boolean | undefined;
|
|
299
305
|
byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
|
|
300
306
|
[x: string]: unknown;
|
|
301
307
|
acceptance?: boolean | undefined;
|
|
@@ -329,8 +335,8 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
329
335
|
};
|
|
330
336
|
executionPolicy?: {
|
|
331
337
|
boundedInfrastructure: boolean;
|
|
332
|
-
repairSelection: boolean;
|
|
333
338
|
taskExecutionLimitMs: number;
|
|
339
|
+
repairSelection?: boolean | undefined;
|
|
334
340
|
} | undefined;
|
|
335
341
|
setup?: string | undefined;
|
|
336
342
|
cost?: {
|
|
@@ -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 { ConfigError, 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";
|
|
@@ -346,8 +346,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
346
346
|
const channels = discoverChannels(cfg, adapters, health);
|
|
347
347
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
348
348
|
const stagedDenyOf = (editable) => Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), editable[scope.key] ?? []]));
|
|
349
|
+
// OBS-1188: previews stage no effort, so both sides drop efforts — the writer then needs no lower layers
|
|
350
|
+
const previewInitial = { ...structuredClone(initial), efforts: undefined };
|
|
349
351
|
const stagedEditable = (map, deny) => ({
|
|
350
|
-
...structuredClone(
|
|
352
|
+
...structuredClone(previewInitial),
|
|
351
353
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, deny[stagedDenyKeyOf(scope)]])),
|
|
352
354
|
// OBS-1046: the allow complement is staged beside the deny lists, never folded into them
|
|
353
355
|
allowOut: deny.allowOut ?? initial.allowOut,
|
|
@@ -362,7 +364,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
362
364
|
let preview;
|
|
363
365
|
try {
|
|
364
366
|
const bytes = renderFleetOverlayWrite(currentRepoOverlayText(cwd), {
|
|
365
|
-
initial,
|
|
367
|
+
initial: previewInitial,
|
|
366
368
|
edited: stagedEditable(map, deny),
|
|
367
369
|
universe,
|
|
368
370
|
...(mode !== rm.mode.mode ? { mode } : {}),
|
|
@@ -581,7 +583,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
581
583
|
captions.push(`task hint: routing.map.${shape}.prefer ranks ${entry.prefer.join(", ")} — not manageable here, edit it in Shapes`);
|
|
582
584
|
}
|
|
583
585
|
// 8. deny∩prefer collision — a standing lint on this shape's declaration, never a fleet toggle.
|
|
584
|
-
for (const collision of denyPreferCollisions(cfgPreview, [shape])) {
|
|
586
|
+
for (const collision of denyPreferCollisions(cfgPreview, [shape], health)) {
|
|
585
587
|
captions.push(denyPreferCollisionLine(collision));
|
|
586
588
|
}
|
|
587
589
|
// 9. first-match provenance — the per-channel ledger below lists EVERY collector scope.
|
|
@@ -594,7 +596,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
594
596
|
const channelLedger = (shape, map, cfgPreview, offered) => {
|
|
595
597
|
const entry = map[shape];
|
|
596
598
|
const floor = cfgPreview.routing.floors[shape];
|
|
597
|
-
const collisions = denyPreferCollisions(cfgPreview, [shape]);
|
|
599
|
+
const collisions = denyPreferCollisions(cfgPreview, [shape], health);
|
|
598
600
|
const discovered = new Set(previewChannels.map((c) => `${c.adapter}:${c.model}`));
|
|
599
601
|
// T2 review (D-225): discovery already dropped a channel whose model probe failed — it is
|
|
600
602
|
// rebuilt as a structured channel from its adapter's own declaration and walks the SAME
|
|
@@ -616,7 +618,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
616
618
|
// never greys beside itself; its pin/pool/floor/prefer captions stay in excludedNote
|
|
617
619
|
const ledgerChannels = [...previewChannels.map((c) => ({ ...c, authReason: undefined })), ...failedProbes, ...unauthedCliChannels]
|
|
618
620
|
.filter((c) => !offered.has(`${c.adapter}:${c.model}`));
|
|
619
|
-
|
|
621
|
+
const lines = ledgerChannels.flatMap((c) => {
|
|
620
622
|
const key = `${c.adapter}:${c.model}`;
|
|
621
623
|
const reasons = exclusionCollector(c, cfgPreview.routing, "worker").map(exclusionReason);
|
|
622
624
|
// OBS-1065: a greyed picker row carries its reach beside its reasons, like the models view
|
|
@@ -645,8 +647,19 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
645
647
|
if (c.authReason)
|
|
646
648
|
reasons.push(c.authReason);
|
|
647
649
|
// 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("; ")}`] : [];
|
|
650
|
+
return reasons.length ? [{ key, line: `${c.adapter}/${c.model} — reach: ${reach} — ${reasons.join("; ")}` }] : [];
|
|
649
651
|
});
|
|
652
|
+
// OBS-1145 add.1: every carried pool member the picker does not offer leads the ledger in chain
|
|
653
|
+
// order — one no reason above names still gets its own row — so the picker renders its ordinal
|
|
654
|
+
// and Space can drop it; a chain member with no row is hidden AND irremovable
|
|
655
|
+
const carried = entry?.pool?.channels.filter((key) => !offered.has(key)) ?? [];
|
|
656
|
+
const named = new Set(lines.map((row) => row.key));
|
|
657
|
+
const unnamed = carried.filter((key) => !named.has(key)).map((key) => ({
|
|
658
|
+
key,
|
|
659
|
+
line: `${key.replace(":", "/")} — ${discovered.has(key) ? `not offered for ${shape}` : "not served by an installed adapter"} — carried in routing.map.${shape}.pool`,
|
|
660
|
+
}));
|
|
661
|
+
const order = (key) => (carried.includes(key) ? carried.indexOf(key) : carried.length);
|
|
662
|
+
return [...lines, ...unnamed].sort((a, b) => order(a.key) - order(b.key)).map((row) => row.line);
|
|
650
663
|
};
|
|
651
664
|
const candidatesForShape = (shape, mode, map, deny) => {
|
|
652
665
|
const preview = previewCfg(mode, map, deny);
|
|
@@ -677,6 +690,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
677
690
|
review: cfg.review.prefer?.slice(),
|
|
678
691
|
consult: cfg.consult.prefer?.slice(),
|
|
679
692
|
};
|
|
693
|
+
// OBS-1052(3): shown on the Steering view beside review.prefer; the pickers above stay untouched.
|
|
694
|
+
const reviewAdvisories = reviewNoVerdictRows(readReviewNoVerdictHistory(cwd))
|
|
695
|
+
.filter((row) => row.verdict === "warn")
|
|
696
|
+
.map((row) => `review history: ${row.channel} — ${row.value}`);
|
|
680
697
|
const steeringOptionsFor = (which, current) => {
|
|
681
698
|
const discovered = which === "review" ? [...reviewAdapters, ...seats] : seats;
|
|
682
699
|
return [...discovered, ...current.filter((entry) => !discovered.includes(entry))];
|
|
@@ -685,6 +702,26 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
685
702
|
// discovered seats universe plus a keep-default row, and a write is staged only on a real change.
|
|
686
703
|
const initialJudge = `${cfg.judge.adapter}:${cfg.judge.model}`;
|
|
687
704
|
let pendingWrite = null;
|
|
705
|
+
// OBS-1182: the layers under the repo overlay, read raw, so clearing an effort masks an inherited
|
|
706
|
+
// one even when those layers only validate beside repo fields. OBS-1188: re-read at every review
|
|
707
|
+
// and save, never an assembly-time snapshot; an unreadable read keeps the last good one for the
|
|
708
|
+
// preview only — the save guard below refuses it.
|
|
709
|
+
let lowerOverrides = {};
|
|
710
|
+
// OBS-1188: the reviewed bytes are the only bytes that may land. Re-rendered from the repo overlay
|
|
711
|
+
// and lower layers as they are NOW; any difference is a stale preview. Null = still current.
|
|
712
|
+
const stalePreview = (write, reviewed, prior = currentRepoOverlayText(cwd)) => {
|
|
713
|
+
const lower = lowerLayerModelOverrides({ globalDir });
|
|
714
|
+
if (!lower.ok)
|
|
715
|
+
return lower.error;
|
|
716
|
+
try {
|
|
717
|
+
if (renderFleetOverlayWrite(prior, { ...write, lowerOverrides: lower.overrides }) === reviewed)
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
return error.message;
|
|
722
|
+
}
|
|
723
|
+
return "stale preview — the repo overlay or a lower config layer changed after the review diff was rendered; press w to review what would be written now";
|
|
724
|
+
};
|
|
688
725
|
const reviewOverlay = (state) => {
|
|
689
726
|
const staged = structuredClone(initial);
|
|
690
727
|
// OBS-994/FL-1, OBS-1099 add.1: every schema-enumerated deny scope rides the same review/write funnel
|
|
@@ -692,6 +729,12 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
692
729
|
staged[scope.key] = state[scope.key];
|
|
693
730
|
staged.allowOut = state.allowOut ?? initial.allowOut;
|
|
694
731
|
staged.map = state.map;
|
|
732
|
+
// OBS-1182: staged efforts replace the loaded ones; none staged anywhere ⇒ no key, as loaded
|
|
733
|
+
const efforts = Object.fromEntries(Object.entries(state.efforts ?? {}).filter(([, models]) => Object.keys(models).length));
|
|
734
|
+
if (Object.keys(efforts).length)
|
|
735
|
+
staged.efforts = efforts;
|
|
736
|
+
else
|
|
737
|
+
delete staged.efforts;
|
|
695
738
|
const today = new Date().toISOString().slice(0, 10);
|
|
696
739
|
for (const classification of state.classifications) {
|
|
697
740
|
staged.tiers[classification.adapter] ??= {};
|
|
@@ -713,10 +756,14 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
713
756
|
pendingWrite = null;
|
|
714
757
|
return { kind: "empty" };
|
|
715
758
|
}
|
|
759
|
+
const lower = lowerLayerModelOverrides({ globalDir });
|
|
760
|
+
if (lower.ok)
|
|
761
|
+
lowerOverrides = lower.overrides;
|
|
716
762
|
const write = {
|
|
717
763
|
initial,
|
|
718
764
|
edited: staged,
|
|
719
765
|
universe,
|
|
766
|
+
lowerOverrides,
|
|
720
767
|
...(modeChanged ? { mode: state.selectedMode } : {}),
|
|
721
768
|
...(judgeChanged && judgeSeat ? { judge: judgeSeat } : {}),
|
|
722
769
|
steering: { initial: initialSteering, edited: state.steering },
|
|
@@ -727,22 +774,37 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
727
774
|
return { kind: "empty" };
|
|
728
775
|
}
|
|
729
776
|
pendingWrite = write;
|
|
777
|
+
// OBS-1144: the review names every dead pool entry the written config carries, with its reason and
|
|
778
|
+
// what still routes — before y. Unloadable bytes carry no notes: the reload guard refuses them anyway.
|
|
779
|
+
let poolNotes = [];
|
|
780
|
+
try {
|
|
781
|
+
const written = loadConfigWithMode(cwd, { globalDir, repoOverlayText: after }).cfg;
|
|
782
|
+
poolNotes = deadPoolEntries(written, undefined, health).map(deadPoolEntryLine);
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
poolNotes = [];
|
|
786
|
+
}
|
|
787
|
+
const notes = [...(lower.ok ? [] : [`${lower.error} — y will be refused`]), ...poolNotes];
|
|
730
788
|
return {
|
|
731
789
|
kind: "diff",
|
|
732
790
|
before,
|
|
733
791
|
after,
|
|
734
792
|
diff: unifiedYamlDiff(before, after, path),
|
|
735
793
|
path,
|
|
794
|
+
...(notes.length ? { notes } : {}),
|
|
736
795
|
};
|
|
737
796
|
};
|
|
738
|
-
const
|
|
797
|
+
const loaderGuard = io.reloadGuard
|
|
739
798
|
?? ((bytes) => overlayBytesLoadError(cwd, bytes, { globalDir }));
|
|
799
|
+
// OBS-1188: a stale preview or an unreadable lower layer answers on the same error channel as the loader
|
|
800
|
+
const reloadGuard = (bytes) => (pendingWrite && stalePreview(pendingWrite, bytes)) || loaderGuard(bytes);
|
|
740
801
|
const props = {
|
|
741
802
|
ageMs: doctorAgeMs(cwd),
|
|
742
803
|
adapters,
|
|
743
804
|
health,
|
|
744
805
|
...Object.fromEntries(DENY_SCOPES.map((scope) => [initialDenyPropOf(scope), editable[scope.key] ?? []])),
|
|
745
806
|
initialAllowOut: editable.allowOut,
|
|
807
|
+
initialEfforts: editable.efforts,
|
|
746
808
|
modelGroups,
|
|
747
809
|
initialMode: rm.mode.mode,
|
|
748
810
|
modeOptions: ROUTING_MODES.map((mode) => ({ id: mode, gloss: MODE_GLOSS[mode] })),
|
|
@@ -756,6 +818,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
756
818
|
],
|
|
757
819
|
initialSteering,
|
|
758
820
|
steeringOptionsFor,
|
|
821
|
+
reviewAdvisories,
|
|
759
822
|
reviewOverlay,
|
|
760
823
|
reloadGuard,
|
|
761
824
|
stagedRouting,
|
|
@@ -782,7 +845,20 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
782
845
|
const write = pendingWrite;
|
|
783
846
|
if (!write)
|
|
784
847
|
throw new Error("fleet write reached confirmation without a staged overlay mutation");
|
|
785
|
-
|
|
848
|
+
// OBS-1188: re-checked against the bytes on disk at write time; a refusal throws before the temp file exists
|
|
849
|
+
try {
|
|
850
|
+
writeFleetOverlay(result.review.path, (prior) => {
|
|
851
|
+
const stale = stalePreview(write, result.review.after, prior);
|
|
852
|
+
if (stale !== null)
|
|
853
|
+
throw new ConfigError(stale);
|
|
854
|
+
return result.review.after;
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
if (error instanceof ConfigError)
|
|
859
|
+
return `fleet: nothing written — ${error.message}`;
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
786
862
|
// OBS-529: a freshly classified model has no probe verdict (doctor probes CONFIGURED models,
|
|
787
863
|
// and it was not configured at probe time), so it stays unroutable — invisible in every
|
|
788
864
|
// picker — until the next probe. Name the step, or the classify flow reads as broken.
|