tickmarkr 2.6.1 → 2.6.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 (105) hide show
  1. package/README.md +16 -3
  2. package/dist/adapters/catalog-remote.js +89 -47
  3. package/dist/adapters/claude-code.js +9 -6
  4. package/dist/adapters/codex.js +7 -4
  5. package/dist/adapters/prompt.d.ts +1 -0
  6. package/dist/adapters/prompt.js +14 -6
  7. package/dist/adapters/registry.js +3 -3
  8. package/dist/adapters/types.d.ts +12 -4
  9. package/dist/adapters/types.js +6 -0
  10. package/dist/cli/commands/approve.d.ts +11 -4
  11. package/dist/cli/commands/approve.js +82 -27
  12. package/dist/cli/commands/compile.js +13 -3
  13. package/dist/cli/commands/doctor.d.ts +8 -2
  14. package/dist/cli/commands/doctor.js +11 -3
  15. package/dist/cli/commands/fleet.js +87 -11
  16. package/dist/cli/commands/plan.js +13 -8
  17. package/dist/cli/commands/report.d.ts +2 -1
  18. package/dist/cli/commands/report.js +74 -8
  19. package/dist/cli/commands/resume.js +4 -2
  20. package/dist/cli/commands/status.js +43 -20
  21. package/dist/cli/help.d.ts +2 -0
  22. package/dist/cli/help.js +9 -2
  23. package/dist/compile/native.js +7 -0
  24. package/dist/config/config.d.ts +35 -2
  25. package/dist/config/config.js +86 -10
  26. package/dist/config/fleet-overlay.d.ts +13 -2
  27. package/dist/config/fleet-overlay.js +60 -0
  28. package/dist/drivers/herdr.d.ts +12 -0
  29. package/dist/drivers/herdr.js +51 -0
  30. package/dist/drivers/orca.d.ts +35 -2
  31. package/dist/drivers/orca.js +222 -67
  32. package/dist/drivers/types.d.ts +2 -0
  33. package/dist/drivers/types.js +2 -2
  34. package/dist/eval/canary.d.ts +2 -1
  35. package/dist/eval/canary.js +2 -2
  36. package/dist/eval/dispatch.js +1 -0
  37. package/dist/gates/acceptance.d.ts +9 -1
  38. package/dist/gates/acceptance.js +31 -4
  39. package/dist/gates/baseline.d.ts +32 -2
  40. package/dist/gates/baseline.js +111 -24
  41. package/dist/gates/cache.d.ts +8 -0
  42. package/dist/gates/cache.js +12 -2
  43. package/dist/gates/llm.d.ts +11 -4
  44. package/dist/gates/llm.js +40 -21
  45. package/dist/gates/review.d.ts +14 -1
  46. package/dist/gates/review.js +160 -34
  47. package/dist/gates/run-gates.d.ts +56 -4
  48. package/dist/gates/run-gates.js +358 -58
  49. package/dist/gates/test-manifest.d.ts +45 -1
  50. package/dist/gates/test-manifest.js +78 -12
  51. package/dist/graph/schema.d.ts +2 -0
  52. package/dist/graph/schema.js +2 -0
  53. package/dist/plan/scope.js +2 -2
  54. package/dist/route/preference.d.ts +20 -2
  55. package/dist/route/preference.js +48 -13
  56. package/dist/route/router.d.ts +12 -1
  57. package/dist/route/router.js +56 -24
  58. package/dist/run/consult.d.ts +15 -1
  59. package/dist/run/consult.js +18 -7
  60. package/dist/run/daemon.d.ts +38 -2
  61. package/dist/run/daemon.js +895 -192
  62. package/dist/run/git.d.ts +8 -0
  63. package/dist/run/git.js +14 -0
  64. package/dist/run/interactive-seed.d.ts +4 -0
  65. package/dist/run/interactive-seed.js +35 -9
  66. package/dist/run/journal.d.ts +152 -3
  67. package/dist/run/journal.js +551 -50
  68. package/dist/run/lease.d.ts +13 -0
  69. package/dist/run/lease.js +45 -0
  70. package/dist/run/merge.d.ts +3 -1
  71. package/dist/run/merge.js +3 -2
  72. package/dist/run/operator-summary.d.ts +3 -0
  73. package/dist/run/operator-summary.js +3 -1
  74. package/dist/run/protocol.d.ts +46 -1
  75. package/dist/run/protocol.js +14 -2
  76. package/dist/run/receipt-resolver.d.ts +22 -0
  77. package/dist/run/receipt-resolver.js +40 -1
  78. package/dist/run/repair-selection.d.ts +11 -1
  79. package/dist/run/repair-selection.js +17 -9
  80. package/dist/run/supervision.d.ts +7 -1
  81. package/dist/run/supervision.js +5 -2
  82. package/dist/run/wall-budget.d.ts +48 -0
  83. package/dist/run/wall-budget.js +280 -0
  84. package/dist/tui/cockpit/board.js +3 -3
  85. package/dist/tui/cockpit/decision-actions.d.ts +8 -5
  86. package/dist/tui/cockpit/decision-actions.js +55 -32
  87. package/dist/tui/cockpit/derive.js +13 -2
  88. package/dist/tui/cockpit/live-runtime.d.ts +10 -0
  89. package/dist/tui/cockpit/live-runtime.js +50 -3
  90. package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
  91. package/dist/tui/cockpit/run-cockpit.js +27 -2
  92. package/dist/tui/cockpit/run-view.d.ts +9 -2
  93. package/dist/tui/cockpit/run-view.js +66 -9
  94. package/dist/tui/cockpit/setup-cockpit.d.ts +6 -0
  95. package/dist/tui/cockpit/setup-cockpit.js +10 -3
  96. package/dist/tui/ink/fleet-app.d.ts +15 -3
  97. package/dist/tui/ink/fleet-app.js +91 -22
  98. package/package.json +3 -1
  99. package/schema/config.schema.json +825 -0
  100. package/skills/tickmarkr-loop/SKILL.md +15 -3
  101. package/skills/tickmarkr-overseer/SKILL.md +42 -0
  102. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +91 -0
  103. package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
  104. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
  105. package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
@@ -11,9 +11,9 @@ import { batteryPriority, chainDepth, dispatchWaves, graphDefinitionHash, loadGr
11
11
  import { filesGlob } from "../../graph/files-glob.js";
12
12
  import { renderAcceptanceItem } from "../../graph/schema.js";
13
13
  import { pendingDaemonApprovalActions, resolveRunMode } from "../../run/daemon.js";
14
- import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
14
+ import { disallowedBy, excludedChannels, exclusionLine, observedSeat, routingEntrySeatLines } from "../../route/preference.js";
15
15
  import { decayWeight, HALF_LIFE_RUNS, staffLedEvidence } from "../../route/profile.js";
16
- import { route, RoutingError } from "../../route/router.js";
16
+ import { resolvedFloor, route, RoutingError } from "../../route/router.js";
17
17
  import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
18
18
  import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
19
19
  import { Journal, loadRoutingProfile, readProfileCursor, recordedGraphDefinitionHash, RUNS_WINDOW } from "../../run/journal.js";
@@ -237,16 +237,21 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
237
237
  ...(runLine ? [runLine] : []),
238
238
  "",
239
239
  ];
240
- const judgeDeny = disallowedBy(cfg.judge, cfg.routing, "judge");
240
+ // OBS-1186: the judge seat is judged under doctor's cached identity for that exact channel — the
241
+ // runtime judge's rule; unknown stays conservative (alias-family deny).
242
+ const judgeDeny = disallowedBy(observedSeat(health, cfg.judge.adapter, cfg.judge.model), cfg.routing, "judge");
241
243
  if (judgeDeny?.by === "deny") {
242
244
  lines.push(`REFUSAL OBS-576: judge seat ${cfg.judge.adapter}:${cfg.judge.model} is removed by flat routing.deny (${judgeDeny.entry}) — move worker-only policy to routing.deny.workers`, "");
243
245
  }
244
246
  const entrySeats = routingEntrySeatLines(cfg);
245
247
  if (entrySeats.length)
246
248
  lines.push(...entrySeats, "");
247
- const derivation = (shape) => {
248
- const floor = cfg.routing.floors[shape];
249
- return floor ? ` floor ${floor} ← ${mode.provenance[shape] ?? "config floors"}` : null;
249
+ // OBS-1185: the floor route() resolved for THIS task — a task hint over the configured/mode floor.
250
+ const derivation = (t) => {
251
+ const floor = resolvedFloor(t, cfg);
252
+ if (!floor)
253
+ return null;
254
+ return ` floor ${floor.tier} ← ${floor.source === "task" ? "task hint" : mode.provenance[t.shape] ?? "config floors"}`;
250
255
  };
251
256
  const excluded = excludedChannels(cfg, adapters, health);
252
257
  if (excluded.length)
@@ -408,7 +413,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
408
413
  const review = reviewSeat(t, r.assignment);
409
414
  cost += est + judge.cost + review.cost;
410
415
  lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
411
- const d = derivation(t.shape);
416
+ const d = derivation(t);
412
417
  if (d)
413
418
  lines.push(d);
414
419
  if (r.deviation) {
@@ -432,7 +437,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
432
437
  throw e;
433
438
  const msg = `${e.message}${exclusionReason(e.message)}`;
434
439
  lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! ${msg}`, chainLine(t));
435
- const d = derivation(t.shape);
440
+ const d = derivation(t);
436
441
  if (d)
437
442
  lines.push(d);
438
443
  lints.push(`${t.id}: unroutable — ${msg}`);
@@ -1,4 +1,5 @@
1
+ import type { BaselineCommand } from "../../gates/baseline.js";
1
2
  import { type ChannelCost } from "../../report/cost.js";
2
3
  import { type JournalEvent, type TelemetryRow } from "../../run/journal.js";
3
- export declare function renderMarkdownRecord(runId: string, events: JournalEvent[], prices?: ChannelCost[], rows?: TelemetryRow[]): string;
4
+ export declare function renderMarkdownRecord(runId: string, events: JournalEvent[], prices?: ChannelCost[], rows?: TelemetryRow[], suite?: BaselineCommand): string;
4
5
  export declare function report(argv: string[], cwd?: string): Promise<string>;
@@ -1,4 +1,5 @@
1
- import { writeFileSync } from "node:fs";
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
2
3
  import { parseArgs } from "node:util";
3
4
  import { ttyVisual } from "../../adapters/model-lints.js";
4
5
  import { addUsage } from "../../adapters/types.js";
@@ -11,7 +12,8 @@ import { estimateCosts } from "../../report/cost.js";
11
12
  import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChannelTokens, formatTokenUsage as fields, totalTokens as total, } from "../../report/operator-record.js";
12
13
  import { cellsOf, cellSummary } from "../../route/profile.js";
13
14
  import { Journal, loadRoutingProfile } from "../../run/journal.js";
14
- import { formatTipProof, runEndTipProof } from "../../run/daemon.js";
15
+ import { baselineProvenanceOf, fingerprintsOf, forgivenFingerprints, forgivenGateRow, formatBaselineProvenance, formatFingerprints, formatForgiven, formatTipProof, runEndTipProof, } from "../../run/daemon.js";
16
+ import { formatSpan, WALL_PRIORITY_TEXT, wallBudget, wallBudgetFacts } from "../../run/wall-budget.js";
15
17
  import { deriveRunCockpitData } from "../../tui/cockpit/derive.js";
16
18
  const n = (x) => x.toLocaleString("en-US"); // explicit locale — CI/darwin flake guard
17
19
  const EM = "—";
@@ -75,6 +77,44 @@ const wallClock = (start, end) => {
75
77
  const minutes = Math.floor(seconds / 60);
76
78
  return minutes ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
77
79
  };
80
+ // OBS-1201: the disjoint wall budget, or why the journal cannot give one — never a zeroed table.
81
+ const wallFacts = (events) => {
82
+ const budget = wallBudget(events);
83
+ if (!budget)
84
+ return [["window", "not measurable — the journal names no run-start with a readable timestamp"]];
85
+ return [["window", `${formatSpan(budget.wallMs)} — each instant counted once, by priority ${WALL_PRIORITY_TEXT}; task-time sums concurrent spans`], ...wallBudgetFacts(budget)];
86
+ };
87
+ // OBS-634: the four numbers the baseline capture already measured for the test command. A capture that
88
+ // measured nothing says so; a missing number is not measurable, never a zero.
89
+ const suiteTelemetry = (entry) => {
90
+ if (!entry)
91
+ return "not recorded — this run's baseline holds no test capture";
92
+ if (entry.infra)
93
+ return `not measurable — the baseline capture returned no verdict (${entry.invalidCause ?? "infra"})`;
94
+ if (typeof entry.durationMs !== "number" || !Number.isFinite(entry.durationMs))
95
+ return "not recorded — the baseline predates capture timing";
96
+ const unmeasured = "not measurable (the runner named no per-file durations)";
97
+ const sum = entry.fileDurationSumMs;
98
+ const parallelism = entry.impliedParallelism;
99
+ const longest = entry.longestFile;
100
+ return [
101
+ `wall ${formatSpan(entry.durationMs)}`,
102
+ `file-sum ${typeof sum === "number" ? formatSpan(sum) : unmeasured}`,
103
+ `implied parallelism ${typeof parallelism === "number" ? parallelism.toFixed(2) : unmeasured}`,
104
+ `longest file ${longest ? `${longest.file} ${formatSpan(longest.durationMs)}` : unmeasured}`,
105
+ ...(typeof entry.fileCount === "number" ? [`${entry.fileCount} files`] : []),
106
+ ].join(" · ");
107
+ };
108
+ /** The run's own baseline.json test entry; absent or unreadable reads as not recorded. */
109
+ const baselineTestEntry = (runDir) => {
110
+ try {
111
+ const baseline = JSON.parse(readFileSync(join(runDir, "baseline.json"), "utf8"));
112
+ return baseline.commands?.test;
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ };
78
118
  const detail = (value) => typeof value === "string" || typeof value === "number" ? String(value) : EM;
79
119
  // T11: a gate that declined never ran. Drop those before the comparison metrics so the pass rate
80
120
  // counts only gates that actually ran — a declined gate is never a pass and never pads the base.
@@ -170,6 +210,10 @@ const VERIFICATION_READING = {
170
210
  failed: "FAILED — the run did not verify its own tip",
171
211
  absent: "absent — no tip verification recorded: neither passed nor failed",
172
212
  };
213
+ function forgivenLines(events) {
214
+ const forgiven = forgivenFingerprints(closedCycle(events));
215
+ return forgiven.length ? ["- **forgiven vs baseline:**", ...forgiven.map((f) => ` - ${formatForgiven(f)}`)] : [];
216
+ }
173
217
  function verificationReading(runId, events) {
174
218
  const state = verificationOf(runId, events);
175
219
  const proof = runEndTipProof(closedCycle(events));
@@ -179,8 +223,18 @@ function verificationReading(runId, events) {
179
223
  }
180
224
  return VERIFICATION_READING[state];
181
225
  }
226
+ // OBS-1123: a battery row that carried baseline reds names them apart from any red it introduced, so a
227
+ // new regression can never read as forgiven. Rows without the structured fields render as before.
228
+ const fingerprintClauses = (data) => {
229
+ const fresh = fingerprintsOf(data.freshFingerprints) ?? [];
230
+ const forgiven = fingerprintsOf(data.forgivenFingerprints);
231
+ if (!forgiven && !forgivenGateRow(data))
232
+ return "";
233
+ return `${fresh.length ? `; new red (not in baseline): ${formatFingerprints(fresh)}` : ""}`
234
+ + `; forgiven vs baseline: ${formatFingerprints(forgiven)} — ${formatBaselineProvenance(baselineProvenanceOf(data.baselineProvenance))}`;
235
+ };
182
236
  // VIS-07 / REC-01: derived only from the run journal, telemetry, and local configuration.
183
- export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
237
+ export function renderMarkdownRecord(runId, events, prices = [], rows = [], suite) {
184
238
  const runStart = events.find((e) => e.event === "run-start");
185
239
  const runEnd = [...events].reverse().find((e) => e.event === "run-end");
186
240
  const baseRef = typeof runStart?.data.baseRef === "string" ? runStart.data.baseRef : EM;
@@ -216,6 +270,8 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
216
270
  `- **failed:** ${count("failed")}`,
217
271
  `- **human:** ${count("human")}`,
218
272
  `- **verification:** ${verificationReading(runId, events)}`,
273
+ // OBS-1123: the same fold the run-end record states, over the same closed cycle as verification.
274
+ ...forgivenLines(events),
219
275
  "",
220
276
  "## Usage & efficiency",
221
277
  "",
@@ -226,6 +282,11 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
226
282
  `- **consults:** ${events.filter((e) => e.event === "consult-verdict").length}`,
227
283
  `- **escalations:** ${events.filter((e) => e.event === "escalation").length}`,
228
284
  "",
285
+ "## Wall budget",
286
+ "",
287
+ ...wallFacts(events).map(([label, fact]) => `- **${label}:** ${fact}`),
288
+ `- **suite telemetry (baseline test capture):** ${suiteTelemetry(suite)}`,
289
+ "",
229
290
  ];
230
291
  lines.push("## Channels", "");
231
292
  const operatorRecords = buildOperatorRecord(events, prices);
@@ -280,7 +341,7 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
280
341
  }
281
342
  const resolved = gate === "review" && g.data.pass === true && Array.isArray(g.data.resolved)
282
343
  ? g.data.resolved.filter((id) => typeof id === "string") : [];
283
- lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}${resolved.length ? `; resolved: ${resolved.join(", ")}` : ""}`);
344
+ lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}${resolved.length ? `; resolved: ${resolved.join(", ")}` : ""}${fingerprintClauses(g.data)}`);
284
345
  }
285
346
  for (const row of leg2) {
286
347
  const pass = row.data.pass === true ? "pass" : row.data.pass === false ? "fail" : EM;
@@ -310,7 +371,7 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
310
371
  }
311
372
  return lines.join("\n").trimEnd() + "\n";
312
373
  }
313
- function textReport(runId, events, rows, cwd) {
374
+ function textReport(runId, events, rows, cwd, suite) {
314
375
  // one group per adapter:model, carrying channel + the folded usage across its rows
315
376
  const groups = new Map();
316
377
  for (const r of rows) {
@@ -372,6 +433,10 @@ function textReport(runId, events, rows, cwd) {
372
433
  `tickmark rate: ${gateRan ? Math.round((100 * gatePass) / gateRan) : 0}% (${gatePass}/${gateRan})${declinedCount ? ` · declined: ${declinedCount}` : ""}`,
373
434
  `escalations: ${escalations} · National Office consults: ${consults} · quota failovers: ${failovers}`,
374
435
  "",
436
+ "wall budget — exposed wall, disjoint:",
437
+ ...wallFacts(events).map(([label, fact]) => ` ${label.padEnd(14)} ${fact}`),
438
+ ` ${"suite".padEnd(14)} ${suiteTelemetry(suite)}`,
439
+ "",
375
440
  "spend — tokens (measured where observed):",
376
441
  ...tokenLines,
377
442
  "spend — money:",
@@ -391,7 +456,7 @@ const stylizeReport = (out) => {
391
456
  return out;
392
457
  return out
393
458
  .replace(/^.*$/m, (first) => `${title(first)}\n${rule()}`) // non-global /m ⇒ first line only
394
- .replace(/^(engagement summary — audit trail:|spend — tokens[^\n]*|spend — money:|learning \([^\n]*)$/gm, (l) => dim(l));
459
+ .replace(/^(engagement summary — audit trail:|wall budget — [^\n]*|spend — tokens[^\n]*|spend — money:|learning \([^\n]*)$/gm, (l) => dim(l));
395
460
  };
396
461
  export async function report(argv, cwd = process.cwd()) {
397
462
  const { values, positionals } = parseArgs({
@@ -412,6 +477,7 @@ export async function report(argv, cwd = process.cwd()) {
412
477
  const events = j.read();
413
478
  const rows = j.readTelemetry();
414
479
  const cfg = loadConfig(cwd);
480
+ const suite = baselineTestEntry(j.dir);
415
481
  let bundleNote = "";
416
482
  if (values.bundle) {
417
483
  // Local-only write of the pure proof packet — no network path exists in buildProofBundle.
@@ -438,7 +504,7 @@ export async function report(argv, cwd = process.cwd()) {
438
504
  comparison = "\n" + outcome.text;
439
505
  }
440
506
  if (values.md) {
441
- return bundleNote + renderMarkdownRecord(runId, events, estimateCosts(rows, cfg.cost), rows) + comparison;
507
+ return bundleNote + renderMarkdownRecord(runId, events, estimateCosts(rows, cfg.cost), rows, suite) + comparison;
442
508
  }
443
- return bundleNote + stylizeReport(textReport(runId, events, rows, cwd)) + comparison;
509
+ return bundleNote + stylizeReport(textReport(runId, events, rows, cwd, suite)) + comparison;
444
510
  }
@@ -1,4 +1,5 @@
1
1
  import { parseArgs } from "node:util";
2
+ import { readDoctor } from "../../adapters/registry.js";
2
3
  import { loadConfig } from "../../config/config.js";
3
4
  import { classifyHost, parseDriverOverride, pickDriver, preflightHostDriver } from "../../drivers/index.js";
4
5
  import { loadGraph } from "../../graph/graph.js";
@@ -31,9 +32,10 @@ export async function resume(argv, cwd = process.cwd()) {
31
32
  // v1.87 T3 (OBS-162, twice-carried workaround): the preflight runs AFTER the graph is read and
32
33
  // sees only the shapes the resumed graph carries. A deny∩prefer collision on a shape no resumed
33
34
  // task uses is a config fact the run would never resolve — it must not refuse the only
34
- // crash-recovery path. doctor still walks the whole map.
35
+ // crash-recovery path. doctor still walks the whole map. OBS-1143: aliases are judged by the
36
+ // identity doctor cached — the same one the daemon's discovery routes with.
35
37
  const graph = loadGraph(cwd);
36
- const collisions = denyPreferCollisions(cfg, graph.tasks.map((t) => t.shape));
38
+ const collisions = denyPreferCollisions(cfg, graph.tasks.map((t) => t.shape), readDoctor(cwd));
37
39
  if (collisions.length) {
38
40
  throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
39
41
  }
@@ -12,11 +12,12 @@ import { authorsNote, doneAuthors } from "../../run/operator-state.js";
12
12
  import { projectOperatorSummary } from "../../run/operator-summary.js";
13
13
  import { trackJournalRows } from "../../run/protocol.js";
14
14
  import { newestPark, permittedDecisionVerbs } from "./approve.js";
15
- import { Journal, formatJournalNarration, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
15
+ import { Journal, bindingToken, effectiveEvents, formatJournalNarration, parseJournalText, physicalLine, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
16
16
  import { isPidLive, runLockRunId, runStatusLine } from "../../run/lock.js";
17
17
  import { normalizeGateOutcome } from "../../run/outcome.js";
18
18
  import { desiredPanes } from "../../run/reconcile.js";
19
19
  import { normalizeStallSnapshot } from "../../run/stall.js";
20
+ import { formatSpan, wallBudget, wallBudgetFacts } from "../../run/wall-budget.js";
20
21
  import { armWatchSupervision, observeNamedRun, WATCH_OWNER_ENV, readSupervision, supervisionText } from "../../run/supervision.js";
21
22
  import { deriveRunCockpitData, } from "../../tui/cockpit/derive.js";
22
23
  import { cellWidth, fitCells, wrapCells } from "../../tui/cockpit/width.js";
@@ -167,7 +168,7 @@ export const decisionEventsFromJournal = (events, runId, stateDir = ".tickmarkr"
167
168
  ...base,
168
169
  type: "human-decision-required",
169
170
  tier: "decision",
170
- approvalCommand: `tickmarkr approve ${runId} ${event.taskId}`,
171
+ approvalCommand: `tickmarkr approve ${runId} ${event.taskId} --park ${bindingToken({ line: physicalLine(events, index), ts: event.ts })}`,
171
172
  ...(typeof event.data.kind === "string" ? { kind: event.data.kind } : {}),
172
173
  ...(typeof event.data.reason === "string" ? { reason: event.data.reason } : {}),
173
174
  }];
@@ -841,18 +842,6 @@ const statusEngagement = (events, loadedHash) => {
841
842
  }
842
843
  return { comparable: true };
843
844
  };
844
- // The journal's own reader rule (src/run/journal.ts readJsonl), applied to bytes already in hand:
845
- // skip blanks, drop a line that will not parse (a torn trailing write after a crash), keep the rest.
846
- const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
847
- if (!line.trim())
848
- return [];
849
- try {
850
- return [JSON.parse(line)];
851
- }
852
- catch {
853
- return [];
854
- }
855
- });
856
845
  // runLockOwner is the single lock payload/liveness reader. Its current path helper also initializes
857
846
  // .tickmarkr metadata; an engine-written lock necessarily passed through that initializer already, so
858
847
  // status avoids invoking it in stripped reader-purity fixtures where no engine lock can exist.
@@ -892,7 +881,7 @@ const readRunRecord = (cwd, graph, namedRunId) => {
892
881
  if (!runId)
893
882
  return undefined;
894
883
  const raw = readRunJournalRaw(cwd, runId, lockedRunId === runId);
895
- const events = parseJournalSnapshot(raw);
884
+ const events = parseJournalText(raw);
896
885
  // The resume comparator is the fail-closed baseline; a matching graph-rehash is the daemon's
897
886
  // append-only audit that authorizes this status replay after stop-amend-resume.
898
887
  const engagement = statusEngagement(events, graphDefinitionHash(graph));
@@ -918,7 +907,8 @@ const recordTaskRows = (record, graph, isDaemonAlive) => record.comparable && re
918
907
  const recordProjection = (record, graph, isDaemonAlive) => {
919
908
  const rows = record ? recordTaskRows(record, graph, isDaemonAlive) : new Map();
920
909
  const events = record?.comparable ? record.events : [];
921
- const activity = projectActivity(record?.runId ?? "", trackJournalRows(record?.runId ?? "", events.map((raw, sourceIndex) => ({ raw, sourceIndex }))), graph.tasks);
910
+ // OBS-1178: a refused or unsound decision released nothing, so the activity fold never reads it.
911
+ const activity = projectActivity(record?.runId ?? "", trackJournalRows(record?.runId ?? "", effectiveEvents(events).map((raw, sourceIndex) => ({ raw, sourceIndex }))), graph.tasks);
922
912
  const tasks = graph.tasks.map(task => {
923
913
  const row = rows.get(task.id);
924
914
  const evidence = events.filter(event => event.taskId === task.id);
@@ -1068,12 +1058,28 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
1068
1058
  // OBS-738: recovery facts stay on the journal's two established reducers. The prefix fold keeps an
1069
1059
  // older journal equally readable by asking upheldFeedbackByTask what was active at that exact
1070
1060
  // restore boundary; current resume-restore rows additionally carry the same task for narration.
1061
+ // OBS-1178: both folds read decisions through the one decision fold over the whole journal.
1062
+ const decided = effectiveEvents(events);
1071
1063
  const preservedRefs = preservedRefsByTask(events);
1072
- const restoredUpheldFeedback = new Set(events.flatMap((event, index) => event.event === "resume-restore" && event.taskId
1073
- && upheldFeedbackByTask(events.slice(0, index)).has(event.taskId)
1064
+ const restoredUpheldFeedback = new Set(decided.flatMap((event, index) => event.event === "resume-restore" && event.taskId
1065
+ && upheldFeedbackByTask(decided.slice(0, index)).has(event.taskId)
1074
1066
  ? [event.taskId]
1075
1067
  : []));
1068
+ // OBS-1178: a failed task's newest task-failed row is the token its recheck binds to. Only an
1069
+ // effective decision consumes it — a refused or unsound recheck leaves the task failed and the token shown.
1070
+ const failureTokens = new Map();
1071
+ decided.forEach((event, index) => {
1072
+ if (!event.taskId || !["task-dispatch", "task-done", "task-failed", "task-human", "task-approved"].includes(event.event))
1073
+ return;
1074
+ if (event.event === "task-failed")
1075
+ failureTokens.set(event.taskId, bindingToken({ line: physicalLine(decided, index), ts: event.ts }));
1076
+ else
1077
+ failureTokens.delete(event.taskId);
1078
+ });
1076
1079
  const recoveryLinesForTask = (taskId) => [
1080
+ ...(failureTokens.has(taskId) && runId
1081
+ ? [`failed — ${taskId} — failure ${failureTokens.get(taskId)} — re-gate landed work with \`tickmarkr approve ${runId} ${taskId} --recheck --park ${failureTokens.get(taskId)}\``]
1082
+ : []),
1077
1083
  ...(preservedRefs.get(taskId) ?? []).flatMap(({ ref, diffCommand }) => [
1078
1084
  `preserved worktree — ${taskId} — ${ref}`,
1079
1085
  ` ${diffCommand}`,
@@ -1150,6 +1156,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
1150
1156
  ...(hotPhase ? { hotPhase } : {}),
1151
1157
  ...(runId ? { runId } : {}),
1152
1158
  workerPhases: [...phases.values()].filter((phase) => phase.phase === "worker"),
1159
+ events,
1153
1160
  };
1154
1161
  }
1155
1162
  // TTY: the operator-approved task table. The daemon gives this surface a ~110-column pane first;
@@ -1359,8 +1366,22 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
1359
1366
  ...(hotPhase ? { hotPhase } : {}),
1360
1367
  ...(runId ? { runId } : {}),
1361
1368
  workerPhases: [...phases.values()].filter((phase) => phase.phase === "worker"),
1369
+ events,
1362
1370
  };
1363
1371
  };
1372
+ // OBS-1201: the one-shot print closes with where the run's wall went (src/run/wall-budget.ts). A
1373
+ // window of zero length measured nothing, so it prints nothing; watch frames are untouched. Rows wrap
1374
+ // to the board width, so narrowing costs rows, never columns.
1375
+ const wallBudgetLines = (events) => {
1376
+ const budget = wallBudget(events);
1377
+ if (!budget || budget.wallMs <= 0)
1378
+ return [];
1379
+ const columns = Math.max(40, process.stdout.columns ?? 120);
1380
+ return [
1381
+ ` wall budget ${formatSpan(budget.wallMs)} · each instant once, by priority`,
1382
+ ...wallBudgetFacts(budget).map(([label, fact]) => ` ${label} ${fact}`),
1383
+ ].flatMap((line) => wrapCells(line, columns, { continuationPrefix: " " }));
1384
+ };
1364
1385
  /**
1365
1386
  * The compact form a statusline calls, so journal interpretation happens ONCE, inside the product.
1366
1387
  * (An operator-local bash reimplementation counted `tip-verify` events instead of reading the
@@ -1396,7 +1417,9 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1396
1417
  // The task-table frame owns its own two-row brand lockup; the four-row global banner would
1397
1418
  // create the second header the approved replacement removes.
1398
1419
  if (!argv.includes("--watch")) {
1399
- return renderFrame(cwd, binaryVersion, opts.now?.() ?? Date.now(), 0, undefined, false, namedRunId).content;
1420
+ const frame = renderFrame(cwd, binaryVersion, opts.now?.() ?? Date.now(), 0, undefined, false, namedRunId);
1421
+ const wall = wallBudgetLines(frame.events);
1422
+ return wall.length ? [frame.content, "", ...wall].join("\n") : frame.content;
1400
1423
  }
1401
1424
  const eventStream = argv.some((arg) => arg === "--events" || arg === "--jsonl" || arg === "--decision-events");
1402
1425
  const webhookUrl = opts.webhookUrl
@@ -1501,7 +1524,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1501
1524
  decisionRunId = runId;
1502
1525
  journalCursor = 0;
1503
1526
  }
1504
- const journalEvents = parseJournalSnapshot(readRunJournalRaw(cwd, runId, lockedRunId === runId));
1527
+ const journalEvents = parseJournalText(readRunJournalRaw(cwd, runId, lockedRunId === runId));
1505
1528
  if (journalEvents.length < journalCursor)
1506
1529
  journalCursor = 0;
1507
1530
  const fresh = decisionEventsFromJournal(journalEvents, runId, stateDirName(cwd))
@@ -163,6 +163,8 @@ export declare const COMMAND_HELP: {
163
163
  usage: string;
164
164
  description: string;
165
165
  options: {
166
+ "--park <line>@<ts>": string;
167
+ "--gate <gate>": string;
166
168
  "--files <glob,\u2026>": string;
167
169
  "--by <name>": string;
168
170
  "--reason <text>": string;
package/dist/cli/help.js CHANGED
@@ -131,14 +131,21 @@ export const COMMAND_HELP = {
131
131
  approve: {
132
132
  usage: "approve <run-id> <task-id> [options]", description: "Append a validated park decision. A running owner may enact it; a closed run requires a separate resume. Decisions cannot be undone.",
133
133
  options: {
134
+ "--park <line>@<ts>": "Bind the decision to the park token status prints (a failed task's recheck binds its failure token); refused once a newer park opens.",
135
+ "--gate <gate>": "Bind a waive to the park's failed gate; refused when the park failed another gate.",
134
136
  "--files <glob,…>": "Extend files[] for a scope-request park with comma-separated repository-relative globs; required for scope approval.",
135
137
  "--by <name>": "Name the actor (default: current OS user).",
136
138
  "--reason <text>": "Record the decision reason.",
137
139
  "--waive": "Waive only the identified failed gate.",
138
140
  "--uphold": "Uphold a review failure and fund a fixed attempt.",
139
141
  "--recheck": "Request rechecking an infra or failed-gate park; satisfies no gate.",
140
- "--review-rounds <N>": "Set a positive integer review-round ceiling with the decision.",
141
- }, examples: ["approve run-example T1 --by operator --reason 'ready to proceed'", "approve run-example T1 --recheck --reason 'infra recovered'"],
142
+ "--review-rounds <N>": "Set a positive integer review-round ceiling that binds only this approval engagement; a later approval without --review-rounds restores the built-in default.",
143
+ }, examples: [
144
+ "approve run-example T1 --park 42@2026-09-26T08:00:00.000Z --by operator --reason 'ready to proceed'",
145
+ "approve run-example T1 --waive --park 42@2026-09-26T08:00:00.000Z --gate review",
146
+ "approve run-example T1 --recheck --park 42@2026-09-26T08:00:00.000Z --reason 'infra recovered'",
147
+ "approve run-example T3 --recheck --park 57@2026-09-26T08:05:00.000Z --reason 'failed task: re-gate its landed commits'",
148
+ ],
142
149
  },
143
150
  beat: {
144
151
  usage: "beat <orchestrator|orchestrator-context|overseer|overseer-context|watch> --seat <identity> [options]",
@@ -911,6 +911,13 @@ acceptance is required on every task (a nested list of observable outcomes).
911
911
  - Enumerating one axis exhaustively is what hides the others. A spec that guards PARTIAL coverage
912
912
  site-by-site, member-by-member, can be defeated wholesale by CONDITIONAL coverage, which leaves
913
913
  every enumeration satisfied. After you enumerate, ask what a single flag would do to the whole set.
914
+ - A GOAL STATING AN INVARIANT OVER "every", "never" OR "any" CARRIES ITS CLOSED CASE TABLE AS A
915
+ CRITERION: the in-scope consumers, bridges, operations and event sequences it ranges over, each a
916
+ row a test exercises. An unenumerated invariant is repaired one edge per review round while every
917
+ round re-judges the accumulated diff (OBS-1019 add.2). The table binds author and reviewer, never a
918
+ worker's claim: each review material binds its declared class to the goal clause or criterion it
919
+ violates through input-to-consequence evidence marked executed, static or blocked. A worker's own
920
+ table alone never silences a real finding, and blocked evidence is never a pass.
914
921
  - EVERY CRITERION NAMES THE PAIR IT DISCRIMINATES: the correct case that MUST PASS, and the
915
922
  neighbouring plausible-wrong or false-clean case that MUST FAIL. Two easy examples that both pass are
916
923
  not discrimination — they are two ways of being green, and the wrong half is the whole point: it is
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { type Effort } from "../graph/schema.js";
2
3
  declare const TierEnum: z.ZodEnum<{
3
4
  cheap: "cheap";
4
5
  mid: "mid";
@@ -41,6 +42,7 @@ export declare const MapEntrySchema: z.ZodObject<{
41
42
  escalate: z.ZodOptional<z.ZodBoolean>;
42
43
  }, z.core.$strip>;
43
44
  export type MapEntry = z.infer<typeof MapEntrySchema>;
45
+ export declare const EFFORT_ADAPTERS: readonly ["claude-code", "codex"];
44
46
  export declare const TierEntrySchema: z.ZodObject<{
45
47
  vendor: z.ZodNullable<z.ZodString>;
46
48
  channel: z.ZodEnum<{
@@ -58,6 +60,11 @@ export declare const TierEntrySchema: z.ZodObject<{
58
60
  sub: "sub";
59
61
  api: "api";
60
62
  }>>;
63
+ effort: z.ZodOptional<z.ZodEnum<{
64
+ low: "low";
65
+ medium: "medium";
66
+ high: "high";
67
+ }>>;
61
68
  }, z.core.$strip>>>;
62
69
  windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
63
70
  }, z.core.$strip>;
@@ -174,13 +181,13 @@ export declare function effectiveReviewPolicy(files: ReadonlyArray<string>, revi
174
181
  export declare function criticalPathHits(files: ReadonlyArray<string>, criticalPaths?: ReadonlyArray<string>): string[];
175
182
  export declare const ExecutionPolicySchema: z.ZodObject<{
176
183
  boundedInfrastructure: z.ZodDefault<z.ZodBoolean>;
177
- repairSelection: z.ZodDefault<z.ZodBoolean>;
184
+ repairSelection: z.ZodOptional<z.ZodBoolean>;
178
185
  taskExecutionLimitMs: z.ZodNumber;
179
186
  }, z.core.$strict>;
180
187
  export declare const TickmarkrConfigSchema: z.ZodObject<{
181
188
  executionPolicy: z.ZodOptional<z.ZodObject<{
182
189
  boundedInfrastructure: z.ZodDefault<z.ZodBoolean>;
183
- repairSelection: z.ZodDefault<z.ZodBoolean>;
190
+ repairSelection: z.ZodOptional<z.ZodBoolean>;
184
191
  taskExecutionLimitMs: z.ZodNumber;
185
192
  }, z.core.$strict>>;
186
193
  concurrency: z.ZodNumber;
@@ -274,6 +281,11 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
274
281
  sub: "sub";
275
282
  api: "api";
276
283
  }>>;
284
+ effort: z.ZodOptional<z.ZodEnum<{
285
+ low: "low";
286
+ medium: "medium";
287
+ high: "high";
288
+ }>>;
277
289
  }, z.core.$strip>>>;
278
290
  windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
279
291
  }, z.core.$strip>>;
@@ -297,6 +309,8 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
297
309
  tipTest: z.ZodOptional<z.ZodString>;
298
310
  lint: z.ZodOptional<z.ZodString>;
299
311
  diffCap: z.ZodOptional<z.ZodNumber>;
312
+ evidenceQuotaBytes: z.ZodOptional<z.ZodNumber>;
313
+ repairSelection: z.ZodOptional<z.ZodBoolean>;
300
314
  byShape: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodEnum<{
301
315
  plan: "plan";
302
316
  spec: "spec";
@@ -362,6 +376,7 @@ export type TickmarkrConfig = z.infer<typeof TickmarkrConfigSchema>;
362
376
  export declare class ConfigError extends Error {
363
377
  constructor(message: string);
364
378
  }
379
+ export declare const DEFAULT_EVIDENCE_QUOTA_BYTES: number;
365
380
  export declare const DEFAULT_CONFIG: TickmarkrConfig;
366
381
  export declare function globalConfigDir(): string;
367
382
  export declare function overlayPreferShapes(repoRoot: string, opts?: {
@@ -383,6 +398,22 @@ export declare function loadConfigWithMode(repoRoot: string, opts?: {
383
398
  cfg: TickmarkrConfig;
384
399
  mode: ModeResolution;
385
400
  };
401
+ /** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the repo overlay resolve them. */
402
+ export type LowerLayerModelOverrides = Record<string, Record<string, Record<string, unknown>>>;
403
+ /** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the repo overlay (defaults + global)
404
+ * merge them. Read raw, never schema-validated: a lower layer may only validate beside repo fields
405
+ * (global `vendor: null` completed by a repo vendor) and its override metadata is still inherited.
406
+ * OBS-1188: an unparseable global file, or a non-map where this path expects a map, is an error —
407
+ * never an empty lower layer, which would silently drop the metadata a write must re-mask. */
408
+ export declare function lowerLayerModelOverrides(opts?: {
409
+ globalDir?: string;
410
+ }): {
411
+ ok: true;
412
+ overrides: LowerLayerModelOverrides;
413
+ } | {
414
+ ok: false;
415
+ error: string;
416
+ };
386
417
  export declare function loadConfig(repoRoot: string, opts?: {
387
418
  globalDir?: string;
388
419
  }): TickmarkrConfig;
@@ -416,6 +447,8 @@ export type FleetEditable = Partial<Record<DenyScopeKey, string[]>> & {
416
447
  denyModels: string[];
417
448
  allowOut?: string[];
418
449
  tiers: Record<string, Record<string, FleetTierAssignment | null>>;
450
+ /** OBS-1182: tiers.<adapter>.modelOverrides.<model>.effort — absent = CLI default; present only when one is set */
451
+ efforts?: Record<string, Record<string, Effort>>;
419
452
  map: Record<string, MapEntry>;
420
453
  floors: Record<string, Tier>;
421
454
  };