tickmarkr 2.5.3 → 2.5.5

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 (71) hide show
  1. package/dist/adapters/registry.js +6 -1
  2. package/dist/adapters/types.d.ts +3 -0
  3. package/dist/cli/commands/approve.d.ts +1 -0
  4. package/dist/cli/commands/approve.js +54 -6
  5. package/dist/cli/commands/doctor.d.ts +4 -0
  6. package/dist/cli/commands/doctor.js +44 -29
  7. package/dist/cli/commands/fleet.js +195 -33
  8. package/dist/cli/commands/init.js +196 -6
  9. package/dist/cli/commands/plan.js +144 -3
  10. package/dist/cli/commands/resume.js +6 -2
  11. package/dist/cli/commands/run.js +21 -2
  12. package/dist/cli/commands/verify.js +1 -1
  13. package/dist/cli/help.d.ts +2 -0
  14. package/dist/cli/help.js +3 -1
  15. package/dist/compile/collateral.d.ts +2 -0
  16. package/dist/compile/collateral.js +50 -9
  17. package/dist/compile/ownership.d.ts +7 -0
  18. package/dist/compile/ownership.js +59 -22
  19. package/dist/config/config.d.ts +23 -8
  20. package/dist/config/config.js +42 -28
  21. package/dist/config/fleet-overlay.d.ts +3 -9
  22. package/dist/config/fleet-overlay.js +68 -11
  23. package/dist/config/fleet-why.d.ts +7 -0
  24. package/dist/config/fleet-why.js +5 -0
  25. package/dist/drivers/herdr.d.ts +5 -0
  26. package/dist/drivers/herdr.js +14 -3
  27. package/dist/drivers/index.d.ts +15 -1
  28. package/dist/drivers/index.js +38 -10
  29. package/dist/drivers/orca.d.ts +99 -10
  30. package/dist/drivers/orca.js +586 -97
  31. package/dist/drivers/types.d.ts +1 -0
  32. package/dist/gates/baseline.d.ts +3 -0
  33. package/dist/gates/baseline.js +2 -1
  34. package/dist/gates/cache.d.ts +100 -0
  35. package/dist/gates/cache.js +389 -0
  36. package/dist/gates/review.d.ts +35 -2
  37. package/dist/gates/review.js +64 -16
  38. package/dist/gates/run-gates.d.ts +5 -0
  39. package/dist/gates/run-gates.js +134 -18
  40. package/dist/gates/test-manifest.d.ts +99 -0
  41. package/dist/gates/test-manifest.js +389 -0
  42. package/dist/gates/test-reporter.d.ts +4 -0
  43. package/dist/gates/test-reporter.js +49 -0
  44. package/dist/route/preference.d.ts +22 -1
  45. package/dist/route/preference.js +123 -25
  46. package/dist/route/router.d.ts +13 -0
  47. package/dist/route/router.js +95 -17
  48. package/dist/run/daemon.d.ts +13 -0
  49. package/dist/run/daemon.js +398 -96
  50. package/dist/run/git.d.ts +35 -1
  51. package/dist/run/git.js +111 -10
  52. package/dist/run/journal.d.ts +14 -2
  53. package/dist/run/journal.js +78 -10
  54. package/dist/run/lease.d.ts +14 -0
  55. package/dist/run/lease.js +87 -0
  56. package/dist/run/merge.d.ts +2 -0
  57. package/dist/run/merge.js +91 -3
  58. package/dist/run/operator-state.d.ts +11 -0
  59. package/dist/run/operator-state.js +17 -3
  60. package/dist/tui/cockpit/board.d.ts +96 -0
  61. package/dist/tui/cockpit/board.js +346 -0
  62. package/dist/tui/cockpit/decision-actions.js +2 -0
  63. package/dist/tui/cockpit/layout.d.ts +5 -1
  64. package/dist/tui/cockpit/layout.js +8 -3
  65. package/dist/tui/cockpit/live-runtime.js +83 -31
  66. package/dist/tui/cockpit/run-view.d.ts +7 -5
  67. package/dist/tui/cockpit/run-view.js +12 -11
  68. package/dist/tui/ink/fleet-app.d.ts +41 -27
  69. package/dist/tui/ink/fleet-app.js +204 -31
  70. package/package.json +1 -1
  71. package/skills/tickmarkr-overseer/SKILL.md +180 -99
@@ -33,3 +33,16 @@ export declare class RoutingError extends Error {
33
33
  export declare function marginalCostRank(c: BillingChannel): number;
34
34
  export declare function route(task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext, exclude?: ReadonlySet<string>, exploreCtx?: ExploreContext): Route;
35
35
  export declare function nextChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile, exclude?: ReadonlySet<string>): Assignment | null;
36
+ export type ClimbPick = Assignment & {
37
+ climbed: boolean;
38
+ reason?: string;
39
+ };
40
+ export type ClimbResult = ClimbPick;
41
+ /**
42
+ * v2.5.4 OBS-986 (ES-1): climb one tier on request when untried channels exist in higher tiers.
43
+ * With routing.escalateTier on, returns the cheapest untried live channel strictly above current.tier,
44
+ * ordered as nextChannel orders (tier, then marginal cost, learned score strictly last), flagged climbed: true.
45
+ * When the knob is off, when current is frontier, or when no higher tier has an untried channel,
46
+ * returns nextChannel's same-or-higher pick flagged climbed: false naming the reason.
47
+ */
48
+ export declare function climbChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile, exclude?: ReadonlySet<string>): ClimbPick | null;
@@ -103,6 +103,32 @@ function withoutExcluded(channels, exclude) {
103
103
  // OBS-57: in-run demotion after consecutive no-trailer windows — route around poisoned channels for the rest of the run.
104
104
  return channels.filter((c) => !exclude.has(channelKey(c)));
105
105
  }
106
+ // Pools spread providers before tier, independently within each marginal-cost run. Stable
107
+ // cost sorting preserves declaration order when forming groups; gateways share the served
108
+ // model's provider, so adding a second transport does not buy that provider another turn.
109
+ function rankAnyPool(live, task) {
110
+ const sorted = [...live].sort((a, b) => marginalCostRank(a) - marginalCostRank(b));
111
+ const out = [];
112
+ for (let i = 0; i < sorted.length;) {
113
+ let j = i + 1;
114
+ while (j < sorted.length && marginalCostRank(sorted[j]) === marginalCostRank(sorted[i]))
115
+ j++;
116
+ const groups = new Map();
117
+ for (const c of sorted.slice(i, j)) {
118
+ const provider = routingModelProvider(c.model, c.vendor);
119
+ const group = groups.get(provider) ?? [];
120
+ group.push(c);
121
+ groups.set(provider, group);
122
+ }
123
+ const ordered = [...groups.values()];
124
+ const k = spreadOffset(task) % ordered.length;
125
+ for (const group of [...ordered.slice(k), ...ordered.slice(0, k)]) {
126
+ out.push(...group.sort((a, b) => TIER_RANK[a.tier] - TIER_RANK[b.tier]));
127
+ }
128
+ i = j;
129
+ }
130
+ return out;
131
+ }
106
132
  const maybeSlaLint = (lints, task, profile, slaMinutes, c) => {
107
133
  if (slaMinutes === undefined || !profile)
108
134
  return;
@@ -206,21 +232,20 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
206
232
  // live preserves pool declaration order — ordered mode and any-mode stable-sort ties depend on it
207
233
  const live = pool.channels
208
234
  .map((id) => channels.find((c) => channelKey(c) === id))
209
- .filter((c) => c !== undefined);
235
+ .filter((c) => c !== undefined && (!taskFloor || TIER_RANK[c.tier] >= TIER_RANK[taskFloor]));
210
236
  if (!live.length) {
211
- throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel — declared: ${pool.channels.join(", ")}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
237
+ 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)"}`);
212
238
  }
213
- // ponytail: no learned/explore/spread inside pools in v1 — a pool pick is fully static; the
214
- // upgrade path is feeding the pool-filtered set through the learned path below.
239
+ // No learned/explore inside pools: any spreads deterministically; ordered keeps declaration order.
215
240
  const chosen = pool.mode === "ordered"
216
241
  ? live[0]
217
- : [...live].sort((a, b) => marginalCostRank(a) - marginalCostRank(b) || TIER_RANK[a.tier] - TIER_RANK[b.tier])[0];
242
+ : rankAnyPool(live, task)[0];
218
243
  lintMapPinFloor(chosen.tier, "pool");
219
244
  maybeSlaLint(lints, task, profile, slaMinutes, chosen);
220
245
  return { assignment: toAssignment(chosen), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pool ${pool.mode} ${channelKey(chosen)} (config routing.map)` };
221
246
  }
222
247
  const baseTier = floor ?? "cheap";
223
- // D-04: a task floor is hard only on floor/auto paths; the map-pin branch above stays supreme.
248
+ // A task floor is hard on pool and floor/auto paths; the map-pin branch above stays supreme.
224
249
  const minTier = taskFloor && TIER_RANK[taskFloor] > TIER_RANK[baseTier] ? taskFloor : baseTier;
225
250
  if (prefActive)
226
251
  for (const p of prefer ?? [])
@@ -328,15 +353,8 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
328
353
  maybeSlaLint(lints, task, profile, slaMinutes, eligible[0]);
329
354
  return { assignment: toAssignment(eligible[0]), ladder: ladderFor(task, entry), lints, provenance: `${degraded}${bound}, marginal-cost auto (${chosenBy})`, ...(deviation ? { deviation } : {}) };
330
355
  }
331
- export function nextChannel(current, task, cfg, channels, tried, profile, exclude) {
356
+ function candidatePool(current, channels, tried, tierPredicate, exclude) {
332
357
  channels = withoutExcluded(channels, exclude);
333
- // already cheapest-sufficient: TIER_RANK asc is the PRIMARY key so escalation climbs one band at a time.
334
- // Do NOT "unify" this onto route()'s key order (marginal-cost first) — that reverses climb-one-band on mixed fleets (ROUTE-02, D2).
335
- // ROUTE-13: learnedScore is the STRICTLY-LAST key — within-band tiebreak only. Precomputed
336
- // outside the comparator (Pitfall 1), never arithmetic-combined with the band keys, no
337
- // profile-dependent filter. NO exploration bonus here (route():110 has one; a probe on the
338
- // failure path would spend a real retry). Absent profile ⇒ every score is 0 ⇒ third key
339
- // all-ties ⇒ the stable sort preserves the exact v1.7 candidate ORDER.
340
358
  const triedKeys = new Set(tried);
341
359
  const triedIdentities = new Set(tried.map((key) => {
342
360
  const channel = channels.find((c) => channelKey(c) === key);
@@ -348,11 +366,71 @@ export function nextChannel(current, task, cfg, channels, tried, profile, exclud
348
366
  && channels.filter((c) => c.adapter === current.adapter).every((c) => triedKeys.has(channelKey(c)));
349
367
  const currentChannel = channels.find((c) => c.adapter === current.adapter && c.model === current.model);
350
368
  const excludedProvider = currentAdapterExcluded ? routingModelProvider(current.model, currentChannel?.vendor) : undefined;
351
- const pool = channels.filter((c) => !triedIdentities.has(modelRouteIdentity(c.model, c.vendor))
369
+ return channels.filter((c) => !triedIdentities.has(modelRouteIdentity(c.model, c.vendor))
352
370
  && (!excludedProvider || routingModelProvider(c.model, c.vendor) !== excludedProvider)
353
- && TIER_RANK[c.tier] >= TIER_RANK[current.tier]);
371
+ && tierPredicate(c.tier));
372
+ }
373
+ function rankFailoverCandidates(pool, task, cfg, profile) {
354
374
  const scores = new Map(pool.map((c) => [channelKey(c), profile ? learnedScore(profile, task.shape, channelKey(c), c.channel, { availWeight: cfg.routing.learnedTuning?.availWeight }) : 0]));
355
375
  const scoreOf = (c) => scores.get(channelKey(c));
356
- const candidates = pool.sort((a, b) => TIER_RANK[a.tier] - TIER_RANK[b.tier] || marginalCostRank(a) - marginalCostRank(b) || scoreOf(b) - scoreOf(a));
376
+ return pool.sort((a, b) => TIER_RANK[a.tier] - TIER_RANK[b.tier] || marginalCostRank(a) - marginalCostRank(b) || scoreOf(b) - scoreOf(a));
377
+ }
378
+ export function nextChannel(current, task, cfg, channels, tried, profile, exclude) {
379
+ // already cheapest-sufficient: TIER_RANK asc is the PRIMARY key so escalation climbs one band at a time.
380
+ // Do NOT "unify" this onto route()'s key order (marginal-cost first) — that reverses climb-one-band on mixed fleets (ROUTE-02, D2).
381
+ // ROUTE-13: learnedScore is the STRICTLY-LAST key — within-band tiebreak only. Precomputed
382
+ // outside the comparator (Pitfall 1), never arithmetic-combined with the band keys, no
383
+ // profile-dependent filter. NO exploration bonus here (route():110 has one; a probe on the
384
+ // failure path would spend a real retry). Absent profile ⇒ every score is 0 ⇒ third key
385
+ // all-ties ⇒ the stable sort preserves the exact v1.7 candidate ORDER.
386
+ const pool = candidatePool(current, channels, tried, (tier) => TIER_RANK[tier] >= TIER_RANK[current.tier], exclude);
387
+ const candidates = rankFailoverCandidates(pool, task, cfg, profile);
357
388
  return candidates.length ? toAssignment(candidates[0]) : null;
358
389
  }
390
+ function makeClimbPick(assignment, climbed, reason) {
391
+ const pick = {
392
+ adapter: assignment.adapter,
393
+ model: assignment.model,
394
+ channel: assignment.channel,
395
+ tier: assignment.tier,
396
+ climbed,
397
+ ...(climbed ? {} : { reason }),
398
+ };
399
+ Object.defineProperty(pick, "assignment", {
400
+ get() {
401
+ return {
402
+ adapter: this.adapter,
403
+ model: this.model,
404
+ channel: this.channel,
405
+ tier: this.tier,
406
+ };
407
+ },
408
+ enumerable: false,
409
+ });
410
+ return pick;
411
+ }
412
+ /**
413
+ * v2.5.4 OBS-986 (ES-1): climb one tier on request when untried channels exist in higher tiers.
414
+ * With routing.escalateTier on, returns the cheapest untried live channel strictly above current.tier,
415
+ * ordered as nextChannel orders (tier, then marginal cost, learned score strictly last), flagged climbed: true.
416
+ * When the knob is off, when current is frontier, or when no higher tier has an untried channel,
417
+ * returns nextChannel's same-or-higher pick flagged climbed: false naming the reason.
418
+ */
419
+ export function climbChannel(current, task, cfg, channels, tried, profile, exclude) {
420
+ const escalateOn = cfg.routing.escalateTier !== "off";
421
+ if (!escalateOn) {
422
+ const pick = nextChannel(current, task, cfg, channels, tried, profile, exclude);
423
+ return pick ? makeClimbPick(pick, false, "routing.escalateTier knob is off") : null;
424
+ }
425
+ if (current.tier === "frontier") {
426
+ const pick = nextChannel(current, task, cfg, channels, tried, profile, exclude);
427
+ return pick ? makeClimbPick(pick, false, "no higher tier") : null;
428
+ }
429
+ const higherPool = candidatePool(current, channels, tried, (tier) => TIER_RANK[tier] > TIER_RANK[current.tier], exclude);
430
+ if (higherPool.length > 0) {
431
+ const candidates = rankFailoverCandidates(higherPool, task, cfg, profile);
432
+ return makeClimbPick(toAssignment(candidates[0]), true);
433
+ }
434
+ const pick = nextChannel(current, task, cfg, channels, tried, profile, exclude);
435
+ return pick ? makeClimbPick(pick, false, "no higher tier has an untried channel") : null;
436
+ }
@@ -126,6 +126,19 @@ export declare const WORKER_STARTUP_WINDOW_BYTES: number;
126
126
  /** Test seam — exercises both sides of the startup boundary without a production-length fixture. */
127
127
  export declare function setWorkerStartupWindowMsForTests(ms: number): void;
128
128
  export declare function resetWorkerStartupWindowMsForTests(): void;
129
+ export interface StartupFailureEvidence {
130
+ matchedBytes: string;
131
+ offset: number;
132
+ row: string;
133
+ rowNumber: number;
134
+ }
135
+ /** One seat, one startup prefix. Once execution or its input box is seen, later panes are ineligible. */
136
+ export declare class StartupFailureDetector {
137
+ private readonly adapter;
138
+ private closed;
139
+ constructor(adapter: Pick<WorkerAdapter, "inputBox" | "harnessBannerRows">);
140
+ sample(output: string, launchedAt: number | undefined): StartupFailureEvidence | undefined;
141
+ }
129
142
  /** Test seam — shrink the quota-banner silence gate without minute-long sleeps. */
130
143
  export declare function setQuotaBannerSilentMsForTests(ms: number): void;
131
144
  export declare function resetQuotaBannerSilentMsForTests(): void;