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
package/dist/route/router.js
CHANGED
|
@@ -44,6 +44,7 @@ export function marginalCostRank(c) {
|
|
|
44
44
|
}
|
|
45
45
|
const toAssignment = (c) => ({
|
|
46
46
|
adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier,
|
|
47
|
+
...(c.effort ? { effort: c.effort } : {}),
|
|
47
48
|
});
|
|
48
49
|
function resolvePin(pin, channels) {
|
|
49
50
|
const c = channels.find((c) => c.adapter === pin.via && c.model === pin.model);
|
|
@@ -64,6 +65,24 @@ export function rankPreferredChannels(channels, prefer = [], opts = {}) {
|
|
|
64
65
|
}
|
|
65
66
|
/** Review preserves its established tier-then-cost order inside a preference band. */
|
|
66
67
|
export const reviewPreferenceTieBreak = (a, b) => TIER_RANK[b.tier] - TIER_RANK[a.tier] || Number(a.channel === "api") - Number(b.channel === "api");
|
|
68
|
+
// OBS-1185: the floor route() resolves, shared with plan's display — a task hint at or above the
|
|
69
|
+
// configured (or mode-compiled) floor wins; otherwise the configured floor stands.
|
|
70
|
+
export function resolvedFloor(task, cfg) {
|
|
71
|
+
const taskFloor = task.routingHints?.floor;
|
|
72
|
+
const configFloor = cfg.routing.floors[task.shape];
|
|
73
|
+
if (taskFloor && (!configFloor || TIER_RANK[taskFloor] >= TIER_RANK[configFloor]))
|
|
74
|
+
return { tier: taskFloor, source: "task" };
|
|
75
|
+
return configFloor ? { tier: configFloor, source: "config" } : undefined;
|
|
76
|
+
}
|
|
77
|
+
// OBS-1187: a declared map pool is the shape's closed candidate set for EVERY dispatch — demotion,
|
|
78
|
+
// retry-ban failover, tier climb, recycling and resume draw inside it, never from the open fleet.
|
|
79
|
+
export const taskPool = (task, cfg) => cfg.routing.map[task.shape]?.pool;
|
|
80
|
+
/** Park-reason suffix naming the pool an exhausted re-dispatch stayed inside; empty when unpooled. */
|
|
81
|
+
export const poolExhaustion = (task, cfg) => {
|
|
82
|
+
const pool = taskPool(task, cfg);
|
|
83
|
+
return pool ? ` — routing.map.${task.shape}.pool (${pool.mode}: ${pool.channels.join(", ")}) is exhausted` : "";
|
|
84
|
+
};
|
|
85
|
+
export const inTaskPool = (task, cfg, key) => taskPool(task, cfg)?.channels.includes(key) ?? true;
|
|
67
86
|
function ladderFor(task, entry) {
|
|
68
87
|
const escalate = task.routingHints?.escalate ?? entry?.escalate ?? true;
|
|
69
88
|
return escalate ? ["retry", "escalate", "consult", "human"] : ["retry", "consult", "human"];
|
|
@@ -150,6 +169,13 @@ const maybeSlaLint = (lints, task, profile, slaMinutes, c) => {
|
|
|
150
169
|
lints.push(`${task.id} (${task.shape}): median ${medianMin}m exceeds sla ${slaMinutes}m — learned perf term references sla ${slaMinutes}m ref`);
|
|
151
170
|
};
|
|
152
171
|
export function route(task, cfg, channels, profile, preferCtx, exclude, exploreCtx) {
|
|
172
|
+
// OBS-1143: deny checks below read the probed identity the offered channels carry — the identity
|
|
173
|
+
// discovery admitted them under. A seat no channel vouches for stays unknown (conservative).
|
|
174
|
+
const observed = new Map(channels.flatMap((c) => (c.identity ? [[channelKey(c), c.identity]] : [])));
|
|
175
|
+
const seat = (adapter, model) => {
|
|
176
|
+
const identity = observed.get(channelKey({ adapter, model }));
|
|
177
|
+
return identity ? { adapter, model, identity } : { adapter, model };
|
|
178
|
+
};
|
|
153
179
|
channels = withoutExcluded(channels, exclude);
|
|
154
180
|
const lints = [];
|
|
155
181
|
const advisoryFloor = cfg.routing.floors[task.shape];
|
|
@@ -161,7 +187,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
161
187
|
const prefer = effectivePrefer(entry);
|
|
162
188
|
const prefActive = !!(cfg.routing.allow || cfg.routing.deny);
|
|
163
189
|
const disallowedPin = (via, model, kind) => {
|
|
164
|
-
const d = disallowedBy(
|
|
190
|
+
const d = disallowedBy(seat(via, model), cfg.routing);
|
|
165
191
|
if (d) {
|
|
166
192
|
throw new RoutingError(`${task.id}: ${kind} ${via}:${model} is disallowed by routing.${d.by} (${d.entry}) — remove the ${d.by} entry or re-pin to an allowed channel`);
|
|
167
193
|
}
|
|
@@ -174,8 +200,8 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
174
200
|
}
|
|
175
201
|
if (cfg.tiers[p]) {
|
|
176
202
|
const expanded = channelsFromConfig(p, cfg);
|
|
177
|
-
if (expanded.length && expanded.every((c) => disallowedBy(c, cfg.routing) !== null)) {
|
|
178
|
-
const d = disallowedBy(expanded[0], cfg.routing);
|
|
203
|
+
if (expanded.length && expanded.every((c) => disallowedBy(seat(c.adapter, c.model), cfg.routing) !== null)) {
|
|
204
|
+
const d = disallowedBy(seat(expanded[0].adapter, expanded[0].model), cfg.routing);
|
|
179
205
|
throw new RoutingError(`${task.id}: prefer entry ${p} is disallowed by routing.${d.by} (${d.entry}) — remove the ${d.by} entry or re-pin to an allowed channel`);
|
|
180
206
|
}
|
|
181
207
|
return;
|
|
@@ -191,9 +217,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
191
217
|
}
|
|
192
218
|
};
|
|
193
219
|
const taskFloor = task.routingHints?.floor;
|
|
194
|
-
const mapPinFloor =
|
|
195
|
-
? { tier: taskFloor, source: "task" }
|
|
196
|
-
: advisoryFloor ? { tier: advisoryFloor, source: "config" } : undefined;
|
|
220
|
+
const mapPinFloor = resolvedFloor(task, cfg);
|
|
197
221
|
const lintMapPinFloor = (tier, what = "pin") => {
|
|
198
222
|
if (mapPinFloor && TIER_RANK[tier] < TIER_RANK[mapPinFloor.tier]) {
|
|
199
223
|
lints.push(`${task.id} (${task.shape}): map ${what} routes ${tier}, below ${mapPinFloor.source} floor ${mapPinFloor.tier} — map ${what}s are supreme`);
|
|
@@ -228,21 +252,28 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
228
252
|
// single-channel pool) and the open floor/auto path. Exhaustion stays fail-loud (pin precedent).
|
|
229
253
|
if (entry?.pool) {
|
|
230
254
|
const pool = entry.pool;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
255
|
+
// OBS-1144: a disallowed entry is skipped, named in provenance, never fatal for the pool; only an
|
|
256
|
+
// empty admitted remainder refuses. Filtering keeps declaration order, so ordered stays ordered.
|
|
257
|
+
const skipped = [];
|
|
258
|
+
const admitted = pool.channels.filter((p) => {
|
|
259
|
+
if (!prefActive)
|
|
260
|
+
return true;
|
|
261
|
+
const i = p.indexOf(":");
|
|
262
|
+
const d = disallowedBy(seat(p.slice(0, i), p.slice(i + 1)), cfg.routing);
|
|
263
|
+
if (d)
|
|
264
|
+
skipped.push(`${p} (disallowed by routing.${d.by} (${d.entry}))`);
|
|
265
|
+
return d === null;
|
|
266
|
+
});
|
|
267
|
+
const skippedNote = skipped.length ? `; skipped ${skipped.join(", ")}` : "";
|
|
268
|
+
if (!admitted.length) {
|
|
269
|
+
throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool is exhausted — no admitted entry remains: ${skipped.join(", ")} — remove the deny/allow entry or add an admitted channel to the pool`);
|
|
239
270
|
}
|
|
240
271
|
// live preserves pool declaration order — ordered mode and any-mode stable-sort ties depend on it
|
|
241
|
-
const live =
|
|
272
|
+
const live = admitted
|
|
242
273
|
.map((id) => channels.find((c) => channelKey(c) === id))
|
|
243
274
|
.filter((c) => c !== undefined && (!taskFloor || TIER_RANK[c.tier] >= TIER_RANK[taskFloor]));
|
|
244
275
|
if (!live.length) {
|
|
245
|
-
throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel${taskFloor ? ` at task floor ${taskFloor}` : ""} — declared: ${pool.channels.join(", ")}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
|
|
276
|
+
throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel${taskFloor ? ` at task floor ${taskFloor}` : ""} — declared: ${pool.channels.join(", ")}${skippedNote}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
|
|
246
277
|
}
|
|
247
278
|
// No learned/explore inside pools: any spreads deterministically; ordered keeps declaration order.
|
|
248
279
|
const chosen = pool.mode === "ordered"
|
|
@@ -250,7 +281,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
250
281
|
: rankAnyPool(live, task)[0];
|
|
251
282
|
lintMapPinFloor(chosen.tier, "pool");
|
|
252
283
|
maybeSlaLint(lints, task, profile, slaMinutes, chosen);
|
|
253
|
-
return { assignment: toAssignment(chosen), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pool ${pool.mode} ${channelKey(chosen)} (config routing.map)` };
|
|
284
|
+
return { assignment: toAssignment(chosen), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pool ${pool.mode} ${channelKey(chosen)} (config routing.map${skippedNote})` };
|
|
254
285
|
}
|
|
255
286
|
const baseTier = floor ?? "cheap";
|
|
256
287
|
// A task floor is hard on pool and floor/auto paths; the map-pin branch above stays supreme.
|
|
@@ -268,7 +299,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
268
299
|
let msg = `${task.id}: no channel at tier>=${minTier}; available: ${channels.map(channelKey).join(", ") || "(none)"}`;
|
|
269
300
|
if (prefActive) {
|
|
270
301
|
const excluded = Object.keys(cfg.tiers).flatMap((id) => channelsFromConfig(id, cfg))
|
|
271
|
-
.filter((c) => disallowedBy(c, cfg.routing) !== null && TIER_RANK[c.tier] >= TIER_RANK[minTier])
|
|
302
|
+
.filter((c) => disallowedBy(seat(c.adapter, c.model), cfg.routing) !== null && TIER_RANK[c.tier] >= TIER_RANK[minTier])
|
|
272
303
|
.map(channelKey);
|
|
273
304
|
if (excluded.length)
|
|
274
305
|
msg += `; ${excluded.length} channel(s) excluded by routing.allow/deny: ${excluded.join(", ")}`;
|
|
@@ -349,8 +380,9 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
349
380
|
eligible = spreadStatic(sortedStatic);
|
|
350
381
|
spreadDecided = channelKey(eligible[0]) !== channelKey(sortedStatic[0]);
|
|
351
382
|
}
|
|
352
|
-
const
|
|
353
|
-
|
|
383
|
+
const resolved = resolvedFloor(task, cfg);
|
|
384
|
+
const bound = resolved?.source === "task" ? `floor ${resolved.tier} (task hint${src})` :
|
|
385
|
+
resolved ? `floor ${resolved.tier} (config floors)` :
|
|
354
386
|
"tier cheap (default)";
|
|
355
387
|
// name the key that actually broke the tie: prefer outranks the marginal-cost/tier keys, so if the
|
|
356
388
|
// winner matched a prefer entry, prefer decided it — not "cheapest sufficient tier" (ROUTE-03, WR-01)
|
|
@@ -361,8 +393,8 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
361
393
|
maybeSlaLint(lints, task, profile, slaMinutes, eligible[0]);
|
|
362
394
|
return { assignment: toAssignment(eligible[0]), ladder: ladderFor(task, entry), lints, provenance: `${degraded}${bound}, marginal-cost auto (${chosenBy})`, ...(deviation ? { deviation } : {}) };
|
|
363
395
|
}
|
|
364
|
-
function candidatePool(current, channels, tried, tierPredicate, exclude) {
|
|
365
|
-
channels = withoutExcluded(channels, exclude);
|
|
396
|
+
function candidatePool(current, task, cfg, channels, tried, tierPredicate, exclude) {
|
|
397
|
+
channels = withoutExcluded(channels, exclude).filter((c) => inTaskPool(task, cfg, channelKey(c)));
|
|
366
398
|
const triedKeys = new Set(tried);
|
|
367
399
|
const triedIdentities = new Set(tried.map((key) => {
|
|
368
400
|
const channel = channels.find((c) => channelKey(c) === key);
|
|
@@ -391,7 +423,7 @@ export function nextChannel(current, task, cfg, channels, tried, profile, exclud
|
|
|
391
423
|
// profile-dependent filter. NO exploration bonus here (route():110 has one; a probe on the
|
|
392
424
|
// failure path would spend a real retry). Absent profile ⇒ every score is 0 ⇒ third key
|
|
393
425
|
// all-ties ⇒ the stable sort preserves the exact v1.7 candidate ORDER.
|
|
394
|
-
const pool = candidatePool(current, channels, tried, (tier) => TIER_RANK[tier] >= TIER_RANK[current.tier], exclude);
|
|
426
|
+
const pool = candidatePool(current, task, cfg, channels, tried, (tier) => TIER_RANK[tier] >= TIER_RANK[current.tier], exclude);
|
|
395
427
|
const candidates = rankFailoverCandidates(pool, task, cfg, profile);
|
|
396
428
|
return candidates.length ? toAssignment(candidates[0]) : null;
|
|
397
429
|
}
|
|
@@ -434,7 +466,7 @@ export function climbChannel(current, task, cfg, channels, tried, profile, exclu
|
|
|
434
466
|
const pick = nextChannel(current, task, cfg, channels, tried, profile, exclude);
|
|
435
467
|
return pick ? makeClimbPick(pick, false, "no higher tier") : null;
|
|
436
468
|
}
|
|
437
|
-
const higherPool = candidatePool(current, channels, tried, (tier) => TIER_RANK[tier] > TIER_RANK[current.tier], exclude);
|
|
469
|
+
const higherPool = candidatePool(current, task, cfg, channels, tried, (tier) => TIER_RANK[tier] > TIER_RANK[current.tier], exclude);
|
|
438
470
|
if (higherPool.length > 0) {
|
|
439
471
|
const candidates = rankFailoverCandidates(higherPool, task, cfg, profile);
|
|
440
472
|
return makeClimbPick(toAssignment(candidates[0]), true);
|
package/dist/run/consult.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AuthHealth, type WorkerAdapter } from "../adapters/types.js";
|
|
2
2
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
3
|
+
import type { Effort } from "../graph/schema.js";
|
|
3
4
|
import type { ExecutorDriver, Slot } from "../drivers/types.js";
|
|
4
5
|
import type { GateResult } from "../gates/types.js";
|
|
5
6
|
import { type VerdictUnparseableCause } from "../gates/verdict-cause.js";
|
|
@@ -13,6 +14,16 @@ export interface ConsultVerdict {
|
|
|
13
14
|
adapter?: string;
|
|
14
15
|
model?: string;
|
|
15
16
|
vendor?: string;
|
|
17
|
+
effort?: Effort;
|
|
18
|
+
invocations?: ConsultInvocation[];
|
|
19
|
+
}
|
|
20
|
+
export interface ConsultInvocation {
|
|
21
|
+
adapter: string;
|
|
22
|
+
model: string;
|
|
23
|
+
vendor: string;
|
|
24
|
+
effort?: Effort;
|
|
25
|
+
outcome: "completed" | "failed";
|
|
26
|
+
reason: string;
|
|
16
27
|
}
|
|
17
28
|
export declare function renderRetryGuidance(v: ConsultVerdict): string;
|
|
18
29
|
export declare function augmentRetryBrief(feedback: string, opts: {
|
|
@@ -37,6 +48,7 @@ export declare function buildDossierPrompt(d: Dossier, nonce: string): string;
|
|
|
37
48
|
export declare function consult(d: Dossier, cfg: TickmarkrConfig, adapters: WorkerAdapter[], driver: ExecutorDriver, cwd: string, runDir: string, opts?: {
|
|
38
49
|
keep?: boolean;
|
|
39
50
|
onSlot?: (slot: Slot) => void;
|
|
51
|
+
onInvocation?: (inv: ConsultInvocation) => void;
|
|
40
52
|
runId?: string;
|
|
41
53
|
channels?: Array<{
|
|
42
54
|
adapter: string;
|
|
@@ -44,5 +56,7 @@ export declare function consult(d: Dossier, cfg: TickmarkrConfig, adapters: Work
|
|
|
44
56
|
vendor?: string;
|
|
45
57
|
channel?: "sub" | "api";
|
|
46
58
|
tier?: string;
|
|
59
|
+
identity?: string;
|
|
47
60
|
}>;
|
|
61
|
+
health?: Record<string, AuthHealth> | null;
|
|
48
62
|
}): Promise<ConsultVerdict>;
|
package/dist/run/consult.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAdapter } from "../adapters/registry.js";
|
|
4
|
+
import { configuredEffort } from "../adapters/types.js";
|
|
4
5
|
import { bannerShell, paneDispatchCommand } from "../brand.js";
|
|
5
6
|
import { dewrapPaneVerdict, extractVerdictJson, gateExitTrailer, gatePaneName, generateVerdictNonce, verdictNonceLine } from "../gates/llm.js";
|
|
6
7
|
import { classifyVerdictCause } from "../gates/verdict-cause.js";
|
|
7
8
|
import { rankPreferredChannels } from "../route/role-pick.js";
|
|
8
|
-
import { disallowedBy, routingModelProvider } from "../route/preference.js";
|
|
9
|
+
import { disallowedBy, observedSeat, routingModelProvider } from "../route/preference.js";
|
|
9
10
|
import { sh } from "./git.js";
|
|
10
11
|
import { redactSecrets } from "./redact.js";
|
|
11
12
|
import { filterLlmTranscript } from "./stall.js";
|
|
@@ -145,6 +146,9 @@ export async function consult(d, cfg, adapters, driver, cwd, runDir,
|
|
|
145
146
|
// via SlotOpts.owned; without it the legacy gatePaneName shape survives (non-daemon callers/tests).
|
|
146
147
|
// v1.54 T1: channels — the daemon's doctor-filtered live channel list; prefer-seat liveness is
|
|
147
148
|
// judged against it only (never rebuilt from config, which would select installed-but-unauthed seats).
|
|
149
|
+
// OBS-1182: onInvocation hears every launched seat — its identity, effort and outcome — failed seats
|
|
150
|
+
// included, so a seat that never produced the verdict still reaches the journal.
|
|
151
|
+
// OBS-1186: health — doctor's cached verdict, the observed identity of the pinned seat (absent ⇒ unknown, conservative).
|
|
148
152
|
opts = {}) {
|
|
149
153
|
const n = ++consultSeq;
|
|
150
154
|
const nonce = generateVerdictNonce();
|
|
@@ -157,11 +161,11 @@ opts = {}) {
|
|
|
157
161
|
// One seat = the WHOLE invoke-and-parse unit, both visibility branches (OBS-69 class: a headless-only
|
|
158
162
|
// failover would leave the production pane path hard-failing on seat one). A null verdict retains
|
|
159
163
|
// the classifier's cause when extraction failed; parsed content rejections deliberately have none.
|
|
160
|
-
const invokeSeat = async (seatAdapter, seatModel, seatIdx) => {
|
|
164
|
+
const invokeSeat = async (seatAdapter, seatModel, seatIdx, effort) => {
|
|
161
165
|
const adapter = getAdapter(seatAdapter, adapters);
|
|
162
166
|
let out;
|
|
163
167
|
if (cfg.visibility.llm === "headless") {
|
|
164
|
-
const r = await sh(adapter.headlessCommand(promptFile, seatModel), cwd, cfg.consult.stallMinutes * 60_000);
|
|
168
|
+
const r = await sh(adapter.headlessCommand(promptFile, seatModel, effort), cwd, cfg.consult.stallMinutes * 60_000);
|
|
165
169
|
out = r.stdout + r.stderr;
|
|
166
170
|
}
|
|
167
171
|
else {
|
|
@@ -169,7 +173,7 @@ opts = {}) {
|
|
|
169
173
|
// but never exits, so the exit trailer after it never runs, the wait runs to the stall cap and
|
|
170
174
|
// the 300-line read is a TUI frame. The headless command exits, the trailer prints, and the
|
|
171
175
|
// pane is harvested through dewrap exactly like a judge or review seat.
|
|
172
|
-
const command = adapter.headlessCommand(promptFile, seatModel);
|
|
176
|
+
const command = adapter.headlessCommand(promptFile, seatModel, effort);
|
|
173
177
|
// T8: role-first pane name for fleet visibility (consult · T2); consultSeq stays on the dossier artifact only
|
|
174
178
|
const slot = await driver.slot(cwd, gatePaneName("consult", d.taskId), {
|
|
175
179
|
label: `CONSULT ${d.taskId}`,
|
|
@@ -216,8 +220,9 @@ opts = {}) {
|
|
|
216
220
|
// Rank only exact live models from the daemon's role-scoped discovery pool.
|
|
217
221
|
// The explicit pin remains a caller-owned fallback, outside strict preference selection.
|
|
218
222
|
const live = (opts.channels ?? []).filter((c) => typeof c.model === "string");
|
|
223
|
+
// Ranking keeps each live channel's observed identity; the pin carries its cached identity too.
|
|
219
224
|
const seats = rankPreferredChannels(live.filter((c) => disallowedBy(c, cfg.routing, "consult") === null), cfg.consult.prefer);
|
|
220
|
-
seats.push(
|
|
225
|
+
seats.push(observedSeat(opts.health, cfg.consult.adapter, cfg.consult.model));
|
|
221
226
|
// v1.87 T2: a consult seat reads the code, so it passes through the operator's policy exactly like
|
|
222
227
|
// a worker does. The filter runs AFTER the pin is pushed, so the final pinned seat is checked by
|
|
223
228
|
// the same rule as every prefer entry — and disallowedBy carries the full deny grammar (adapter,
|
|
@@ -242,8 +247,12 @@ opts = {}) {
|
|
|
242
247
|
};
|
|
243
248
|
}
|
|
244
249
|
for (const [i, seat] of allowedSeats.entries()) {
|
|
250
|
+
// OBS-1182: each seat launches at its own configured effort, never the worker's.
|
|
251
|
+
const effort = configuredEffort(cfg, seat);
|
|
252
|
+
const record = (outcome, reason) => opts.onInvocation?.({ ...seatIdentity(seat), ...(effort ? { effort } : {}), outcome, reason });
|
|
245
253
|
try {
|
|
246
|
-
const parsed = await invokeSeat(seat.adapter, seat.model, i);
|
|
254
|
+
const parsed = await invokeSeat(seat.adapter, seat.model, i, effort);
|
|
255
|
+
record(parsed.verdict ? "completed" : "failed", parsed.verdict ? "verdict" : `verdict unparseable${parsed.cause ? ` (${parsed.cause})` : ""}`);
|
|
247
256
|
if (parsed.verdict) {
|
|
248
257
|
const excludeProvider = parsed.verdict.excludeAdapter
|
|
249
258
|
? excludedProviderFromDossier(d, parsed.verdict.excludeAdapter)
|
|
@@ -255,11 +264,13 @@ opts = {}) {
|
|
|
255
264
|
notes: `${parsed.verdict.notes} — excluded provider ${excludeProvider}`,
|
|
256
265
|
} : {}),
|
|
257
266
|
...seatIdentity(seat),
|
|
267
|
+
...(effort ? { effort } : {}),
|
|
258
268
|
};
|
|
259
269
|
}
|
|
260
270
|
}
|
|
261
|
-
catch {
|
|
271
|
+
catch (e) {
|
|
262
272
|
// failed seat (unknown adapter, dead driver/pane, shell error) — fall to the next entry
|
|
273
|
+
record("failed", `seat error: ${e instanceof Error ? e.message : String(e)}`.slice(0, 200));
|
|
263
274
|
}
|
|
264
275
|
}
|
|
265
276
|
return {
|
package/dist/run/daemon.d.ts
CHANGED
|
@@ -2,12 +2,14 @@ import { type WorkerAdapter } from "../adapters/types.js";
|
|
|
2
2
|
import { type ModeResolution, type RoutingMode, type TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import { type DriverChoice } from "../drivers/index.js";
|
|
4
4
|
import { type ExecutorDriver, type Slot } from "../drivers/types.js";
|
|
5
|
-
import { type Baseline } from "../gates/baseline.js";
|
|
5
|
+
import { type Baseline, type BaselineProvenance, type GateEvidenceOptions } from "../gates/baseline.js";
|
|
6
6
|
import type { GateResult } from "../gates/types.js";
|
|
7
7
|
import { type RunGraph } from "../graph/schema.js";
|
|
8
8
|
import { type PreserveProducer } from "./git.js";
|
|
9
9
|
import { Journal, type JournalEvent } from "./journal.js";
|
|
10
10
|
export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
|
|
11
|
+
export declare function setContactUnreadableDeadlineMsForTests(ms: number): void;
|
|
12
|
+
export declare function resetContactUnreadableDeadlineMsForTests(): void;
|
|
11
13
|
/** Optional authoritative transport receipt. Screen text can prove execution, never nonacceptance. */
|
|
12
14
|
export interface DispatchObservation {
|
|
13
15
|
slotId: string;
|
|
@@ -67,7 +69,34 @@ export interface RunSummary {
|
|
|
67
69
|
approvalDisposition?: "complete" | "outstanding";
|
|
68
70
|
/** the accepted approvals that never reached a dispatch — named, never left to the park buckets */
|
|
69
71
|
outstandingApprovals?: string[];
|
|
72
|
+
/** OBS-1123: every baseline-recorded red a standing green carried, with the capture that recorded it */
|
|
73
|
+
forgiven?: ForgivenFingerprints[];
|
|
70
74
|
}
|
|
75
|
+
/** OBS-1123: one forgiveness — the fingerprints a green carried and the capture that recorded them. */
|
|
76
|
+
export interface ForgivenFingerprints {
|
|
77
|
+
/** the task whose gate forgave them; absent for the integration tip */
|
|
78
|
+
taskId?: string;
|
|
79
|
+
gate: string;
|
|
80
|
+
/** absent: a legacy row that named no fingerprints */
|
|
81
|
+
fingerprints?: string[];
|
|
82
|
+
/** absent: a legacy capture or row that recorded no provenance — rendered unknown, never dated */
|
|
83
|
+
baseline?: BaselineProvenance;
|
|
84
|
+
}
|
|
85
|
+
/** Journal rows are untrusted input: a malformed field reads as not recorded, never as a value. */
|
|
86
|
+
export declare const fingerprintsOf: (v: unknown) => string[] | undefined;
|
|
87
|
+
export declare const baselineProvenanceOf: (v: unknown) => BaselineProvenance | undefined;
|
|
88
|
+
/** A forgiven battery green: the structured field, or the battery's own "(forgiven)" wording on a legacy row. */
|
|
89
|
+
export declare const forgivenGateRow: (data: Record<string, unknown>) => boolean;
|
|
90
|
+
/**
|
|
91
|
+
* OBS-1123: the forgiveness standing at this point in the journal — each task gate's LATEST verdict
|
|
92
|
+
* where it is a forgiven green, then the latest verification cycle's forgiven tip rows. A legacy row
|
|
93
|
+
* keeps what it never recorded absent, so the reader says "unknown" instead of borrowing a date.
|
|
94
|
+
*/
|
|
95
|
+
export declare function forgivenFingerprints(events: readonly JournalEvent[]): ForgivenFingerprints[];
|
|
96
|
+
/** The capture a forgiveness rests on, or an explicit unknown — never a time read off another clock. */
|
|
97
|
+
export declare function formatBaselineProvenance(p: BaselineProvenance | undefined): string;
|
|
98
|
+
export declare function formatFingerprints(fingerprints: string[] | undefined): string;
|
|
99
|
+
export declare function formatForgiven(f: ForgivenFingerprints): string;
|
|
71
100
|
export declare function pendingDaemonApprovalActions(events: JournalEvent[]): Map<string, import("./journal.js").PendingApprovalAction>;
|
|
72
101
|
/**
|
|
73
102
|
* T14, amended by v2.2 T3: approvals the run accepted and never acted on. `approved` above is still
|
|
@@ -131,6 +160,9 @@ export declare const resetApprovalWindowForTests: () => void;
|
|
|
131
160
|
/** Test seam — shrink the capacity backoff without minute-long sleeps. */
|
|
132
161
|
export declare function setCapacityBackoffMsForTests(ms: number): void;
|
|
133
162
|
export declare function resetCapacityBackoffMsForTests(): void;
|
|
163
|
+
/** Test seam — shrink the bootstrap backoff. */
|
|
164
|
+
export declare function setBootstrapBackoffMsForTests(ms: number): void;
|
|
165
|
+
export declare function resetBootstrapBackoffMsForTests(): void;
|
|
134
166
|
export declare const EARLY_LAUNCH_LIVENESS_MS = 60000;
|
|
135
167
|
/** Test seam — lowers the empty-pane liveness window without sleeping 60s per case. */
|
|
136
168
|
export declare function setEarlyLaunchLivenessMsForTests(ms: number): void;
|
|
@@ -154,8 +186,11 @@ export interface StartupFailureEvidence {
|
|
|
154
186
|
/** One seat, one startup prefix. Once execution or its input box is seen, later panes are ineligible. */
|
|
155
187
|
export declare class StartupFailureDetector {
|
|
156
188
|
private readonly adapter;
|
|
189
|
+
private readonly pattern;
|
|
190
|
+
private readonly wholePrefix;
|
|
157
191
|
private closed;
|
|
158
|
-
constructor(adapter: Pick<WorkerAdapter, "inputBox" | "harnessBannerRows"
|
|
192
|
+
constructor(adapter: Pick<WorkerAdapter, "inputBox" | "harnessBannerRows">, pattern?: RegExp, // OBS-1169: or known bootstrap text
|
|
193
|
+
wholePrefix?: boolean);
|
|
159
194
|
sample(output: string, launchedAt: number | undefined): StartupFailureEvidence | undefined;
|
|
160
195
|
}
|
|
161
196
|
/** Test seam — shrink the quota-banner silence gate without minute-long sleeps. */
|
|
@@ -209,6 +244,7 @@ export declare function verifyIntegrationTipCached(intWt: string, commands: Reco
|
|
|
209
244
|
lastMergedTask?: string;
|
|
210
245
|
baseline?: Baseline;
|
|
211
246
|
signal?: AbortSignal;
|
|
247
|
+
evidence?: GateEvidenceOptions;
|
|
212
248
|
}): Promise<boolean>;
|
|
213
249
|
type SuitePidProbe = (pid: number) => number | undefined;
|
|
214
250
|
/** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
|