tickmarkr 2.5.0 → 2.5.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.
@@ -1,6 +1,7 @@
1
1
  import { type Assignment, type BillingChannel, type WorkerAdapter } from "../adapters/types.js";
2
2
  import { type TickmarkrConfig, type Tier } from "../config/config.js";
3
3
  import { type Task } from "../graph/schema.js";
4
+ import { type StructuredFinding } from "../run/journal.js";
4
5
  import { modelProvider } from "../route/preference.js";
5
6
  import { type GateVia } from "./llm.js";
6
7
  import type { GateResult } from "./types.js";
@@ -16,6 +17,8 @@ export interface ReviewFinding {
16
17
  }
17
18
  export interface ReviewVerdict {
18
19
  approve?: boolean;
20
+ resolved?: string[];
21
+ reraised?: string[];
19
22
  issues?: string[];
20
23
  findings?: ReviewFinding[];
21
24
  comments?: Array<{
@@ -54,12 +57,12 @@ export declare function pickReviewer(author: Assignment, channels: BillingChanne
54
57
  prefer?: string[], // v1.53 T2: review.prefer — reorders eligible channels, never changes eligibility
55
58
  floor?: Tier, // task-declared only; config floors govern workers and must not silently move review seats
56
59
  history?: string[], // run-scoped picks, oldest to newest; empty preserves the established ranking
57
- onSeat?: (seat: number) => void): BillingChannel | null;
58
- export type ReviewUnparseableCause = VerdictUnparseableCause;
60
+ onSeat?: (seat: number, count: number) => void, demoted?: ReadonlySet<string>): BillingChannel | null;
61
+ export type ReviewUnparseableCause = VerdictUnparseableCause | "launch-never-started" | "truncated" | "silent";
59
62
  /**
60
63
  * This shows the reviewer what the task DECLARED, never what the diff may actually reach. The diff
61
64
  * remains a separate stated input, so whether its touched paths fit the declaration stays a reviewer
62
65
  * judgement rather than a guarantee made by this renderer.
63
66
  */
64
67
  export declare function renderDeclaredWriteScope(files: ReadonlyArray<string>): string;
65
- export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: TickmarkrConfig, via?: GateVia, excludeReviewers?: string[], artifactDir?: string, reviewHistory?: string[]): Promise<GateResult>;
68
+ export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: TickmarkrConfig, via?: GateVia, excludeReviewers?: string[], artifactDir?: string, reviewHistory?: string[], demotedReviewers?: ReadonlySet<string>, carriedFindings?: readonly StructuredFinding[]): Promise<GateResult>;
@@ -6,6 +6,7 @@ import { filesGlob } from "../graph/files-glob.js";
6
6
  import { renderAcceptanceItem } from "../graph/schema.js";
7
7
  import { getAdapter } from "../adapters/registry.js";
8
8
  import { shOk } from "../run/git.js";
9
+ import { structuredFindings } from "../run/journal.js";
9
10
  import { redactSecrets } from "../run/redact.js";
10
11
  import { marginalCostRank } from "../route/router.js";
11
12
  import { modelProvider } from "../route/preference.js";
@@ -178,7 +179,7 @@ export function pickReviewer(author, channels, exclude = [], // v1.1 failover: r
178
179
  prefer = [], // v1.53 T2: review.prefer — reorders eligible channels, never changes eligibility
179
180
  floor, // task-declared only; config floors govern workers and must not silently move review seats
180
181
  history = [], // run-scoped picks, oldest to newest; empty preserves the established ranking
181
- onSeat) {
182
+ onSeat, demoted = new Set()) {
182
183
  // FLEET-05 success criterion 2: an author not resolvable in the channel list yields NO reviewer.
183
184
  // The old `?? author.adapter` fallback compared an adapter id to vendor names, matched nothing, and
184
185
  // admitted every reviewer — including the author's own channel (fail-OPEN). null lands on reviewGate's
@@ -188,20 +189,21 @@ onSeat) {
188
189
  return null;
189
190
  const authorProvider = modelProvider(author.model, authorChannel.vendor);
190
191
  const ranked = channels
191
- // two independent axes: different vendor AND different base-model identity (ADDED TO the vendor
192
- // rule, never replacing it — a future edit can't silently drop either). Failover additionally guards
193
- // true provider identity; the initial pick keeps the established stamped-vendor contract. The diversity
192
+ // Three independent axes: different vendor, different resolved provider identity (OBS-946: on initial pick
193
+ // as well as failover, so an aggregator channel stamped "mixed" never seats the author's own provider),
194
+ // and different base-model identity (ADDED TO the vendor rule, never replacing it). The diversity
194
195
  // filter runs BEFORE preference ranking, so prefer cannot resurrect an excluded channel.
195
196
  .filter((c) => c.vendor !== authorChannel.vendor
196
- && (exclude.length === 0 || modelProvider(c.model, c.vendor) !== authorProvider)
197
+ && modelProvider(c.model, c.vendor) !== authorProvider
197
198
  && modelId(c.model) !== modelId(author.model)
198
199
  && !exclude.includes(channelKey(c))
199
200
  && (floor === undefined || TIER_RANK[c.tier] >= TIER_RANK[floor]))
200
201
  .sort((a, b) => reviewPreferIndex(a, prefer) - reviewPreferIndex(b, prefer) || TIER_RANK[b.tier] - TIER_RANK[a.tier] || marginalCostRank(a) - marginalCostRank(b));
201
- const reviewer = [...ranked].sort((a, b) => history.lastIndexOf(channelKey(a)) - history.lastIndexOf(channelKey(b))
202
+ const reviewer = [...ranked].sort((a, b) => Number(demoted.has(channelKey(a))) - Number(demoted.has(channelKey(b)))
203
+ || history.lastIndexOf(channelKey(a)) - history.lastIndexOf(channelKey(b))
202
204
  || ranked.indexOf(a) - ranked.indexOf(b))[0] ?? null;
203
205
  if (reviewer)
204
- onSeat?.(ranked.indexOf(reviewer) + 1);
206
+ onSeat?.(ranked.indexOf(reviewer) + 1, ranked.length);
205
207
  return reviewer;
206
208
  }
207
209
  /**
@@ -220,7 +222,7 @@ ${files.map((path) => `- ${path}`).join("\n")}`;
220
222
  export async function reviewGate(task, worktree, baseRef, author, channels, adapters, cfg, via, excludeReviewers,
221
223
  // OBS-196: run dir for raw-output persistence on an unparseable verdict; absent (older callers,
222
224
  // direct tests) skips persistence and changes nothing else.
223
- artifactDir, reviewHistory) {
225
+ artifactDir, reviewHistory, demotedReviewers, carriedFindings = []) {
224
226
  // R3 (OBS-186): participation is keyed on PATHS. The compiler's assignment comes from the DECLARED
225
227
  // files[]; the operator's floor may RAISE it to full and can never lower it. `complexityThreshold` is
226
228
  // retired — the branch that returned a green skip on a complexity comparison is gone, and with it the
@@ -241,8 +243,9 @@ artifactDir, reviewHistory) {
241
243
  // so production rounds have journaled both siblings all along — only the fixtures were blind to it.
242
244
  // Fixed in the ledger rather than in the oracles, because determinism run-to-run is a property of
243
245
  // the journal, not of three test files that happen to assert it.
246
+ const priorMaterials = carriedFindings.filter((finding) => finding.class === "review:material");
244
247
  const declaredPolicy = declaredReviewPolicy(task.files);
245
- const policy = raiseReviewPolicy(declaredPolicy, cfg.review.policy);
248
+ const policy = priorMaterials.length ? "full" : raiseReviewPolicy(declaredPolicy, cfg.review.policy);
246
249
  // PROMOTION: the declared assignment is a claim about paths, and the diff is the evidence. A
247
250
  // judge-only task whose diff left the leaf class is reviewed in full — the claim never outranks
248
251
  // what actually happened, and an empty diff promotes too (a skip earned by an absence is not earned).
@@ -293,15 +296,15 @@ artifactDir, reviewHistory) {
293
296
  // historical seat for every task that never asked for review-tier coupling.
294
297
  const reviewerFloor = task.routingHints?.floor;
295
298
  let rotationSeat;
296
- const reviewer = pickReviewer(author, channels, excludeReviewers ?? [], cfg.review.prefer ?? [], reviewerFloor, reviewHistory, reviewHistory ? (seat) => { rotationSeat = seat; } : undefined);
299
+ const reviewer = pickReviewer(author, channels, excludeReviewers ?? [], cfg.review.prefer ?? [], reviewerFloor, reviewHistory, reviewHistory ? (seat) => { rotationSeat = seat; } : undefined, demotedReviewers);
297
300
  if (!reviewer) {
298
301
  // meta.noEligibleReviewer lets run-gates' review-retry keep the ORIGINAL unparseable result when
299
302
  // the retry finds no second seat — a truthful cause beats a synthetic no-reviewer failure.
300
303
  const reason = reviewerFloor
301
304
  ? `no cross-vendor reviewer available at or above task-declared ${reviewerFloor} floor (diversity rule)`
302
305
  : "no cross-vendor reviewer available (diversity rule)";
303
- return cfg.review.required
304
- ? { gate: "review", pass: false, details: `unreadable — ${reason}; set review.required:false to waive`, meta: { noEligibleReviewer: true, unreadable: true, ...(reviewerFloor ? { reviewerFloor } : {}) } }
306
+ return cfg.review.required || priorMaterials.length > 0
307
+ ? { gate: "review", pass: false, details: `unreadable — ${reason}; ${priorMaterials.length ? "carried materials require a review verdict" : "set review.required:false to waive"}`, meta: { noEligibleReviewer: true, unreadable: true, ...(reviewerFloor ? { reviewerFloor } : {}) } }
305
308
  : { gate: "review", pass: true, details: `WARNING: ${reason} — review waived by config`, meta: { noEligibleReviewer: true, ...(reviewerFloor ? { reviewerFloor } : {}) } };
306
309
  }
307
310
  reviewHistory?.push(channelKey(reviewer));
@@ -327,7 +330,10 @@ ${task.acceptance.map((a) => `- ${renderAcceptanceItem(a)}`).join("\n")}
327
330
 
328
331
  ${renderDeclaredWriteScope(task.files)}
329
332
 
330
- ## Diff
333
+ ${priorMaterials.length ? `## Prior materials this attempt must close
334
+ ${priorMaterials.map((finding) => `Fingerprint: ${finding.fingerprint}\n${finding.note}`).join("\n\n")}
335
+
336
+ ` : ""}## Diff
331
337
  \`\`\`diff
332
338
  ${diff}
333
339
  \`\`\`
@@ -340,18 +346,31 @@ block approval. For a minor concern you have decided not to block on, set "defer
340
346
  one-line "rationale" — it is recorded in the review, never dropped.
341
347
 
342
348
  Respond with ONLY this JSON:
343
- {"nonce": "${nonce}", "approve": true|false, "findings": [{"note": "...", "severity": "material"|"minor", "defer": false, "rationale": ""}], "comments": [{"path": "path/to/file", "line": 42, "body": "actionable feedback"}]}
344
- Approve iff no material finding remains; an empty findings list is a clean approval.
349
+ {"nonce": "${nonce}", "approve": true|false, "resolved": [], "reraised": [], "findings": [{"note": "...", "severity": "material"|"minor", "defer": false, "rationale": ""}], "comments": [{"path": "path/to/file", "line": 42, "body": "actionable feedback"}]}
350
+ For every prior material, put its fingerprint in exactly one of resolved (verified fixed) or reraised
351
+ (still a blocking defect). Use only the listed fingerprints; never omit one or put it in both lists.
352
+ Approve iff no material finding remains and every prior material is resolved.
345
353
  The top-level comments array is optional. Use it only for actionable line-anchored feedback.
346
354
  `;
347
- let concludedOnInactivity = false;
355
+ const artifactId = `${task.id}-${nonce}`;
356
+ const briefPath = artifactDir ? join(artifactDir, `review-brief-${artifactId}.md`) : undefined;
357
+ // Persistence is evidence, not a gate input: a full disk or a removed run dir never fails the gate.
358
+ let savedBrief;
359
+ if (briefPath) {
360
+ try {
361
+ writeFileSync(briefPath, redactSecrets(prompt));
362
+ savedBrief = briefPath;
363
+ }
364
+ catch {
365
+ savedBrief = undefined;
366
+ }
367
+ }
348
368
  const llm = await runLlmDetailed(getAdapter(reviewer.adapter, adapters), reviewer.model, prompt, worktree, via ? {
349
369
  driver: via.driver,
350
370
  keep: via.keep,
351
371
  onSlot: via.onSlot,
352
372
  name: via.nameFor("review", reviewer.adapter),
353
373
  label: via.labelFor("review"),
354
- onInactivity: () => { concludedOnInactivity = true; },
355
374
  } : undefined,
356
375
  // frontier reviewers routinely need >5min on a configured-cap-sized diff, and `claude -p` buffers all
357
376
  // output until completion — runLlm's 300s default killed reviews mid-flight, returning empty
@@ -362,53 +381,80 @@ The top-level comments array is optional. Use it only for actionable line-anchor
362
381
  const provider = modelProvider(reviewer.model, reviewer.vendor);
363
382
  const v = extractVerdictJson(raw, nonce);
364
383
  const findings = v && Array.isArray(v.findings) ? v.findings : null;
384
+ const priorIds = new Set(priorMaterials.map((finding) => finding.fingerprint));
385
+ const closureLists = [v?.resolved, v?.reraised];
386
+ const closureInvalid = !!v && (priorIds.size > 0 || closureLists.some((list) => list !== undefined)) && (closureLists.some((list) => !Array.isArray(list) || list.some((id) => typeof id !== "string" || !priorIds.has(id)))
387
+ || new Set([...(v?.resolved ?? []), ...(v?.reraised ?? [])]).size !== (v?.resolved?.length ?? 0) + (v?.reraised?.length ?? 0)
388
+ || [...priorIds].some((id) => !v?.resolved?.includes(id) && !v?.reraised?.includes(id)));
365
389
  // findings decides the verdict on its own; the legacy path still needs approve + issues to parse.
366
- if (!v || (findings === null && (typeof v.approve !== "boolean" || !Array.isArray(v.issues)))) {
390
+ if (!v || closureInvalid || (findings === null && (typeof v.approve !== "boolean" || !Array.isArray(v.issues)))) {
367
391
  // OBS-196: name the cause and persist the raw bytes — a ruled-on "unparseable" without its
368
392
  // evidence cannot be audited, and a cutoff must never be indistinguishable from a parse defect.
369
- const cause = classifyVerdictCause(raw, nonce, "approve", llm);
370
- const bytes = Buffer.byteLength(raw, "utf8");
393
+ const bytes = llm.seatAuthoredBytes ?? Buffer.byteLength(raw.trim(), "utf8");
394
+ const cause = closureInvalid ? "malformed-verdict" : llm.launchNeverStarted ? "launch-never-started"
395
+ : llm.timedOut ? (bytes > 0 ? "truncated" : "silent")
396
+ : classifyVerdictCause(raw, nonce, "approve", llm);
371
397
  let saved;
372
398
  if (artifactDir) {
373
399
  try {
374
- saved = join(artifactDir, `review-raw-${task.id}-${Date.now()}.txt`);
400
+ saved = join(artifactDir, `review-raw-${artifactId}.txt`);
375
401
  writeFileSync(saved, redactSecrets(raw));
376
402
  }
377
403
  catch {
378
404
  saved = undefined; // persistence is evidence, not a gate input — never fail the gate on it
379
405
  }
380
406
  }
381
- const failure = concludedOnInactivity
382
- ? "review dispatch concluded on the inactivity policy without a structurally valid nonce-bound response; output unparseable"
383
- : cause === "malformed-verdict"
384
- ? "review output unparseable"
385
- : "review dispatch failed — no structurally valid nonce-bound response; output unparseable";
407
+ const failure = cause === "malformed-verdict"
408
+ ? "review output unparseable"
409
+ : "review dispatch failed — no structurally valid nonce-bound response";
386
410
  return {
387
411
  gate: "review",
388
412
  pass: false,
389
- details: `${failure} (reviewer ${reviewer.adapter}:${reviewer.model}; vendor: ${reviewer.vendor}; provider: ${provider}; cause: ${cause}${cause === "timeout" ? `; killed at configured review timeout ${cfg.review.timeoutMs}ms` : ""}${saved ? `; raw saved: ${saved}` : ""}) — failing closed`,
413
+ details: `${failure} (reviewer ${reviewer.adapter}:${reviewer.model}; vendor: ${reviewer.vendor}; provider: ${provider}; cause: ${cause}${llm.timedOut ? `; killed at configured review timeout ${cfg.review.timeoutMs}ms` : ""}${saved ? `; raw saved: ${saved}` : ""}) — failing closed`,
390
414
  meta: {
391
415
  ...policyMeta,
392
416
  ...rotationMeta,
393
417
  reviewer: channelKey(reviewer),
394
418
  vendor: reviewer.vendor,
395
419
  provider,
396
- unparseable: true,
420
+ ...(cause === "malformed-verdict" ? { unparseable: true } : { noVerdict: true, classification: "infra", infra: true }),
397
421
  cause,
398
- ...(cause === "empty-output" ? { bytes } : {}),
399
- ...(cause === "timeout" ? { timeoutMs: cfg.review.timeoutMs } : {}),
400
- ...(concludedOnInactivity ? { classification: "infra", infra: true } : {}),
422
+ bytes, seatAuthoredBytes: bytes,
423
+ ...(saved ? { rawPath: saved } : {}),
424
+ ...(savedBrief ? { briefPath: savedBrief } : {}),
425
+ ...(llm.timedOut ? { timeoutMs: cfg.review.timeoutMs } : {}),
401
426
  },
402
427
  };
403
428
  }
404
429
  const decided = findings !== null
405
430
  ? classifyReviewFindings(findings)
406
431
  : classifyReviewIssues(v.approve, v.issues);
432
+ const reraised = priorMaterials.filter((finding) => v.reraised?.includes(finding.fingerprint));
433
+ if (reraised.length) {
434
+ if (decided.pass)
435
+ decided.headline = "requested changes";
436
+ decided.pass = false;
437
+ // A reviewer may also restate a re-raised material in findings. Preserve the original
438
+ // prose once so an unchanged defect keeps the same failure brief across repair rounds.
439
+ for (const finding of reraised) {
440
+ const line = `- [material] ${finding.note}`;
441
+ if (!decided.lines.includes(line))
442
+ decided.lines.push(line);
443
+ }
444
+ }
407
445
  const prose = `reviewer ${reviewer.adapter}:${reviewer.model} (vendor: ${reviewer.vendor}; provider: ${provider}): ${decided.headline}${decided.lines.length ? "\n" + decided.lines.join("\n") : ""}`;
446
+ const details = appendAnchoredReview(prose, v);
408
447
  return {
409
448
  gate: "review",
410
449
  pass: decided.pass,
411
- details: appendAnchoredReview(prose, v),
412
- meta: { ...policyMeta, ...rotationMeta, reviewer: channelKey(reviewer), vendor: reviewer.vendor, provider },
450
+ details,
451
+ meta: {
452
+ ...policyMeta, ...rotationMeta, reviewer: channelKey(reviewer), vendor: reviewer.vendor, provider,
453
+ ...(priorMaterials.length ? { resolved: v.resolved, reraised: v.reraised } : {}),
454
+ ...(reraised.length ? { findings: [
455
+ ...structuredFindings("review", details).filter((finding) => !reraised.some((prior) => prior.note === finding.note)),
456
+ ...reraised,
457
+ ] } : {}),
458
+ },
413
459
  };
414
460
  }
@@ -4,6 +4,7 @@ import { type GateName, type Task } from "../graph/schema.js";
4
4
  import { type Baseline } from "./baseline.js";
5
5
  import { type GateVia } from "./llm.js";
6
6
  import type { GateResult } from "./types.js";
7
+ import { type StructuredFinding } from "../run/journal.js";
7
8
  export type LoadProvider = () => number;
8
9
  /** Test seam — inject deterministic load samples; production always reads os.loadavg. */
9
10
  export declare function setLoadProviderForTests(provider: LoadProvider): void;
@@ -52,7 +53,9 @@ export interface GateContext {
52
53
  adapters: WorkerAdapter[];
53
54
  cfg: TickmarkrConfig;
54
55
  via?: GateVia;
56
+ carriedFindings?: readonly StructuredFinding[];
55
57
  excludeReviewers?: string[];
58
+ demotedReviewers?: Set<string>;
56
59
  reviewHistory?: string[];
57
60
  artifactDir?: string;
58
61
  pipeline?: "v185" | "legacy";
@@ -590,13 +590,23 @@ export async function runGates(task, ctx) {
590
590
  const dispatch = async (run) => {
591
591
  const captured = await captureLlmDispatches(ctx.adapters, run);
592
592
  invocations.push(...captured.invocations);
593
- return captured.value;
593
+ const rv = captured.value;
594
+ if (rv.meta?.noVerdict === true || rv.meta?.unparseable === true) {
595
+ await ctx.onGate?.({ phase: "note", gate: "review", name: "review-no-verdict", payload: { ...rv.meta }, result: rv });
596
+ if (rv.meta.seatAuthoredBytes === 0 && typeof rv.meta.reviewer === "string"
597
+ && !ctx.demotedReviewers?.has(rv.meta.reviewer)) {
598
+ ctx.demotedReviewers?.add(rv.meta.reviewer);
599
+ await ctx.onGate?.({ phase: "note", gate: "review", name: "review-pool-demotion",
600
+ payload: { reviewer: rv.meta.reviewer, cause: rv.meta.cause, seatAuthoredBytes: 0 }, result: rv });
601
+ }
602
+ }
603
+ return rv;
594
604
  };
595
- let rv = await dispatch((adapters) => reviewGate(task, ctx.worktree, ctx.baseRef, ctx.author, ctx.channels, adapters, ctx.cfg, ctx.via, ctx.excludeReviewers, ctx.artifactDir, ctx.reviewHistory));
605
+ let rv = await dispatch((adapters) => reviewGate(task, ctx.worktree, ctx.baseRef, ctx.author, ctx.channels, adapters, ctx.cfg, ctx.via, ctx.excludeReviewers, ctx.artifactDir, ctx.reviewHistory, ctx.demotedReviewers, ctx.carriedFindings));
596
606
  // OBS-193/574: an unparseable review verdict retries the REVIEW exactly once, preferring a
597
607
  // different adapter. Only a single-adapter eligible pool may fall back to another channel on the
598
608
  // flaked adapter. The flaked verdict never enters results; an exhausted pool preserves its cause.
599
- if (rv.meta?.unparseable === true && typeof rv.meta.reviewer === "string") {
609
+ if ((rv.meta?.unparseable === true || rv.meta?.noVerdict === true) && typeof rv.meta.reviewer === "string") {
600
610
  const flaked = rv.meta.reviewer;
601
611
  const emptyOutput = rv.meta.cause === "empty-output";
602
612
  if (emptyOutput) {
@@ -615,7 +625,7 @@ export async function runGates(task, ctx) {
615
625
  const crossAdapter = pickReviewer(ctx.author, ctx.channels, [...priorExclusions, ...adapterExclusions], ctx.cfg.review.prefer ?? [], task.routingHints?.floor);
616
626
  const exclusion = crossAdapter ? "adapter" : "channel";
617
627
  const retryExclusions = [...priorExclusions, ...(crossAdapter ? adapterExclusions : [flaked])];
618
- const second = await dispatch((adapters) => reviewGate(task, ctx.worktree, ctx.baseRef, ctx.author, ctx.channels, adapters, ctx.cfg, retryVia, retryExclusions, ctx.artifactDir, ctx.reviewHistory));
628
+ const second = await dispatch((adapters) => reviewGate(task, ctx.worktree, ctx.baseRef, ctx.author, ctx.channels, adapters, ctx.cfg, retryVia, retryExclusions, ctx.artifactDir, ctx.reviewHistory, ctx.demotedReviewers, ctx.carriedFindings));
619
629
  if (second.meta?.noEligibleReviewer !== true) {
620
630
  const retried = typeof second.meta?.reviewer === "string" ? second.meta.reviewer : "none";
621
631
  const route = exclusion === "adapter"
@@ -1,10 +1,11 @@
1
1
  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
- import { type ExecutorDriver } from "../drivers/types.js";
4
+ 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 { Journal, type JournalEvent } from "./journal.js";
8
+ export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
8
9
  export { harvestCpuFlatWindowMs, resetHarvestCpuFlatMsForTests, setHarvestCpuFlatMsForTests, workerTreeCpuMs, } from "./stall.js";
9
10
  export interface RunOptions {
10
11
  runId?: string;