tickmarkr 2.5.1 → 2.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/adapters/qwen.d.ts +1 -0
  2. package/dist/adapters/qwen.js +8 -0
  3. package/dist/adapters/types.d.ts +1 -0
  4. package/dist/cli/commands/approve.js +1 -1
  5. package/dist/cli/commands/doctor.d.ts +8 -0
  6. package/dist/cli/commands/doctor.js +50 -1
  7. package/dist/cli/commands/init.js +1 -1
  8. package/dist/cli/commands/run.d.ts +5 -0
  9. package/dist/cli/commands/run.js +16 -1
  10. package/dist/cli/commands/status.js +8 -14
  11. package/dist/compile/native.js +3 -0
  12. package/dist/compile/ownership.d.ts +9 -0
  13. package/dist/compile/ownership.js +65 -2
  14. package/dist/config/config.d.ts +6 -0
  15. package/dist/config/config.js +6 -0
  16. package/dist/drivers/subprocess.d.ts +2 -3
  17. package/dist/drivers/subprocess.js +2 -3
  18. package/dist/gates/baseline.d.ts +13 -0
  19. package/dist/gates/baseline.js +99 -30
  20. package/dist/gates/llm.d.ts +1 -1
  21. package/dist/gates/llm.js +35 -13
  22. package/dist/gates/review.d.ts +11 -0
  23. package/dist/gates/review.js +67 -18
  24. package/dist/gates/run-gates.js +2 -2
  25. package/dist/run/daemon.d.ts +8 -3
  26. package/dist/run/daemon.js +3198 -2934
  27. package/dist/run/git.d.ts +4 -2
  28. package/dist/run/git.js +11 -23
  29. package/dist/run/journal.js +16 -7
  30. package/dist/run/merge.d.ts +1 -1
  31. package/dist/run/merge.js +126 -18
  32. package/dist/run/operator-state.d.ts +1 -1
  33. package/dist/run/operator-state.js +5 -9
  34. package/package.json +1 -1
  35. package/skills/tickmarkr-auto/SKILL.md +1 -1
  36. package/skills/tickmarkr-loop/SKILL.md +1 -1
  37. package/skills/tickmarkr-overseer/SKILL.md +61 -4
  38. package/skills/tickmarkr-overseer/scripts/watch-context.sh +5 -2
  39. package/skills/tickmarkr-overseer/scripts/watch-launch.sh +38 -0
@@ -1,5 +1,6 @@
1
1
  import { type ClassifiedWorkerResult } from "./prompt.js";
2
2
  import { type WorkerAdapter } from "./types.js";
3
3
  export declare const QWEN_VERSION_IDENTITY: RegExp;
4
+ export declare const QWEN_HARNESS_BANNER_ROWS: readonly ["⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.", "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice."];
4
5
  export declare function parseQwenResult(raw: string, nonce: string): ClassifiedWorkerResult;
5
6
  export declare const qwen: WorkerAdapter;
@@ -3,6 +3,13 @@ import { parseWorkerResult } from "./prompt.js";
3
3
  import { channelsFromConfig, shq, } from "./types.js";
4
4
  export const QWEN_VERSION_IDENTITY = /^\d+\.\d+\.\d+/;
5
5
  const QWEN_SKIP_UPDATE = "QWEN_CODE_SKIP_UPDATE_CHECK_ONCE=true";
6
+ // Verbatim stderr from tests/fixtures/qwen/safe-mode.stderr (qwen 0.21.15). These bytes are launch
7
+ // harness, not evidence that a reviewer took a turn. Keep the declaration closed and row-shaped so
8
+ // llm.ts can sweep partial terminal paints without a broad SAFE MODE/warning regex.
9
+ export const QWEN_HARNESS_BANNER_ROWS = [
10
+ "⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.",
11
+ "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.",
12
+ ];
6
13
  function decodeQwenEvents(events) {
7
14
  const text = [];
8
15
  let failed = false;
@@ -136,6 +143,7 @@ export const qwen = {
136
143
  channels: (cfg) => channelsFromConfig("qwen", cfg),
137
144
  hardcodedFlags: { binary: "qwen", flags: ["--safe-mode", "--approval-mode", "-m", "-o", "-p"] },
138
145
  headlessCommand: (promptFile, model) => `${QWEN_SKIP_UPDATE} qwen --safe-mode --approval-mode yolo -m ${shq(model)} -o json -p '' < ${shq(promptFile)}`,
146
+ harnessBannerRows: QWEN_HARNESS_BANNER_ROWS,
139
147
  // OBS-905: qwen has NO interactive form. The `-i "$(cat prompt)"` TUI launch put the whole prompt in
140
148
  // argv (the OBS-889 leak-and-census shape) and produced a rendered transcript the JSON decoder above
141
149
  // can never read — under the herdr driver every qwen task read "unparseable" and merged only by harvest.
@@ -171,6 +171,7 @@ export interface WorkerAdapter {
171
171
  probe(): Promise<AuthHealth>;
172
172
  channels(cfg: TickmarkrConfig): BillingChannel[];
173
173
  headlessCommand(promptFile: string, model: string): string;
174
+ harnessBannerRows?: readonly string[];
174
175
  interactiveCommand(promptFile: string, model: string): string | null;
175
176
  interactiveSeed?: InteractiveSeed;
176
177
  resumeCommand?(sessionId: string, promptFile: string, model: string): string;
@@ -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]}`;
@@ -8,6 +8,12 @@ import { type VitestListResult } from "../../gates/acceptance.js";
8
8
  * concatenation and publishes no index, so the release listing is the only enumerable surface. */
9
9
  export declare const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
10
10
  export declare const LIVEBENCH_TABLE_MAX_AGE_DAYS = 90;
11
+ type ReviewDemotionSummary = {
12
+ reviewer: string;
13
+ count: number;
14
+ causes: Record<string, number>;
15
+ };
16
+ export declare function recentReviewDemotions(cwd: string, lastRuns?: number): ReviewDemotionSummary[];
11
17
  export type DoctorOpts = {
12
18
  banner?: boolean;
13
19
  kimiTurnProbe?: (cwd: string) => Promise<KimiDoctorTurnResult>;
@@ -162,6 +168,7 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
162
168
  gates: {
163
169
  build?: string | undefined;
164
170
  test?: string | undefined;
171
+ tipTest?: string | undefined;
165
172
  lint?: string | undefined;
166
173
  diffCap?: number | undefined;
167
174
  byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
@@ -278,6 +285,7 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
278
285
  gates: {
279
286
  build?: string | undefined;
280
287
  test?: string | undefined;
288
+ tipTest?: string | undefined;
281
289
  lint?: string | undefined;
282
290
  diffCap?: number | undefined;
283
291
  byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
@@ -1,6 +1,6 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync } from "node:fs";
3
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { detectPackageManager, turboContinueFindings } from "../../gates/baseline.js";
6
6
  import { version } from "./version.js";
@@ -26,6 +26,46 @@ const initialFetch = globalThis.fetch;
26
26
  const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
27
27
  const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
28
28
  const attentionRow = (text) => ` ${statusRow("warn", text)}`;
29
+ // Use the routing profile's established 50-run recency horizon without importing journal machinery
30
+ // into doctor. Torn/malformed rows are ignored one at a time: diagnostics must survive a killed write.
31
+ export function recentReviewDemotions(cwd, lastRuns = 50) {
32
+ const runs = join(cwd, stateDirName(cwd), "runs");
33
+ if (!existsSync(runs))
34
+ return [];
35
+ const grouped = new Map();
36
+ const ids = readdirSync(runs, { withFileTypes: true })
37
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("run-")
38
+ && existsSync(join(runs, entry.name, "journal.jsonl")))
39
+ .map((entry) => entry.name).sort().slice(-lastRuns);
40
+ for (const id of ids) {
41
+ const lines = readFileSync(join(runs, id, "journal.jsonl"), "utf8").split("\n");
42
+ for (const line of lines) {
43
+ if (!line.trim())
44
+ continue;
45
+ let row;
46
+ try {
47
+ row = JSON.parse(line);
48
+ }
49
+ catch {
50
+ continue;
51
+ }
52
+ if (!row || typeof row !== "object")
53
+ continue;
54
+ const event = row;
55
+ if (event.event !== "review-pool-demotion" || !event.data || typeof event.data !== "object")
56
+ continue;
57
+ const data = event.data;
58
+ if (typeof data.reviewer !== "string" || !data.reviewer.trim())
59
+ continue;
60
+ const cause = typeof data.cause === "string" && data.cause.trim() ? data.cause : "unknown";
61
+ const summary = grouped.get(data.reviewer) ?? { reviewer: data.reviewer, count: 0, causes: {} };
62
+ summary.count++;
63
+ summary.causes[cause] = (summary.causes[cause] ?? 0) + 1;
64
+ grouped.set(data.reviewer, summary);
65
+ }
66
+ }
67
+ return [...grouped.values()].sort((a, b) => a.reviewer.localeCompare(b.reviewer));
68
+ }
29
69
  const ORCA_HOOK_ADAPTERS = {
30
70
  claude: "claude-code",
31
71
  "claude-code": "claude-code",
@@ -581,6 +621,15 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
581
621
  const healthy = h.installed && (a.id !== kimi.id || h.authed);
582
622
  return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
583
623
  });
624
+ const reviewDemotions = recentReviewDemotions(cwd);
625
+ if (reviewDemotions.length) {
626
+ rows.push(legend("recent review-seat demotions:"));
627
+ for (const demotion of reviewDemotions) {
628
+ const causes = Object.entries(demotion.causes)
629
+ .map(([cause, count]) => `${cause}${count === demotion.count ? "" : ` ×${count}`}`).join(", ");
630
+ rows.push(alignedStatusRow("warn", demotion.reviewer, `${demotion.count} review seat${demotion.count === 1 ? "" : "s"} demoted · cause ${causes}`));
631
+ }
632
+ }
584
633
  if (existsSync(graphPath(cwd))) {
585
634
  try {
586
635
  const graph = loadGraph(cwd);
@@ -114,7 +114,7 @@ A run is green only when ALL of these hold: the run-end event exists in the jour
114
114
 
115
115
  ### Verified handoffs
116
116
 
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 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.
118
118
 
119
119
  ### Orient before you act — this block may be the ONLY guidance your host loaded
120
120
 
@@ -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,13 @@ 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
+ "end-condition-held": { label: "close held", tone: "attention" },
39
+ "approval-window-start": { label: "approval window", tone: "attention" },
40
+ "approval-window-expired": { label: "approval window expired", tone: "attention" },
41
+ "tip-verify-cancelled": { label: "tip verify cancelled", tone: "attention" },
42
+ };
36
43
  /** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
37
44
  * own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
38
45
  * or `ok` datum on the event overrides the default tone, so one gate row can read either way.
@@ -116,6 +123,9 @@ export const RAIL_ROWS = {
116
123
  // gate starts and verdicts
117
124
  "phase-start": { label: "gate start", tone: "active" },
118
125
  "gate-result": { label: "gate", tone: "pass" },
126
+ "baseline-wait": { label: "baseline wait", tone: "active" },
127
+ "suite-budget": { label: "suite budget", tone: "attention" },
128
+ "gate-replayed": { label: "gate replayed", tone: "attention" },
119
129
  "gate-reused": { label: "gate reused", tone: "neutral" },
120
130
  "judge-retry": { label: "judge retry", tone: "attention" },
121
131
  "review-no-verdict": { label: "review unavailable", tone: "attention" },
@@ -307,6 +317,11 @@ const RAIL_PROJECTION = {
307
317
  ? `gated ${d.gatedCommit.slice(0, 12)}, tip ${d.branchTip.slice(0, 12)}`
308
318
  : undefined,
309
319
  "trust-auto-answer": (d) => typeof d.adapter === "string" ? `${d.adapter}${typeof d.phase === "string" ? ` ${d.phase}` : ""}` : undefined,
320
+ // SB-1: the census the ceiling released beside, and the budget the round ran under versus the one it
321
+ // would have had on an empty census — none of `{count, occupancyCap, conservativeCap}` is on the ladder
322
+ "suite-budget": (d) => typeof d.count === "number" && typeof d.conservativeCap === "number" && typeof d.occupancyCap === "number"
323
+ ? `beside ${d.count}, cap ${d.conservativeCap} not ${d.occupancyCap}`
324
+ : undefined,
310
325
  };
311
326
  // The row's text: the formatter's detail first, then the salient fields that detail could not carry.
312
327
  // Appending (never prefixing) keeps the operator's prose at the front of the row, so the clip a
@@ -333,7 +348,7 @@ const clipCells = (text, cells) => cells <= 0 ? "" : cellWidth(text) <= cells ?
333
348
  export function narrationRow(event, runId, columns = process.stdout.columns ?? 80) {
334
349
  if (TTY_NOISE_EVENTS.includes(event.event))
335
350
  return null;
336
- const row = RAIL_ROWS[event.event];
351
+ const row = APPROVAL_RAIL_ROWS[event.event] ?? RAIL_ROWS[event.event];
337
352
  if (!row)
338
353
  return null;
339
354
  if (event.event === "phase-start" && typeof event.data.gate !== "string")
@@ -825,23 +825,17 @@ const liveness = (events, daemon, now = Date.now()) => {
825
825
  const state = daemon.state === "alive" ? "alive" : ended ? "finished" : "dead";
826
826
  return `last event ${age} ago · daemon pid ${daemon.pid} ${state}${cause ? ` · ${cause}` : ""}`;
827
827
  };
828
- // Resume's shared comparator remains the fail-closed baseline. Status may additionally accept the
829
- // daemon's audited graph-rehash release, but only when its `from` names that baseline identity (or
830
- // null for an explicitly released legacy journal) and its `to` names the graph loaded now.
828
+ // OBS-978: the one comparator resume, plan and the operator fold read decides the join — it already
829
+ // audits the rehash chain. When it binds and the journal holds a graph-rehash, the newest row is the
830
+ // audit that bound the loaded graph; rehashAt marks the facts recorded against a prior graph.
831
831
  const statusEngagement = (events, loadedHash) => {
832
- const baseline = engagementComparable(events, loadedHash);
832
+ if (!engagementComparable(events, loadedHash).comparable)
833
+ return { comparable: false };
833
834
  for (let i = events.length - 1; i >= 0; i--) {
834
- const event = events[i];
835
- if (event.event !== "graph-rehash")
836
- continue;
837
- const auditsBaseline = baseline.comparable || baseline.reason === "mismatch"
838
- ? event.data.from === baseline.recorded
839
- : event.data.from === null;
840
- return event.data.to === loadedHash && auditsBaseline
841
- ? { comparable: true, rehashAt: i }
842
- : { comparable: false };
835
+ if (events[i].event === "graph-rehash")
836
+ return { comparable: true, rehashAt: i };
843
837
  }
844
- return { comparable: baseline.comparable };
838
+ return { comparable: true };
845
839
  };
846
840
  // The journal's own reader rule (src/run/journal.ts readJsonl), applied to bytes already in hand:
847
841
  // skip blanks, drop a line that will not parse (a torn trailing write after a crash), keep the rest.
@@ -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.
@@ -13,6 +13,12 @@ export type OwnershipFinding = {
13
13
  taskIds: string[];
14
14
  corroboration?: OwnershipCorroboration;
15
15
  detail: string;
16
+ } | {
17
+ code: "unowned-shape-oracle";
18
+ taskId: string;
19
+ source: string;
20
+ oracle: string;
21
+ detail: string;
16
22
  } | {
17
23
  code: "test-path-outside-allowlist";
18
24
  taskId: string;
@@ -26,6 +32,9 @@ export type OwnershipFinding = {
26
32
  path: string;
27
33
  detail: string;
28
34
  };
35
+ export declare const SHAPE_ORACLES: readonly ["tests/run/narration.test.ts", "tests/cli/brand-surfaces.test.ts", "tests/run/notify-identity.test.ts", "tests/run/outcome-projections.test.ts", "tests/cockpit/setup.test.ts"];
36
+ export declare const SHAPE_ORACLE_SOURCES: readonly ["src/run/daemon.ts", "src/cli/commands/run.ts"];
37
+ export declare const SHAPE_ORACLE_MAP: Record<string, readonly string[]>;
29
38
  /**
30
39
  * Cross-task ownership evidence. Findings are data: this checker never throws or changes the graph;
31
40
  * the compile seam promotes only a corroborated unowned-test finding and reports every other shape.
@@ -2,6 +2,38 @@ import { readFileSync, readdirSync } from "node:fs";
2
2
  import { basename, extname, join, posix } from "node:path";
3
3
  import { filesGlob } from "../graph/files-glob.js";
4
4
  import { collateralHits } from "./collateral.js";
5
+ export const SHAPE_ORACLES = [
6
+ "tests/run/narration.test.ts",
7
+ "tests/cli/brand-surfaces.test.ts",
8
+ "tests/run/notify-identity.test.ts",
9
+ "tests/run/outcome-projections.test.ts",
10
+ "tests/cockpit/setup.test.ts",
11
+ ];
12
+ export const SHAPE_ORACLE_SOURCES = [
13
+ "src/run/daemon.ts",
14
+ "src/cli/commands/run.ts",
15
+ ];
16
+ export const SHAPE_ORACLE_MAP = {
17
+ "src/run/daemon.ts": SHAPE_ORACLES,
18
+ "src/cli/commands/run.ts": SHAPE_ORACLES,
19
+ };
20
+ // Anchored-glob only: a files[] entry touches a mapped source when it names the source (or its
21
+ // extensionless stem) exactly, or when its glob's literal head — everything before the first
22
+ // wildcard — is the stem plus a literal dot, i.e. the wildcard only ever spans the extension
23
+ // ("src/run/daemon.*"). A broad multi-file glob like "src/**" that merely happens to cover the
24
+ // source is an unrelated task casting a wide net, not one touching daemon narration — that
25
+ // distinction is what broke every fixture using tests/fixtures/sample.prd.md's files: src/** task.
26
+ function touchesSource(files, source) {
27
+ const stem = source.replace(/\.(?:[cm]?[jt]sx?)$/, "");
28
+ return files.some((entry) => {
29
+ if (entry === source || entry === stem)
30
+ return true;
31
+ const special = entry.search(/[*?{[]/);
32
+ // Leg-2 v2.5.3: the anchored head is necessary, not sufficient — the pattern must also MATCH the
33
+ // source under the shared matcher, or "src/run/daemon.{js,jsx}" (never daemon.ts) would count as a touch.
34
+ return special !== -1 && entry.slice(0, special) === `${stem}.` && filesGlob([entry])(source);
35
+ });
36
+ }
5
37
  const normalize = (path) => path.replace(/^\.\//, "").split("\\").join("/");
6
38
  function testSources(repoRoot) {
7
39
  const root = join(repoRoot, "tests");
@@ -155,6 +187,7 @@ export function ownershipFindings(tasks, repoRoot) {
155
187
  const context = task.context.map(normalize);
156
188
  return {
157
189
  task,
190
+ files,
158
191
  owns: files.length === 0 ? () => false : filesGlob(files),
159
192
  allows: files.length === 0 ? () => true : filesGlob([...files, ...context]),
160
193
  };
@@ -204,6 +237,23 @@ export function ownershipFindings(tasks, repoRoot) {
204
237
  });
205
238
  }
206
239
  }
240
+ for (const entry of indexed) {
241
+ for (const [source, oracles] of Object.entries(SHAPE_ORACLE_MAP)) {
242
+ if (!touchesSource(entry.files, source))
243
+ continue;
244
+ for (const oracle of oracles) {
245
+ if (!entry.owns(oracle)) {
246
+ findings.push({
247
+ code: "unowned-shape-oracle",
248
+ taskId: entry.task.id,
249
+ source,
250
+ oracle,
251
+ detail: `${entry.task.id} touches ${source} without owning shape oracle ${oracle}`,
252
+ });
253
+ }
254
+ }
255
+ }
256
+ }
207
257
  for (const source of sources) {
208
258
  for (const owner of owners(source.path)) {
209
259
  for (const path of repositoryPaths(source.text)) {
@@ -234,11 +284,24 @@ export function ownershipFindings(tasks, repoRoot) {
234
284
  }
235
285
  }
236
286
  }
237
- return findings.sort((a, b) => `${a.code}:${"test" in a ? a.test : a.path}:${"taskId" in a ? a.taskId : ""}`.localeCompare(`${b.code}:${"test" in b ? b.test : b.path}:${"taskId" in b ? b.taskId : ""}`));
287
+ const target = (f) => {
288
+ switch (f.code) {
289
+ case "unowned-test": return f.test;
290
+ case "unowned-shape-oracle": return f.oracle;
291
+ case "test-path-outside-allowlist": return f.test;
292
+ case "unordered-context-write": return f.path;
293
+ }
294
+ };
295
+ return findings.sort((a, b) => {
296
+ const taskA = "taskId" in a ? a.taskId : "";
297
+ const taskB = "taskId" in b ? b.taskId : "";
298
+ return `${a.code}:${target(a)}:${taskA}`.localeCompare(`${b.code}:${target(b)}:${taskB}`);
299
+ });
238
300
  }
239
301
  export function renderOwnershipFinding(finding) {
240
302
  return `tickmarkr: ownership-lint[${finding.code}]: ${finding.detail}`;
241
303
  }
242
304
  export function blocksCompile(finding) {
243
- return finding.code === "unowned-test" && finding.corroboration !== undefined;
305
+ return (finding.code === "unowned-test" && finding.corroboration !== undefined)
306
+ || finding.code === "unowned-shape-oracle";
244
307
  }
@@ -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,8 @@ 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;
164
+ selected?: readonly string[];
161
165
  }): Promise<GateResult[]>;
162
166
  export interface HostStarvedRerun {
163
167
  durationMs: number;
@@ -178,4 +182,13 @@ interface CalmWindow {
178
182
  }
179
183
  export declare function setCalmWindowForTests(over: Partial<CalmWindow>): void;
180
184
  export declare function resetCalmWindowForTests(): void;
185
+ export declare function waitForCalmWindow(): Promise<number>;
186
+ /** Summary totals survive digit-normalized fingerprints and exclude test-owned echoed output. */
187
+ export declare function runnerFileCount(raw: string): number | null;
188
+ export declare function fileCountDeficit(entry: BaselineCommand | undefined, raw: string, opts?: {
189
+ name?: string;
190
+ selected?: readonly string[];
191
+ }): string | undefined;
192
+ /** Both readers classify fresh evidence first, retaining the whole-output guard against infra forgiveness. */
193
+ export declare function classifyFreshRunnerOutput(entry: BaselineCommand | undefined, raw: string, code: number): FailureClassification | undefined;
181
194
  export {};