tickmarkr 2.5.9 → 2.6.1

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 (67) hide show
  1. package/dist/adapters/claude-code.js +19 -6
  2. package/dist/adapters/prompt.d.ts +2 -1
  3. package/dist/adapters/prompt.js +11 -1
  4. package/dist/adapters/types.d.ts +4 -0
  5. package/dist/adapters/types.js +10 -0
  6. package/dist/cli/commands/fleet.js +26 -1
  7. package/dist/cli/commands/init.js +1 -1
  8. package/dist/cli/commands/plan.js +7 -3
  9. package/dist/cli/commands/report.js +11 -1
  10. package/dist/cli/commands/status.js +13 -3
  11. package/dist/cli/commands/verify.js +2 -0
  12. package/dist/cli/help.d.ts +2 -0
  13. package/dist/cli/help.js +2 -0
  14. package/dist/compile/native.js +39 -4
  15. package/dist/drivers/orca.d.ts +1 -1
  16. package/dist/drivers/orca.js +27 -5
  17. package/dist/gates/baseline.d.ts +2 -0
  18. package/dist/gates/baseline.js +9 -1
  19. package/dist/gates/cache.d.ts +11 -1
  20. package/dist/gates/cache.js +35 -15
  21. package/dist/gates/llm.js +4 -1
  22. package/dist/gates/review.d.ts +2 -3
  23. package/dist/gates/review.js +28 -35
  24. package/dist/gates/run-gates.d.ts +4 -0
  25. package/dist/gates/run-gates.js +17 -4
  26. package/dist/gates/test-manifest.d.ts +17 -0
  27. package/dist/gates/test-manifest.js +109 -9
  28. package/dist/gates/test-reporter.js +4 -0
  29. package/dist/graph/graph.d.ts +7 -3
  30. package/dist/graph/graph.js +21 -5
  31. package/dist/graph/schema.d.ts +2 -0
  32. package/dist/graph/schema.js +2 -0
  33. package/dist/route/preference.d.ts +1 -1
  34. package/dist/route/preference.js +10 -39
  35. package/dist/route/role-pick.d.ts +16 -0
  36. package/dist/route/role-pick.js +15 -0
  37. package/dist/route/router.d.ts +14 -0
  38. package/dist/route/router.js +9 -1
  39. package/dist/run/consult.js +5 -9
  40. package/dist/run/daemon.d.ts +16 -1
  41. package/dist/run/daemon.js +554 -77
  42. package/dist/run/git.d.ts +46 -1
  43. package/dist/run/git.js +138 -6
  44. package/dist/run/host-health.d.ts +20 -0
  45. package/dist/run/host-health.js +64 -0
  46. package/dist/run/journal.d.ts +1 -1
  47. package/dist/run/journal.js +51 -6
  48. package/dist/run/merge.d.ts +1 -1
  49. package/dist/run/merge.js +30 -6
  50. package/dist/run/operator-state.d.ts +24 -2
  51. package/dist/run/operator-state.js +41 -5
  52. package/dist/run/stall.d.ts +38 -2
  53. package/dist/run/stall.js +276 -6
  54. package/dist/tui/cockpit/board.d.ts +1 -1
  55. package/dist/tui/cockpit/board.js +27 -19
  56. package/dist/tui/cockpit/derive.d.ts +2 -0
  57. package/dist/tui/cockpit/derive.js +4 -0
  58. package/dist/tui/cockpit/live-runtime.d.ts +4 -0
  59. package/dist/tui/cockpit/live-runtime.js +37 -3
  60. package/dist/tui/cockpit/live-store.d.ts +3 -0
  61. package/dist/tui/cockpit/live-store.js +31 -8
  62. package/dist/tui/cockpit/run-cockpit.js +2 -1
  63. package/dist/tui/cockpit/run-view.d.ts +2 -4
  64. package/dist/tui/cockpit/run-view.js +9 -8
  65. package/package.json +1 -1
  66. package/schema/rungraph.schema.json +7 -0
  67. package/skills/tickmarkr-overseer/SKILL.md +76 -14
@@ -24,7 +24,7 @@ export declare function onDiskSpecHash(_repoRoot: string, graph: RunGraph): OnDi
24
24
  /** A task's definition minus its files[] (and run state): what a scope amendment must NOT move (OBS-1073). */
25
25
  export declare function taskDefinitionFingerprint(task: Task): string;
26
26
  export declare function graphDefinitionHash(g: RunGraph): string;
27
- export declare function taskContentDigest(task: Pick<Task, "goal" | "files" | "acceptance">): string;
27
+ export declare function taskContentDigest(task: Pick<Task, "goal" | "files" | "acceptance" | "outOfScope">): string;
28
28
  export declare function tickmarkrDir(repoRoot: string): string;
29
29
  export declare function loadGraph(repoRoot: string): RunGraph;
30
30
  export declare function saveGraph(repoRoot: string, g: RunGraph): void;
@@ -36,8 +36,12 @@ export declare function addEvidence(g: RunGraph, id: string, patch: {
36
36
  gateResults?: unknown[];
37
37
  }): RunGraph;
38
38
  export declare function chainDepth(g: RunGraph): Map<string, number>;
39
- export declare function readyTasks(g: RunGraph): Task[];
40
- export declare function dispatchWaves(g: RunGraph, concurrency: number): Map<string, number>;
39
+ export declare function readyTasks(g: RunGraph, prioritized?: ReadonlySet<string>): Task[];
40
+ export declare function batteryPriority(actions: Iterable<{
41
+ taskId: string;
42
+ authority: string;
43
+ }>): Set<string>;
44
+ export declare function dispatchWaves(g: RunGraph, concurrency: number, prioritized?: ReadonlySet<string>): Map<string, number>;
41
45
  export declare function isComplete(g: RunGraph): boolean;
42
46
  export declare function isStalled(g: RunGraph): boolean;
43
47
  export declare function closureReaches(g: RunGraph, taskId: string, pred: (t: Task) => boolean): boolean;
@@ -94,9 +94,14 @@ export function graphDefinitionHash(g) {
94
94
  // finding; changing the goal, write surface or acceptance contract does. Keep the full digest here:
95
95
  // unlike graphDefinitionHash this value is persisted beside evidence and is the fail-closed join a
96
96
  // later run uses, so there is no benefit in making collision diagnosis less explicit.
97
+ // OBS-1126: outOfScope joins the identity only when present — an absent list keeps the historical
98
+ // bytes so every digest persisted before the field existed still joins.
97
99
  export function taskContentDigest(task) {
98
100
  return createHash("sha256")
99
- .update(JSON.stringify({ goal: task.goal, files: task.files, acceptance: task.acceptance }))
101
+ .update(JSON.stringify({
102
+ goal: task.goal, files: task.files, acceptance: task.acceptance,
103
+ ...(task.outOfScope ? { outOfScope: task.outOfScope } : {}),
104
+ }))
100
105
  .digest("hex");
101
106
  }
102
107
  export function tickmarkrDir(repoRoot) {
@@ -186,21 +191,32 @@ export function chainDepth(g) {
186
191
  // OBS-1018: admission is critical-path order — deepest chain root first, ties in declaration order.
187
192
  // The daemon's dispatch loop slices this list unchanged. Stated limit: resume-restore of previously
188
193
  // in-flight attempts keeps its own precedence (src/run/daemon.ts); only fresh admission is ordered here.
189
- export function readyTasks(g) {
194
+ // OBS-1158: `prioritized` names tasks holding a pending battery (recheck) approval. Among READY tasks
195
+ // of equal depth an approved recheck precedes fresh work; deeper fresh work keeps its rank, and a
196
+ // recheck that is not ready (dependency-blocked) is not admitted at all, so it gains nothing here.
197
+ export function readyTasks(g, prioritized = NO_PRIORITY) {
190
198
  const done = new Set(g.tasks.filter((t) => t.status === "done").map((t) => t.id));
191
199
  const depth = chainDepth(g);
200
+ const rank = (t) => (prioritized.has(t.id) ? 1 : 0);
192
201
  return g.tasks
193
202
  .filter((t) => t.status === "pending" && t.deps.every((d) => done.has(d)))
194
- .sort((a, b) => depth.get(b.id) - depth.get(a.id)); // Array#sort is stable: equal depths keep declaration order
203
+ .sort((a, b) => depth.get(b.id) - depth.get(a.id) || rank(b) - rank(a)); // Array#sort is stable: remaining ties keep declaration order
204
+ }
205
+ const NO_PRIORITY = new Set();
206
+ // OBS-1158: the one seam plan and the daemon share for battery priority — journal approval actions
207
+ // in (src/run/journal.ts pendingApprovalActions), the ids whose pending authority is `battery` out.
208
+ // Waivers, worker-funding approvals and inert releases never qualify; a consumed approval is absent.
209
+ export function batteryPriority(actions) {
210
+ return new Set([...actions].filter((a) => a.authority === "battery").map((a) => a.taskId));
195
211
  }
196
212
  // OBS-1018: the dispatch wave each pending task would enter at `concurrency` slots if every wave
197
213
  // took one tick — computed by draining readyTasks, so plan and daemon can never disagree on order.
198
214
  // Tasks already past pending carry no wave.
199
- export function dispatchWaves(g, concurrency) {
215
+ export function dispatchWaves(g, concurrency, prioritized = NO_PRIORITY) {
200
216
  const waves = new Map();
201
217
  let sim = g;
202
218
  for (let wave = 1;; wave++) {
203
- const batch = readyTasks(sim).slice(0, Math.max(1, concurrency));
219
+ const batch = readyTasks(sim, prioritized).slice(0, Math.max(1, concurrency));
204
220
  if (!batch.length)
205
221
  return waves;
206
222
  for (const t of batch) {
@@ -80,6 +80,7 @@ export declare const TaskSchema: z.ZodObject<{
80
80
  kind: z.ZodLiteral<"fixture">;
81
81
  paths: z.ZodArray<z.ZodString>;
82
82
  }, z.core.$strip>], "kind">>>;
83
+ outOfScope: z.ZodOptional<z.ZodArray<z.ZodString>>;
83
84
  gates: z.ZodDefault<z.ZodArray<z.ZodEnum<{
84
85
  build: "build";
85
86
  test: "test";
@@ -181,6 +182,7 @@ export declare const RunGraphSchema: z.ZodObject<{
181
182
  kind: z.ZodLiteral<"fixture">;
182
183
  paths: z.ZodArray<z.ZodString>;
183
184
  }, z.core.$strip>], "kind">>>;
185
+ outOfScope: z.ZodOptional<z.ZodArray<z.ZodString>>;
184
186
  gates: z.ZodDefault<z.ZodArray<z.ZodEnum<{
185
187
  build: "build";
186
188
  test: "test";
@@ -71,6 +71,8 @@ export const TaskSchema = z.object({
71
71
  }))
72
72
  .optional(),
73
73
  pins: z.array(PinSchema).optional(),
74
+ // OBS-1126: declared bounds the task must not cross; absent ⇒ no key (sealed graph bytes hold).
75
+ outOfScope: z.array(z.string().min(1)).optional(),
74
76
  gates: z
75
77
  .array(z.enum(GATE_NAMES))
76
78
  .default(["build", "test", "lint", "evidence", "scope", "acceptance", "review"])
@@ -1,5 +1,5 @@
1
1
  import { type AuthHealth } from "../adapters/types.js";
2
- import type { TickmarkrConfig } from "../config/config.js";
2
+ import { type TickmarkrConfig } from "../config/config.js";
3
3
  export interface Disallowed {
4
4
  by: "deny" | "allow";
5
5
  entry: string;
@@ -1,4 +1,5 @@
1
1
  import { channelKey, channelsFromConfig } from "../adapters/types.js";
2
+ import { DENY_SCOPES, denyEntriesAt } from "../config/config.js";
2
3
  import { validateGraph } from "../graph/schema.js";
3
4
  import { route, RoutingError } from "./router.js";
4
5
  const PREFERENCE_ROLES = ["worker", "judge", "review", "consult"];
@@ -121,47 +122,17 @@ export function entryMatchesChannel(entry, c, allowFamilyMatching = false) {
121
122
  export function exclusionCollector(c, routingOrCfg, role = "worker") {
122
123
  const routing = "routing" in routingOrCfg ? routingOrCfg.routing : routingOrCfg;
123
124
  const out = [];
124
- const { allow, deny } = routing ?? {};
125
- for (const entry of deny?.adapters ?? []) {
126
- if (entryMatchesChannel(entry, c, true)) {
127
- out.push({
128
- scope: "routing.deny.adapters",
129
- path: "routing.deny.adapters",
130
- configPath: "routing.deny.adapters",
131
- entry,
132
- by: "deny",
133
- });
134
- }
135
- }
136
- for (const entry of deny?.models ?? []) {
137
- if (entryMatchesChannel(entry, c, true)) {
138
- out.push({
139
- scope: "routing.deny.models",
140
- path: "routing.deny.models",
141
- configPath: "routing.deny.models",
142
- entry,
143
- by: "deny",
144
- });
145
- }
146
- }
147
- if (role === "worker") {
148
- for (const entry of deny?.workers?.adapters ?? []) {
149
- if (entryMatchesChannel(entry, c, true)) {
150
- out.push({
151
- scope: "routing.deny.workers.adapters",
152
- path: "routing.deny.workers.adapters",
153
- configPath: "routing.deny.workers.adapters",
154
- entry,
155
- by: "deny",
156
- });
157
- }
158
- }
159
- for (const entry of deny?.workers?.models ?? []) {
125
+ const { allow } = routing ?? {};
126
+ for (const scope of DENY_SCOPES) {
127
+ // Lists under workers apply only to worker seats; flat deny lists cover every role.
128
+ if (scope.path[2] === "workers" && role !== "worker")
129
+ continue;
130
+ for (const entry of denyEntriesAt(routing, scope) ?? []) {
160
131
  if (entryMatchesChannel(entry, c, true)) {
161
132
  out.push({
162
- scope: "routing.deny.workers.models",
163
- path: "routing.deny.workers.models",
164
- configPath: "routing.deny.workers.models",
133
+ scope: scope.dotted,
134
+ path: scope.dotted,
135
+ configPath: scope.dotted,
165
136
  entry,
166
137
  by: "deny",
167
138
  });
@@ -0,0 +1,16 @@
1
+ import { type AuthHealth, type BillingChannel, type WorkerAdapter } from "../adapters/types.js";
2
+ import { type TickmarkrConfig } from "../config/config.js";
3
+ import type { PreferenceRole } from "./preference.js";
4
+ export { preferIndex, rankPreferredChannels, reviewPreferenceTieBreak } from "./router.js";
5
+ export type RolePick = {
6
+ ok: true;
7
+ channel: BillingChannel;
8
+ } | {
9
+ ok: false;
10
+ reason: "missing-prefer" | "no-eligible-preferred-channel";
11
+ };
12
+ /** Strict role selection: no task, author, pin, or default seat is invented. */
13
+ export declare function pickRole(role: PreferenceRole, cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, opts?: {
14
+ excludeVendors?: ReadonlySet<string>;
15
+ eligible?: (channel: BillingChannel) => boolean;
16
+ }): RolePick;
@@ -0,0 +1,15 @@
1
+ import { discoverChannels } from "../adapters/registry.js";
2
+ import { rankPreferredChannels, reviewPreferenceTieBreak } from "./router.js";
3
+ export { preferIndex, rankPreferredChannels, reviewPreferenceTieBreak } from "./router.js";
4
+ /** Strict role selection: no task, author, pin, or default seat is invented. */
5
+ export function pickRole(role, cfg, adapters, health, opts = {}) {
6
+ // Worker preferences belong to shapes; judge currently has only an explicit pin.
7
+ const prefer = role === "review" ? cfg.review.prefer : role === "consult" ? cfg.consult.prefer : undefined;
8
+ if (!prefer?.length)
9
+ return { ok: false, reason: "missing-prefer" };
10
+ const pool = discoverChannels(cfg, adapters, health, role).filter((c) => !opts.excludeVendors?.has(c.vendor) && (opts.eligible?.(c) ?? true));
11
+ const channel = rankPreferredChannels(pool, prefer, {
12
+ tieBreak: role === "review" ? reviewPreferenceTieBreak : undefined,
13
+ })[0];
14
+ return channel ? { ok: true, channel } : { ok: false, reason: "no-eligible-preferred-channel" };
15
+ }
@@ -31,6 +31,19 @@ export declare class RoutingError extends Error {
31
31
  constructor(msg: string);
32
32
  }
33
33
  export declare function marginalCostRank(c: BillingChannel): number;
34
+ type ChannelSeat = {
35
+ adapter: string;
36
+ model: string;
37
+ };
38
+ /** Shared adapter | adapter:model grammar; unmatched channels follow every declared entry. */
39
+ export declare function preferIndex(c: ChannelSeat, prefer?: readonly string[]): number;
40
+ /** Rank an already eligible pool. Review alone retains its legacy unpreferred tail. */
41
+ export declare function rankPreferredChannels<C extends ChannelSeat>(channels: readonly C[], prefer?: readonly string[], opts?: {
42
+ includeUnpreferred?: boolean;
43
+ tieBreak?: (a: C, b: C) => number;
44
+ }): C[];
45
+ /** Review preserves its established tier-then-cost order inside a preference band. */
46
+ export declare const reviewPreferenceTieBreak: (a: BillingChannel, b: BillingChannel) => number;
34
47
  export declare function route(task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext, exclude?: ReadonlySet<string>, exploreCtx?: ExploreContext): Route;
35
48
  export declare function nextChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile, exclude?: ReadonlySet<string>): Assignment | null;
36
49
  export type ClimbPick = Assignment & {
@@ -46,3 +59,4 @@ export type ClimbResult = ClimbPick;
46
59
  * returns nextChannel's same-or-higher pick flagged climbed: false naming the reason.
47
60
  */
48
61
  export declare function climbChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile, exclude?: ReadonlySet<string>): ClimbPick | null;
62
+ export {};
@@ -52,10 +52,18 @@ function resolvePin(pin, channels) {
52
52
  }
53
53
  return c;
54
54
  }
55
- function preferIndex(c, prefer = []) {
55
+ /** Shared adapter | adapter:model grammar; unmatched channels follow every declared entry. */
56
+ export function preferIndex(c, prefer = []) {
56
57
  const i = prefer.findIndex((p) => p === c.adapter || p === channelKey(c));
57
58
  return i === -1 ? prefer.length : i;
58
59
  }
60
+ /** Rank an already eligible pool. Review alone retains its legacy unpreferred tail. */
61
+ export function rankPreferredChannels(channels, prefer = [], opts = {}) {
62
+ return channels.filter((c) => opts.includeUnpreferred || preferIndex(c, prefer) < prefer.length)
63
+ .sort((a, b) => preferIndex(a, prefer) - preferIndex(b, prefer) || opts.tieBreak?.(a, b) || 0);
64
+ }
65
+ /** Review preserves its established tier-then-cost order inside a preference band. */
66
+ export const reviewPreferenceTieBreak = (a, b) => TIER_RANK[b.tier] - TIER_RANK[a.tier] || Number(a.channel === "api") - Number(b.channel === "api");
59
67
  function ladderFor(task, entry) {
60
68
  const escalate = task.routingHints?.escalate ?? entry?.escalate ?? true;
61
69
  return escalate ? ["retry", "escalate", "consult", "human"] : ["retry", "consult", "human"];
@@ -4,6 +4,7 @@ import { getAdapter } from "../adapters/registry.js";
4
4
  import { bannerShell, paneDispatchCommand } from "../brand.js";
5
5
  import { dewrapPaneVerdict, extractVerdictJson, gateExitTrailer, gatePaneName, generateVerdictNonce, verdictNonceLine } from "../gates/llm.js";
6
6
  import { classifyVerdictCause } from "../gates/verdict-cause.js";
7
+ import { rankPreferredChannels } from "../route/role-pick.js";
7
8
  import { disallowedBy, routingModelProvider } from "../route/preference.js";
8
9
  import { sh } from "./git.js";
9
10
  import { redactSecrets } from "./redact.js";
@@ -212,15 +213,10 @@ opts = {}) {
212
213
  }
213
214
  return parsed;
214
215
  };
215
- // v1.54 T1: ranked seat failover. Walk consult.prefer (adapter:model entries) to the first entry
216
- // whose adapter is in the live channel set; a failed seat or unparseable verdict falls to the next;
217
- // the pinned consult.adapter/model is always the final seat. No channels provided (non-daemon
218
- // callers) ⇒ empty live set ⇒ pin only, byte-identical to pre-v1.54 behavior. Failover changes
219
- // only WHICH seat answers — per-seat parsing and the fail-safe human action below are untouched.
220
- const live = new Set((opts.channels ?? []).map((c) => c.adapter));
221
- const seats = (cfg.consult.prefer ?? [])
222
- .map((entry) => ({ adapter: entry.slice(0, entry.indexOf(":")), model: entry.slice(entry.indexOf(":") + 1) }))
223
- .filter((s) => live.has(s.adapter));
216
+ // Rank only exact live models from the daemon's role-scoped discovery pool.
217
+ // The explicit pin remains a caller-owned fallback, outside strict preference selection.
218
+ const live = (opts.channels ?? []).filter((c) => typeof c.model === "string");
219
+ const seats = rankPreferredChannels(live.filter((c) => disallowedBy(c, cfg.routing, "consult") === null), cfg.consult.prefer);
224
220
  seats.push({ adapter: cfg.consult.adapter, model: cfg.consult.model });
225
221
  // v1.87 T2: a consult seat reads the code, so it passes through the operator's policy exactly like
226
222
  // a worker does. The filter runs AFTER the pin is pushed, so the final pinned seat is checked by
@@ -5,6 +5,7 @@ import { type ExecutorDriver, type Slot } from "../drivers/types.js";
5
5
  import { type Baseline } from "../gates/baseline.js";
6
6
  import type { GateResult } from "../gates/types.js";
7
7
  import { type RunGraph } from "../graph/schema.js";
8
+ import { type PreserveProducer } from "./git.js";
8
9
  import { Journal, type JournalEvent } from "./journal.js";
9
10
  export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
10
11
  /** Optional authoritative transport receipt. Screen text can prove execution, never nonacceptance. */
@@ -67,6 +68,7 @@ export interface RunSummary {
67
68
  /** the accepted approvals that never reached a dispatch — named, never left to the park buckets */
68
69
  outstandingApprovals?: string[];
69
70
  }
71
+ export declare function pendingDaemonApprovalActions(events: JournalEvent[]): Map<string, import("./journal.js").PendingApprovalAction>;
70
72
  /**
71
73
  * T14, amended by v2.2 T3: approvals the run accepted and never acted on. `approved` above is still
72
74
  * built ONCE at startup — replay determinism depends on it — but a live approval is no longer inert:
@@ -126,6 +128,9 @@ export declare const APPROVAL_POLL_MS = 250;
126
128
  export declare const APPROVAL_WINDOW_MS = 120000;
127
129
  export declare const setApprovalWindowForTests: (ms: number) => void;
128
130
  export declare const resetApprovalWindowForTests: () => void;
131
+ /** Test seam — shrink the capacity backoff without minute-long sleeps. */
132
+ export declare function setCapacityBackoffMsForTests(ms: number): void;
133
+ export declare function resetCapacityBackoffMsForTests(): void;
129
134
  export declare const EARLY_LAUNCH_LIVENESS_MS = 60000;
130
135
  /** Test seam — lowers the empty-pane liveness window without sleeping 60s per case. */
131
136
  export declare function setEarlyLaunchLivenessMsForTests(ms: number): void;
@@ -170,6 +175,11 @@ export declare const HARVESTED_RESULT_SUMMARY = "harvested: the worktree carries
170
175
  export declare function commandsHash(commands: Record<string, string>): string;
171
176
  export interface TipProof {
172
177
  kind: "fresh" | "reused" | "failed" | "incomplete";
178
+ /** Each completed gate keeps its own execution provenance, even in a mixed cycle. */
179
+ gates?: Array<{
180
+ gate: string;
181
+ kind: "fresh" | "reused";
182
+ }>;
173
183
  /** the commit the cycle's start row spoke for */
174
184
  tip?: string;
175
185
  }
@@ -177,7 +187,8 @@ export interface TipProof {
177
187
  * OBS-1077 close rider: what the engagement's LATEST verification cycle proved — its
178
188
  * `tip-verify-start` row and what followed, never a commit comparison. Exactly one kind per close:
179
189
  * fresh (every tip command ran AND passed), reused (an eligible cached cycle carried forward),
180
- * failed, or incomplete (cancelled, cut short, mixed or undelimited). Nothing before the start row
190
+ * failed, or incomplete (cancelled, cut short or undelimited). Completed mixed cycles retain each
191
+ * gate's kind beside the whole-cycle reused kind. Nothing before the start row
181
192
  * is read, so an unfinished cycle inherits nothing from an earlier green one.
182
193
  */
183
194
  export declare function runEndTipProof(events: readonly JournalEvent[]): TipProof;
@@ -212,5 +223,9 @@ export declare function liveSuiteCount(repoRoot: string): Promise<number>;
212
223
  /** Test seam — exercise the production observer's total read bound with a small real tree. */
213
224
  export declare function setObserveBudgetBytesForTests(bytes: number): void;
214
225
  export declare function resetObserveBudgetBytesForTests(): void;
226
+ /** The attempt whose worker last launched into the task's current checkout; "unknown" once a
227
+ * recreation replaced that tree without a new launch (recheck, gate-only restore) or when no
228
+ * dispatch assignment is on record. Never the newest dispatch by itself. */
229
+ export declare function knownProducer(events: readonly JournalEvent[], taskId: string): PreserveProducer;
215
230
  export declare function recordFatalRunEnd(journal: Journal, runId: string, branch: string, err: unknown, graph?: RunGraph, phase?: string): TipProof | undefined;
216
231
  export declare function runDaemon(repoRoot: string, opts?: RunOptions): Promise<RunSummary>;