tickmarkr 2.5.0 → 2.5.2

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.
@@ -25,6 +25,7 @@ export declare class FakeAdapter implements WorkerAdapter {
25
25
  private scriptPath;
26
26
  id: string;
27
27
  vendor: string;
28
+ busyFrameMarkers: readonly import("./types.js").BusyFrameMarker[];
28
29
  trustDialog: TrustDialog;
29
30
  private script;
30
31
  private attempts;
@@ -1,12 +1,13 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { parseWorkerResult } from "./prompt.js";
4
- import { shq, TokenUsageSchema } from "./types.js";
4
+ import { OPENCODE_BUSY_FRAME_MARKERS, shq, TokenUsageSchema } from "./types.js";
5
5
  // ponytail: deterministic scripted adapter — the whole integration suite runs on it, zero tokens.
6
6
  export class FakeAdapter {
7
7
  scriptPath;
8
8
  id = "fake";
9
9
  vendor = "fake-a";
10
+ busyFrameMarkers = OPENCODE_BUSY_FRAME_MARKERS;
10
11
  // v1.89 T1 / OBS-414: the scripted adapter runs `bash -c` — it has no TUI, so no prompt of any
11
12
  // kind can render. Daemon tests that exercise the auto-answer seam assign a captured declaration
12
13
  // to this field explicitly; that assignment is the falsifier for the claim made here.
@@ -1,3 +1,4 @@
1
1
  import { type WorkerAdapter } from "./types.js";
2
2
  export declare function parseOpencodeModels(raw: string): string[];
3
+ export { OPENCODE_BUSY_FRAME_MARKERS } from "./types.js";
3
4
  export declare const opencode: WorkerAdapter;
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { probeVersion } from "./claude-code.js";
6
6
  import { parseWorkerResult } from "./prompt.js";
7
- import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
7
+ import { channelsFromConfig, MODEL_ID_RE, OPENCODE_BUSY_FRAME_MARKERS, shq, TokenUsageSchema } from "./types.js";
8
8
  // v1.5 MODEL-01: pure parser for `opencode models` (one provider/model id per line, no header).
9
9
  // Live-verified 2026-07-10, opencode 1.17.15. Lines are already the seed id format; drop blanks.
10
10
  export function parseOpencodeModels(raw) {
@@ -13,9 +13,12 @@ export function parseOpencodeModels(raw) {
13
13
  .filter((l) => l.length > 0)
14
14
  .filter((id) => MODEL_ID_RE.test(id));
15
15
  }
16
+ // Keep the adapter export available without making the fake adapter import probe code.
17
+ export { OPENCODE_BUSY_FRAME_MARKERS } from "./types.js";
16
18
  export const opencode = {
17
19
  id: "opencode",
18
20
  vendor: "mixed",
21
+ busyFrameMarkers: OPENCODE_BUSY_FRAME_MARKERS,
19
22
  probeCwd: "neutral",
20
23
  probe: async () => probeVersion("opencode"),
21
24
  channels: (cfg) => channelsFromConfig("opencode", cfg),
@@ -150,6 +150,19 @@ export declare function matchesInputBox(paneText: string, inputBox: InputBox): b
150
150
  export declare function matchesEmptyInputBox(paneText: string, inputBox: InputBox): boolean;
151
151
  export declare function matchesOccupiedInputBox(paneText: string, inputBox: InputBox): boolean;
152
152
  export declare function missingInputStateDeclarations(inputBox: InputBox | undefined): string[];
153
+ export interface BusyFrameMarker {
154
+ name: string;
155
+ match(paneText: string): boolean;
156
+ }
157
+ export declare const OPENCODE_BUSY_FRAME_MARKERS: readonly BusyFrameMarker[];
158
+ export declare class SettledTrailerTracker {
159
+ private readonly markers;
160
+ private idleSamples;
161
+ readonly busyMarkersSeen: Set<string>;
162
+ settled: boolean;
163
+ constructor(markers: readonly BusyFrameMarker[]);
164
+ sample(paneText: string, trailer: boolean): boolean;
165
+ }
153
166
  export interface WorkerAdapter {
154
167
  id: string;
155
168
  vendor: string;
@@ -174,6 +187,7 @@ export interface WorkerAdapter {
174
187
  trust?(repoRoot: string): TrustVerdict;
175
188
  trustDialog: TrustDialog;
176
189
  inputBox?: InputBox;
190
+ busyFrameMarkers?: readonly BusyFrameMarker[];
177
191
  hardcodedFlags?: {
178
192
  binary: string;
179
193
  flags: string[];
@@ -207,6 +207,31 @@ export function missingInputStateDeclarations(inputBox) {
207
207
  missing.push("occupiedMatch");
208
208
  return missing;
209
209
  }
210
+ // OBS-957 capture retained under fixtures/opencode (the captured UI identifies as omp).
211
+ // Anchor activity glyphs to chrome/tool rows so a quoted spinner in a summary is harmless.
212
+ export const OPENCODE_BUSY_FRAME_MARKERS = [
213
+ { name: "unchecked TODO", match: (text) => /(?:^|\n)[^\n]*[├└]─[^\n]*☐/.test(text) || /(?:^|\n)\s*[-*] \[ \]/.test(text) },
214
+ { name: "in-flight tool", match: (text) => /(?:^|\n)\s*⎋ [^\n]+/.test(text) },
215
+ { name: "spinner", match: (text) => /(?:^|\n)(?:[╭│][─ ]*|\s*)[⠁-⣿](?:\s|$)/.test(text) },
216
+ ];
217
+ export class SettledTrailerTracker {
218
+ markers;
219
+ idleSamples = 0;
220
+ busyMarkersSeen = new Set();
221
+ settled = false;
222
+ constructor(markers) {
223
+ this.markers = markers;
224
+ }
225
+ sample(paneText, trailer) {
226
+ const busy = this.markers.filter((marker) => marker.match(paneText));
227
+ if (trailer)
228
+ for (const marker of busy)
229
+ this.busyMarkersSeen.add(marker.name);
230
+ this.idleSamples = trailer && busy.length === 0 ? this.idleSamples + 1 : 0;
231
+ this.settled = this.idleSamples >= 2;
232
+ return this.settled;
233
+ }
234
+ }
210
235
  export function channelsFromConfig(adapterId, cfg) {
211
236
  const e = cfg.tiers[adapterId];
212
237
  if (!e)
@@ -114,7 +114,7 @@ export function approvalRunOwner(cwd, runId) {
114
114
  /** The one sentence that says who enacts this release and what it buys. */
115
115
  export function approvalEnactment(token, run) {
116
116
  if (run.live) {
117
- return `the live daemon enacts this at its next task boundary — it will ${APPROVAL_ENACTS[token]}`;
117
+ return `the live daemon enacts this at its next task boundary — it will ${APPROVAL_ENACTS[token]}, including in the approval window or by cancelling an in-progress tip verify`;
118
118
  }
119
119
  if (run.blockingRunId) {
120
120
  return `release recorded; live run \`${run.blockingRunId}\` holds the repository lock, so resume \`${run.runId}\` after it ends to ${APPROVAL_ENACTS[token]}`;
@@ -162,6 +162,7 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
162
162
  gates: {
163
163
  build?: string | undefined;
164
164
  test?: string | undefined;
165
+ tipTest?: string | undefined;
165
166
  lint?: string | undefined;
166
167
  diffCap?: number | undefined;
167
168
  byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
@@ -278,6 +279,7 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
278
279
  gates: {
279
280
  build?: string | undefined;
280
281
  test?: string | undefined;
282
+ tipTest?: string | undefined;
281
283
  lint?: string | undefined;
282
284
  diffCap?: number | undefined;
283
285
  byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
@@ -1,3 +1,5 @@
1
+ export type DetectedHost = "herdr" | "orca" | "subprocess";
2
+ export declare function detectedHost(env?: NodeJS.ProcessEnv): DetectedHost;
1
3
  /** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
2
4
  export type InitIO = {
3
5
  input?: NodeJS.ReadStream;
@@ -4,6 +4,7 @@ import { join, relative } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { parseArgs } from "node:util";
7
+ import { parse as parseYaml } from "yaml";
7
8
  import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReuse } from "../../adapters/registry.js";
8
9
  import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
9
10
  import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
@@ -113,7 +114,7 @@ A run is green only when ALL of these hold: the run-end event exists in the jour
113
114
 
114
115
  ### Verified handoffs
115
116
 
116
- When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the target pane afterward; never report "relayed" without read-back.
117
+ When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the TARGET composer afterward; if the sent text still sits unsubmitted after ~15 s, send-keys enter to that pane once and read again (OBS-975); never report "relayed" without read-back.
117
118
 
118
119
  ### Orient before you act — this block may be the ONLY guidance your host loaded
119
120
 
@@ -218,7 +219,14 @@ const describeDrift = (d) => [
218
219
  // should not be asked again just because a copy drifted. Staleness is a different question with a
219
220
  // different answer (`--agent --force`), and it is reported by name on every init path below.
220
221
  const skillsInstalled = (cwd) => hostTargets(cwd).every((t) => AGENT_SKILLS.every((s) => existsSync(join(t.skillsDir, s, "SKILL.md"))));
221
- const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : orcaHostDetected() ? "orca" : "auto";
222
+ export function detectedHost(env = process.env) {
223
+ if (env.HERDR_ENV === "1")
224
+ return "herdr";
225
+ if (orcaHostDetected(env))
226
+ return "orca";
227
+ return "subprocess";
228
+ }
229
+ const wizardDriverDefault = () => "auto";
222
230
  function packageName(cwd) {
223
231
  try {
224
232
  const parsed = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
@@ -323,6 +331,7 @@ async function runInitWizard(cwd, input, output) {
323
331
  visibilityLlm: DEFAULT_CONFIG.visibility.llm,
324
332
  offerSkills: !skillsInstalled(cwd),
325
333
  skillsDefault: existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd),
334
+ detectedHost: detectedHost(),
326
335
  },
327
336
  input,
328
337
  output,
@@ -368,8 +377,37 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
368
377
  }
369
378
  const repoConfigPath = join(tickmarkrDir(cwd), "config.yaml");
370
379
  const repoConfigExists = existsSync(repoConfigPath);
371
- if (repoConfigExists)
372
- notes.push(`kept existing ${repoConfigPath}`);
380
+ if (repoConfigExists) {
381
+ if (values.force) {
382
+ const currentRaw = readFileSync(repoConfigPath, "utf8");
383
+ let parsedDriver;
384
+ try {
385
+ const parsed = parseYaml(currentRaw);
386
+ if (typeof parsed?.driver === "string")
387
+ parsedDriver = parsed.driver;
388
+ }
389
+ catch {
390
+ // malformed: fall back to regex
391
+ }
392
+ if (!parsedDriver) {
393
+ const m = currentRaw.match(/^\s*driver:\s*([^\s#]+)/m);
394
+ if (m)
395
+ parsedDriver = m[1];
396
+ }
397
+ const host = detectedHost();
398
+ if (parsedDriver && parsedDriver === host) {
399
+ const rewritten = currentRaw.replace(new RegExp(`^(\\s*driver:\\s*)${parsedDriver}\\b.*$`, "m"), `$1auto # auto — resolves to ${host} here`);
400
+ writeFileSync(repoConfigPath, rewritten);
401
+ notes.push(`rewrote driver: ${parsedDriver} to auto in ${repoConfigPath}`);
402
+ }
403
+ else {
404
+ notes.push(`kept existing ${repoConfigPath}`);
405
+ }
406
+ }
407
+ else {
408
+ notes.push(`kept existing ${repoConfigPath}`);
409
+ }
410
+ }
373
411
  const specPath = join(cwd, SCAFFOLD_SPEC);
374
412
  if (existsSync(specPath)) {
375
413
  notes.push(`kept existing ${specPath}`);
@@ -417,7 +455,9 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
417
455
  // so costs one clause; everything else stops here. Esc and ctrl+c both land here.
418
456
  return `init: wizard quit — nothing further run (scaffolding kept: ${notes.filter((n) => n.startsWith("wrote")).length} file(s) written above). Re-run tickmarkr init to continue.`;
419
457
  }
420
- writeFileSync(repoConfigPath, configTemplate(wizard.overlay));
458
+ const host = detectedHost();
459
+ const renderedConfig = configTemplate(wizard.overlay).replace(/^driver: auto$/m, `driver: auto # auto — resolves to ${host} here`);
460
+ writeFileSync(repoConfigPath, renderedConfig);
421
461
  notes.push(`wrote ${repoConfigPath}`);
422
462
  if (wizard.installSkills) {
423
463
  await installAgentFiles(cwd, values.force ?? false, wizard.installDocs || (values.docs ?? false), notes);
@@ -462,7 +502,7 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
462
502
  const next = nextSteps(cwd, SCAFFOLD_SPEC);
463
503
  if (!visual())
464
504
  return `${notes.join("\n")}\n${doc}\n${next}\n${ENVIRONMENTS_FOOTER}`;
465
- const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
505
+ const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended|rewrote)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
466
506
  const footerRows = ENVIRONMENTS_FOOTER.split("\n").slice(1).map((line) => {
467
507
  const separator = line.indexOf(" — ");
468
508
  return kvRow(line.slice(2, separator), line.slice(separator + 1), 12);
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
@@ -7,7 +7,7 @@ import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
7
7
  import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
8
8
  import { classifyContextPath } from "../../compile/native.js";
9
9
  import { DEFAULT_CONFIG, effectiveReviewPolicy, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
10
- import { loadGraph } from "../../graph/graph.js";
10
+ import { graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
11
11
  import { renderAcceptanceItem } from "../../graph/schema.js";
12
12
  import { resolveRunMode } from "../../run/daemon.js";
13
13
  import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
@@ -15,7 +15,7 @@ import { staffLedEvidence } from "../../route/profile.js";
15
15
  import { route, RoutingError } from "../../route/router.js";
16
16
  import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
17
17
  import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
18
- import { Journal, loadRoutingProfile } from "../../run/journal.js";
18
+ import { Journal, loadRoutingProfile, recordedGraphDefinitionHash } from "../../run/journal.js";
19
19
  import { harnessLine, resolveHarness } from "../harness.js";
20
20
  import { channelKey, shq } from "../../adapters/types.js";
21
21
  import { shGit } from "../../run/git.js";
@@ -40,7 +40,10 @@ const stylizePlan = (out) => {
40
40
  const fleetCanCrossVendorReview = (channels) => {
41
41
  for (let i = 0; i < channels.length; i++)
42
42
  for (let j = 0; j < channels.length; j++)
43
- if (i !== j && channels[i].vendor !== channels[j].vendor && modelId(channels[i].model) !== modelId(channels[j].model))
43
+ if (i !== j
44
+ && channels[i].vendor !== channels[j].vendor
45
+ && modelProvider(channels[i].model, channels[i].vendor) !== modelProvider(channels[j].model, channels[j].vendor)
46
+ && modelId(channels[i].model) !== modelId(channels[j].model))
44
47
  return true;
45
48
  return false;
46
49
  };
@@ -121,6 +124,43 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
121
124
  throw new Error(`--mode must be one of ${ROUTING_MODES.join(" | ")} (got ${values.mode})`);
122
125
  }
123
126
  const g = loadGraph(cwd);
127
+ const gHash = graphDefinitionHash(g);
128
+ const runsRoot = join(cwd, stateDirName(cwd), "runs");
129
+ let matchingEvents;
130
+ let matchingRunId;
131
+ if (existsSync(runsRoot)) {
132
+ const locked = runLockRunId(cwd);
133
+ const candidates = readdirSync(runsRoot)
134
+ .filter((d) => d.startsWith("run-") && existsSync(join(runsRoot, d, "journal.jsonl")))
135
+ .sort();
136
+ const ordered = locked && candidates.includes(locked)
137
+ ? [locked, ...candidates.filter((id) => id !== locked).reverse()]
138
+ : candidates.reverse();
139
+ for (const runId of ordered) {
140
+ try {
141
+ const events = Journal.open(cwd, runId).read();
142
+ if (recordedGraphDefinitionHash(events) === gHash) {
143
+ matchingEvents = events;
144
+ matchingRunId = runId;
145
+ break;
146
+ }
147
+ }
148
+ catch {
149
+ // ignore unreadable or torn journals
150
+ }
151
+ }
152
+ }
153
+ const hasRun = matchingEvents !== undefined;
154
+ const reviewHistory = matchingEvents
155
+ ? matchingEvents
156
+ .filter((event) => event.event === "gate-result" && event.data.gate === "review" && typeof event.data.reviewer === "string")
157
+ .map((event) => event.data.reviewer)
158
+ : [];
159
+ // OBS-947 repair: a resumed run re-dispatches only tasks whose replayed status is pending (or that the
160
+ // journal never mentions). done, failed and human are all skipped alike, so rendering their preview
161
+ // picks must not advance the shared reviewHistory — a phantom draw wraps the LRU and makes a later
162
+ // pending task print the ranked-first seat while the journal's actual next draw is a later channel.
163
+ const taskStatuses = matchingRunId ? Journal.open(cwd, matchingRunId).replayStatuses() : new Map();
124
164
  const oracleRefusals = new Map();
125
165
  const oracleAdvisories = new Map();
126
166
  if (g.tasks.some((task) => task.acceptance.some((item) => typeof item === "object" && item.oracle === "test"))) {
@@ -306,10 +346,22 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
306
346
  const policy = effectiveReviewPolicy(task.files, cfg.review);
307
347
  if (policy === "judge-only")
308
348
  return { line: "none — reviewPolicy judge-only: declared leaf paths", cost: 0 };
309
- const reviewer = pickReviewer(author, pools.review, [], cfg.review.prefer ?? [], task.routingHints?.floor);
349
+ let rotationSeat = 1;
350
+ let rotationCount = 0;
351
+ const reviewer = pickReviewer(author, pools.review, [], cfg.review.prefer ?? [], task.routingHints?.floor, reviewHistory, (seat, count) => { rotationSeat = seat; rotationCount = count; });
310
352
  if (!reviewer)
311
353
  return { line: "none — no cross-vendor reviewer available", cost: 0 };
312
- return { line: `${channelKey(reviewer)} — reviewPolicy full`, cost: seatCost(reviewer) };
354
+ // Advance the simulated rotation only for tasks the run would actually dispatch next — the same
355
+ // rule as readyTasks (src/graph/graph.ts): pending, with every dependency already done. A pending
356
+ // task behind a parked/failed/pending dependency cannot run, so it consumes no draw (RULING-229-14).
357
+ const isPending = (id) => { const s = taskStatuses.get(id); return s === undefined || s === "pending"; };
358
+ const ready = isPending(task.id) && task.deps.every((d) => taskStatuses.get(d) === "done");
359
+ if (hasRun && ready)
360
+ reviewHistory.push(channelKey(reviewer));
361
+ const rotationNote = rotationCount > 1
362
+ ? (hasRun ? ` (seat ${rotationSeat} of ${rotationCount})` : ` (seat 1 of ${rotationCount} — rotation applies at run time)`)
363
+ : "";
364
+ return { line: `${channelKey(reviewer)}${rotationNote} — reviewPolicy full`, cost: seatCost(reviewer) };
313
365
  };
314
366
  const routed = [];
315
367
  for (const t of g.tasks) {
@@ -268,7 +268,9 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
268
268
  pass = "pass (same-provider — independence not established)";
269
269
  }
270
270
  }
271
- lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}`);
271
+ const resolved = gate === "review" && g.data.pass === true && Array.isArray(g.data.resolved)
272
+ ? g.data.resolved.filter((id) => typeof id === "string") : [];
273
+ lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}${resolved.length ? `; resolved: ${resolved.join(", ")}` : ""}`);
272
274
  }
273
275
  for (const row of leg2) {
274
276
  const pass = row.data.pass === true ? "pass" : row.data.pass === false ? "fail" : EM;
@@ -4,6 +4,11 @@ import { type JournalEvent } from "../../run/journal.js";
4
4
  * it is a phase counter, while a phase-start CARRYING a gate is the gate start the rail draws. */
5
5
  export declare const TTY_NOISE_EVENTS: readonly ["worker-contact", "worker-status"];
6
6
  type RailTone = "pass" | "fail" | "attention" | "active" | "neutral";
7
+ /** Approval-close lifecycle labels extend the established closed rail vocabulary. */
8
+ export declare const APPROVAL_RAIL_ROWS: Record<string, {
9
+ label: string;
10
+ tone: RailTone;
11
+ }>;
7
12
  /** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
8
13
  * own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
9
14
  * or `ok` datum on the event overrides the default tone, so one gate row can read either way.
@@ -33,6 +33,12 @@ const RAIL_TONES = {
33
33
  active: { glyph: GLYPHS.pointer, paint: LIVE.running },
34
34
  neutral: { glyph: GLYPHS.neutral, paint: LIVE.chrome },
35
35
  };
36
+ /** Approval-close lifecycle labels extend the established closed rail vocabulary. */
37
+ export const APPROVAL_RAIL_ROWS = {
38
+ "approval-window-start": { label: "approval window", tone: "attention" },
39
+ "approval-window-expired": { label: "approval window expired", tone: "attention" },
40
+ "tip-verify-cancelled": { label: "tip verify cancelled", tone: "attention" },
41
+ };
36
42
  /** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
37
43
  * own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
38
44
  * or `ok` datum on the event overrides the default tone, so one gate row can read either way.
@@ -116,8 +122,13 @@ export const RAIL_ROWS = {
116
122
  // gate starts and verdicts
117
123
  "phase-start": { label: "gate start", tone: "active" },
118
124
  "gate-result": { label: "gate", tone: "pass" },
125
+ "baseline-wait": { label: "baseline wait", tone: "active" },
126
+ "suite-budget": { label: "suite budget", tone: "attention" },
119
127
  "gate-reused": { label: "gate reused", tone: "neutral" },
120
128
  "judge-retry": { label: "judge retry", tone: "attention" },
129
+ "review-no-verdict": { label: "review unavailable", tone: "attention" },
130
+ "review-pool-demotion": { label: "review seat demoted", tone: "attention" },
131
+ "review-infra-retry": { label: "review infra retry", tone: "attention" },
121
132
  "review-retry": { label: "review retry", tone: "attention" },
122
133
  // repairs and escalations
123
134
  "repair-dispatch": { label: "repair", tone: "attention" },
@@ -304,6 +315,11 @@ const RAIL_PROJECTION = {
304
315
  ? `gated ${d.gatedCommit.slice(0, 12)}, tip ${d.branchTip.slice(0, 12)}`
305
316
  : undefined,
306
317
  "trust-auto-answer": (d) => typeof d.adapter === "string" ? `${d.adapter}${typeof d.phase === "string" ? ` ${d.phase}` : ""}` : undefined,
318
+ // SB-1: the census the ceiling released beside, and the budget the round ran under versus the one it
319
+ // would have had on an empty census — none of `{count, occupancyCap, conservativeCap}` is on the ladder
320
+ "suite-budget": (d) => typeof d.count === "number" && typeof d.conservativeCap === "number" && typeof d.occupancyCap === "number"
321
+ ? `beside ${d.count}, cap ${d.conservativeCap} not ${d.occupancyCap}`
322
+ : undefined,
307
323
  };
308
324
  // The row's text: the formatter's detail first, then the salient fields that detail could not carry.
309
325
  // Appending (never prefixing) keeps the operator's prose at the front of the row, so the clip a
@@ -330,7 +346,7 @@ const clipCells = (text, cells) => cells <= 0 ? "" : cellWidth(text) <= cells ?
330
346
  export function narrationRow(event, runId, columns = process.stdout.columns ?? 80) {
331
347
  if (TTY_NOISE_EVENTS.includes(event.event))
332
348
  return null;
333
- const row = RAIL_ROWS[event.event];
349
+ const row = APPROVAL_RAIL_ROWS[event.event] ?? RAIL_ROWS[event.event];
334
350
  if (!row)
335
351
  return null;
336
352
  if (event.event === "phase-start" && typeof event.data.gate !== "string")
@@ -847,6 +847,9 @@ acceptance is required on every task (a nested list of observable outcomes).
847
847
  hard value anywhere in the domain, the criterion asserts a universal that may be FALSE ABOUT THE
848
848
  WORLD — bound it or say where it stops holding, rather than demanding a value that does not exist.
849
849
 
850
+ - A millisecond ceiling in a test is a BUDGET for the SLOWEST RUNNER.
851
+ Any ceiling under one second must carry a SLOWEST-RUNNER note and a member that OVERRUNS it.
852
+
850
853
  PICK THE CRITERION FORM FROM WHO COULD BE WRONG:
851
854
  - When the WORKER could be wrong because it can choose the value, use "test:" and pin the exact
852
855
  literal it could otherwise choose; an example selected by its implementer proves only itself.
@@ -240,6 +240,7 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
240
240
  gates: z.ZodObject<{
241
241
  build: z.ZodOptional<z.ZodString>;
242
242
  test: z.ZodOptional<z.ZodString>;
243
+ tipTest: z.ZodOptional<z.ZodString>;
243
244
  lint: z.ZodOptional<z.ZodString>;
244
245
  diffCap: z.ZodOptional<z.ZodNumber>;
245
246
  byShape: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodEnum<{
@@ -339,6 +340,11 @@ export type InitConfigOverlay = {
339
340
  llm?: TickmarkrConfig["visibility"]["llm"];
340
341
  };
341
342
  };
343
+ /**
344
+ * Default config overlay template.
345
+ * Seam: gates.test defines the per-task gate and baseline test command;
346
+ * gates.tipTest optionally defines the integration tip verify test command (defaults to gates.test).
347
+ */
342
348
  export declare function configTemplate(overlay?: InitConfigOverlay): string;
343
349
  export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta, renderFleetOverlayWrite, repoOverlayYaml, serializeFleetOverlay, unifiedYamlDiff, } from "./fleet-overlay.js";
344
350
  export type { FleetOverlayWrite } from "./fleet-overlay.js";
@@ -337,6 +337,7 @@ export const TickmarkrConfigSchema = z.object({
337
337
  gates: z.object({
338
338
  build: z.string(),
339
339
  test: z.string(),
340
+ tipTest: z.string(),
340
341
  lint: z.string(),
341
342
  diffCap: z.number().int().positive(),
342
343
  byShape: z.partialRecord(z.enum(SHAPES), ShapeGateParticipationSchema).optional(),
@@ -756,6 +757,11 @@ export function overlayBytesLoadError(repoRoot, bytes, opts = {}) {
756
757
  return e.message;
757
758
  }
758
759
  }
760
+ /**
761
+ * Default config overlay template.
762
+ * Seam: gates.test defines the per-task gate and baseline test command;
763
+ * gates.tipTest optionally defines the integration tip verify test command (defaults to gates.test).
764
+ */
759
765
  export function configTemplate(overlay) {
760
766
  const base = `# tickmarkr config overlay — merges over built-in defaults (repo beats global beats defaults)
761
767
  # concurrency: 3
@@ -3,9 +3,8 @@ export declare const MAX_BUF: number;
3
3
  export declare const HERDR_CONTROL_VARS: readonly ["HERDR_ENV", "HERDR_SOCKET_PATH", "ORCA_TERMINAL_HANDLE", "ORCA_PANE_KEY", "ORCA_TAB_ID"];
4
4
  /**
5
5
  * Copy of worker env with the fork cap applied and host control-plane vars stripped.
6
- * The cap is the one the enclosing run resolved (resolvedForkCap) — a worker's suites divide the
7
- * same machine the gate shells do, so both seams have to read the same run-owned number rather
8
- * than a flat constant. The operator's own export still wins.
6
+ * Workers retain the run's concurrency-derived cap (resolvedForkCap), independently of
7
+ * verification's suite-window budget. The operator's own export still wins.
9
8
  */
10
9
  export declare function sealHerdrEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
11
10
  /** Pane/login-shell form of the same host-neutral worker env seal (herdr seed + daemon setup). */
@@ -22,9 +22,8 @@ export const HERDR_CONTROL_VARS = [
22
22
  ];
23
23
  /**
24
24
  * Copy of worker env with the fork cap applied and host control-plane vars stripped.
25
- * The cap is the one the enclosing run resolved (resolvedForkCap) — a worker's suites divide the
26
- * same machine the gate shells do, so both seams have to read the same run-owned number rather
27
- * than a flat constant. The operator's own export still wins.
25
+ * Workers retain the run's concurrency-derived cap (resolvedForkCap), independently of
26
+ * verification's suite-window budget. The operator's own export still wins.
28
27
  */
29
28
  export function sealHerdrEnv(env = process.env) {
30
29
  const out = { ...env };
@@ -31,6 +31,8 @@ export interface BaselineCommand {
31
31
  durationMs?: number;
32
32
  /** Sum of the per-file durations named by the runner; null when its output names none. */
33
33
  fileDurationSumMs?: number | null;
34
+ /** Total files reported by the runner summary; null when unavailable. */
35
+ fileCount?: number | null;
34
36
  /** fileDurationSumMs / durationMs — average implied file concurrency, not a configured fork count. */
35
37
  impliedParallelism?: number | null;
36
38
  /** The slowest per-file entry named by the runner; null when per-file timing is unavailable. */
@@ -158,6 +160,7 @@ export declare function detectVacuousOracles(cwd: string, tasks: ReadonlyArray<{
158
160
  }>): Promise<VacuousOracleWarning[]>;
159
161
  export declare function compareToBaseline(cwd: string, commands: Record<string, string>, baseline: Baseline, enabled: string[], opts?: {
160
162
  rerunOf?: HostStarvedRerun;
163
+ infraRerun?: HostStarvedRerun;
161
164
  }): Promise<GateResult[]>;
162
165
  export interface HostStarvedRerun {
163
166
  durationMs: number;
@@ -178,4 +181,10 @@ interface CalmWindow {
178
181
  }
179
182
  export declare function setCalmWindowForTests(over: Partial<CalmWindow>): void;
180
183
  export declare function resetCalmWindowForTests(): void;
184
+ export declare function waitForCalmWindow(): Promise<number>;
185
+ /** Summary totals survive digit-normalized fingerprints and exclude test-owned echoed output. */
186
+ export declare function runnerFileCount(raw: string): number | null;
187
+ export declare function fileCountDeficit(entry: BaselineCommand | undefined, raw: string): string | undefined;
188
+ /** Both readers classify fresh evidence first, retaining the whole-output guard against infra forgiveness. */
189
+ export declare function classifyFreshRunnerOutput(entry: BaselineCommand | undefined, raw: string, code: number): FailureClassification | undefined;
181
190
  export {};