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.
Files changed (105) hide show
  1. package/README.md +16 -3
  2. package/dist/adapters/catalog-remote.js +89 -47
  3. package/dist/adapters/claude-code.js +9 -6
  4. package/dist/adapters/codex.js +7 -4
  5. package/dist/adapters/prompt.d.ts +1 -0
  6. package/dist/adapters/prompt.js +14 -6
  7. package/dist/adapters/registry.js +3 -3
  8. package/dist/adapters/types.d.ts +12 -4
  9. package/dist/adapters/types.js +6 -0
  10. package/dist/cli/commands/approve.d.ts +11 -4
  11. package/dist/cli/commands/approve.js +82 -27
  12. package/dist/cli/commands/compile.js +13 -3
  13. package/dist/cli/commands/doctor.d.ts +8 -2
  14. package/dist/cli/commands/doctor.js +11 -3
  15. package/dist/cli/commands/fleet.js +87 -11
  16. package/dist/cli/commands/plan.js +13 -8
  17. package/dist/cli/commands/report.d.ts +2 -1
  18. package/dist/cli/commands/report.js +74 -8
  19. package/dist/cli/commands/resume.js +4 -2
  20. package/dist/cli/commands/status.js +43 -20
  21. package/dist/cli/help.d.ts +2 -0
  22. package/dist/cli/help.js +9 -2
  23. package/dist/compile/native.js +7 -0
  24. package/dist/config/config.d.ts +35 -2
  25. package/dist/config/config.js +86 -10
  26. package/dist/config/fleet-overlay.d.ts +13 -2
  27. package/dist/config/fleet-overlay.js +60 -0
  28. package/dist/drivers/herdr.d.ts +12 -0
  29. package/dist/drivers/herdr.js +51 -0
  30. package/dist/drivers/orca.d.ts +35 -2
  31. package/dist/drivers/orca.js +222 -67
  32. package/dist/drivers/types.d.ts +2 -0
  33. package/dist/drivers/types.js +2 -2
  34. package/dist/eval/canary.d.ts +2 -1
  35. package/dist/eval/canary.js +2 -2
  36. package/dist/eval/dispatch.js +1 -0
  37. package/dist/gates/acceptance.d.ts +9 -1
  38. package/dist/gates/acceptance.js +31 -4
  39. package/dist/gates/baseline.d.ts +32 -2
  40. package/dist/gates/baseline.js +111 -24
  41. package/dist/gates/cache.d.ts +8 -0
  42. package/dist/gates/cache.js +12 -2
  43. package/dist/gates/llm.d.ts +11 -4
  44. package/dist/gates/llm.js +40 -21
  45. package/dist/gates/review.d.ts +14 -1
  46. package/dist/gates/review.js +160 -34
  47. package/dist/gates/run-gates.d.ts +56 -4
  48. package/dist/gates/run-gates.js +358 -58
  49. package/dist/gates/test-manifest.d.ts +45 -1
  50. package/dist/gates/test-manifest.js +78 -12
  51. package/dist/graph/schema.d.ts +2 -0
  52. package/dist/graph/schema.js +2 -0
  53. package/dist/plan/scope.js +2 -2
  54. package/dist/route/preference.d.ts +20 -2
  55. package/dist/route/preference.js +48 -13
  56. package/dist/route/router.d.ts +12 -1
  57. package/dist/route/router.js +56 -24
  58. package/dist/run/consult.d.ts +15 -1
  59. package/dist/run/consult.js +18 -7
  60. package/dist/run/daemon.d.ts +38 -2
  61. package/dist/run/daemon.js +895 -192
  62. package/dist/run/git.d.ts +8 -0
  63. package/dist/run/git.js +14 -0
  64. package/dist/run/interactive-seed.d.ts +4 -0
  65. package/dist/run/interactive-seed.js +35 -9
  66. package/dist/run/journal.d.ts +152 -3
  67. package/dist/run/journal.js +551 -50
  68. package/dist/run/lease.d.ts +13 -0
  69. package/dist/run/lease.js +45 -0
  70. package/dist/run/merge.d.ts +3 -1
  71. package/dist/run/merge.js +3 -2
  72. package/dist/run/operator-summary.d.ts +3 -0
  73. package/dist/run/operator-summary.js +3 -1
  74. package/dist/run/protocol.d.ts +46 -1
  75. package/dist/run/protocol.js +14 -2
  76. package/dist/run/receipt-resolver.d.ts +22 -0
  77. package/dist/run/receipt-resolver.js +40 -1
  78. package/dist/run/repair-selection.d.ts +11 -1
  79. package/dist/run/repair-selection.js +17 -9
  80. package/dist/run/supervision.d.ts +7 -1
  81. package/dist/run/supervision.js +5 -2
  82. package/dist/run/wall-budget.d.ts +48 -0
  83. package/dist/run/wall-budget.js +280 -0
  84. package/dist/tui/cockpit/board.js +3 -3
  85. package/dist/tui/cockpit/decision-actions.d.ts +8 -5
  86. package/dist/tui/cockpit/decision-actions.js +55 -32
  87. package/dist/tui/cockpit/derive.js +13 -2
  88. package/dist/tui/cockpit/live-runtime.d.ts +10 -0
  89. package/dist/tui/cockpit/live-runtime.js +50 -3
  90. package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
  91. package/dist/tui/cockpit/run-cockpit.js +27 -2
  92. package/dist/tui/cockpit/run-view.d.ts +9 -2
  93. package/dist/tui/cockpit/run-view.js +66 -9
  94. package/dist/tui/cockpit/setup-cockpit.d.ts +6 -0
  95. package/dist/tui/cockpit/setup-cockpit.js +10 -3
  96. package/dist/tui/ink/fleet-app.d.ts +15 -3
  97. package/dist/tui/ink/fleet-app.js +91 -22
  98. package/package.json +3 -1
  99. package/schema/config.schema.json +825 -0
  100. package/skills/tickmarkr-loop/SKILL.md +15 -3
  101. package/skills/tickmarkr-overseer/SKILL.md +42 -0
  102. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +91 -0
  103. package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
  104. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
  105. package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
@@ -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({ adapter: via, model }, cfg.routing);
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 = taskFloor && (!advisoryFloor || TIER_RANK[taskFloor] >= TIER_RANK[advisoryFloor])
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
- if (prefActive) {
232
- for (const p of pool.channels) {
233
- const i = p.indexOf(":");
234
- const d = disallowedBy({ adapter: p.slice(0, i), model: p.slice(i + 1) }, cfg.routing);
235
- if (d) {
236
- throw new RoutingError(`${task.id}: pool entry ${p} is disallowed by routing.${d.by} (${d.entry}) — remove the ${d.by} entry or drop it from the pool`);
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 = pool.channels
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 bound = taskFloor && TIER_RANK[taskFloor] >= TIER_RANK[baseTier] ? `floor ${taskFloor} (task hint${src})` :
353
- floor ? `floor ${floor} (config floors)` :
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);
@@ -1,5 +1,6 @@
1
- import type { WorkerAdapter } from "../adapters/types.js";
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>;
@@ -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({ adapter: cfg.consult.adapter, model: cfg.consult.model });
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 {
@@ -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