tickmarkr 2.6.2 → 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 (63) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/commands/approve.d.ts +6 -3
  3. package/dist/cli/commands/approve.js +16 -4
  4. package/dist/cli/commands/doctor.d.ts +6 -2
  5. package/dist/cli/commands/fleet.js +46 -8
  6. package/dist/cli/commands/plan.js +13 -8
  7. package/dist/cli/commands/report.d.ts +2 -1
  8. package/dist/cli/commands/report.js +56 -6
  9. package/dist/cli/commands/status.js +19 -1
  10. package/dist/compile/native.js +7 -0
  11. package/dist/config/config.d.ts +17 -4
  12. package/dist/config/config.js +43 -6
  13. package/dist/config/fleet-overlay.d.ts +13 -3
  14. package/dist/config/fleet-overlay.js +12 -8
  15. package/dist/drivers/herdr.d.ts +12 -0
  16. package/dist/drivers/herdr.js +51 -0
  17. package/dist/drivers/orca.d.ts +9 -1
  18. package/dist/drivers/orca.js +29 -7
  19. package/dist/drivers/types.d.ts +2 -0
  20. package/dist/drivers/types.js +2 -2
  21. package/dist/gates/acceptance.d.ts +7 -0
  22. package/dist/gates/acceptance.js +27 -5
  23. package/dist/gates/baseline.d.ts +20 -1
  24. package/dist/gates/baseline.js +100 -20
  25. package/dist/gates/cache.d.ts +8 -0
  26. package/dist/gates/cache.js +12 -2
  27. package/dist/gates/llm.d.ts +6 -0
  28. package/dist/gates/llm.js +27 -8
  29. package/dist/gates/review.d.ts +6 -1
  30. package/dist/gates/review.js +122 -32
  31. package/dist/gates/run-gates.d.ts +54 -3
  32. package/dist/gates/run-gates.js +331 -45
  33. package/dist/gates/test-manifest.d.ts +42 -0
  34. package/dist/gates/test-manifest.js +69 -10
  35. package/dist/route/router.d.ts +12 -1
  36. package/dist/route/router.js +26 -9
  37. package/dist/run/consult.d.ts +3 -1
  38. package/dist/run/consult.js +4 -2
  39. package/dist/run/daemon.d.ts +2 -1
  40. package/dist/run/daemon.js +342 -77
  41. package/dist/run/interactive-seed.d.ts +4 -0
  42. package/dist/run/interactive-seed.js +35 -9
  43. package/dist/run/journal.d.ts +26 -0
  44. package/dist/run/journal.js +143 -15
  45. package/dist/run/lease.d.ts +13 -0
  46. package/dist/run/lease.js +45 -0
  47. package/dist/run/protocol.d.ts +15 -0
  48. package/dist/run/protocol.js +11 -1
  49. package/dist/run/receipt-resolver.d.ts +22 -0
  50. package/dist/run/receipt-resolver.js +40 -1
  51. package/dist/run/repair-selection.d.ts +11 -1
  52. package/dist/run/repair-selection.js +17 -9
  53. package/dist/run/wall-budget.d.ts +48 -0
  54. package/dist/run/wall-budget.js +280 -0
  55. package/dist/tui/cockpit/run-cockpit.js +2 -2
  56. package/dist/tui/cockpit/run-view.d.ts +2 -1
  57. package/dist/tui/cockpit/run-view.js +13 -9
  58. package/dist/tui/cockpit/setup-cockpit.d.ts +2 -0
  59. package/dist/tui/cockpit/setup-cockpit.js +4 -0
  60. package/package.json +2 -1
  61. package/schema/config.schema.json +8 -1
  62. package/skills/tickmarkr-loop/SKILL.md +7 -1
  63. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +4 -1
package/README.md CHANGED
@@ -208,6 +208,8 @@ worker. Other parks have different permitted decisions:
208
208
  | Human gate / other non-gate park | Plain approve records permission to dispatch. |
209
209
  | Attempt cap | Plain approve grants a fresh attempt budget; prior routing exclusions remain. |
210
210
  | Infrastructure | Plain approve or `--recheck`; recheck reruns the declared battery and satisfies no gate. |
211
+ | Stall with a recorded `reapFailure` (the worker's process census was unreadable or had survivors) | `--recheck` bound to that park's token re-verifies the parked attempt's owned census: only an explicitly recorded empty survivors array (`[]`) releases its harvested commits to the declared battery, with no worker; a missing, unreadable or surviving census re-parks the stall (new token, still approve/recheck). Plain approve dispatches a worker. |
212
+ | Ordinary stall (no `reapFailure`) | Plain approve only — it dispatches a worker; `--recheck` refuses. |
211
213
  | Failed review gate | `--waive` satisfies only that identified gate; `--uphold` funds one fixed attempt carrying findings; `--recheck` reruns the battery. |
212
214
  | Other failed gate | `--waive` or `--recheck`; plain approve refuses. |
213
215
  | Tombstone / gate failure without identifying evidence | Diagnostic only; no invented decision. |
@@ -26,6 +26,8 @@ export interface NewestPark {
26
26
  failedGate: string | undefined;
27
27
  /** A pre-dispatch human gate whose reason marks it permanent by design (see isTombstonePark). */
28
28
  tombstone: boolean;
29
+ /** OBS-1202: a stall park's recorded reap failure (task-human data.reapFailure) — its census is unproven. */
30
+ reapFailure?: string;
29
31
  }
30
32
  /**
31
33
  * There is no closed park kind for a declaration-shaped retirement, so the evidence is the one the
@@ -46,13 +48,14 @@ export declare const parkToken: (park: Pick<NewestPark, "line" | "ts">) => strin
46
48
  /** OBS-1178: the newest task-failed row for a task — the failure a failed-task recheck binds to. */
47
49
  export declare function newestFailure(events: readonly JournalEvent[], taskId: string, sourceIndexes?: readonly number[]): DecisionBinding | undefined;
48
50
  /**
49
- * FINAL §3.3's decision menu as data: human-gate/attempt-cap/other non-gate parks → approve; infra →
50
- * approve or recheck; review gate-fail → waive/uphold/recheck; other gate-fail → waive/recheck; a
51
+ * FINAL §3.3's decision menu as data: human-gate/attempt-cap/other non-gate parks → approve; infra, and
52
+ * a stall park that recorded a reapFailure (OBS-1202), → approve or recheck; review gate-fail →
53
+ * waive/uphold/recheck; other gate-fail → waive/recheck; a
51
54
  * gate-fail park with no failed-gate evidence, or a tombstone → nothing (a diagnostic, never a
52
55
  * fabricated verb). The refusals in `approve` below enforce the same table; this is the one place a
53
56
  * surface may read it from, so what a menu offers and what the command accepts cannot drift.
54
57
  */
55
- export declare function permittedDecisionVerbs(park: Pick<NewestPark, "kind" | "failedGate" | "tombstone"> | undefined): readonly DecisionVerb[];
58
+ export declare function permittedDecisionVerbs(park: Pick<NewestPark, "kind" | "failedGate" | "tombstone" | "reapFailure"> | undefined): readonly DecisionVerb[];
56
59
  /** The release marker this command appends for a verb on a park — the fact a read-back must match. */
57
60
  export declare function releaseForDecision(verb: DecisionVerb, park: Pick<NewestPark, "kind" | "failedGate">): string | undefined;
58
61
  export type ApprovalStatus = "deferred-live" | "recorded-no-owner";
@@ -59,6 +59,7 @@ sourceIndexes) {
59
59
  index: i, line: sourceIndexes?.[i] === undefined ? physicalLine(events, i) : sourceIndexes[i] + 1, ts: typeof e.ts === "string" ? e.ts : undefined, kind, reason,
60
60
  approveCommand: typeof e.data.approveCommand === "string" ? e.data.approveCommand : undefined,
61
61
  failedGate: failedGateBeforePark(events, taskId, i), tombstone: isTombstonePark(kind, reason),
62
+ ...(kind === "stall" && typeof e.data.reapFailure === "string" ? { reapFailure: e.data.reapFailure } : {}),
62
63
  };
63
64
  }
64
65
  return undefined;
@@ -83,8 +84,9 @@ export function newestFailure(events, taskId, sourceIndexes) {
83
84
  return undefined;
84
85
  }
85
86
  /**
86
- * FINAL §3.3's decision menu as data: human-gate/attempt-cap/other non-gate parks → approve; infra →
87
- * approve or recheck; review gate-fail → waive/uphold/recheck; other gate-fail → waive/recheck; a
87
+ * FINAL §3.3's decision menu as data: human-gate/attempt-cap/other non-gate parks → approve; infra, and
88
+ * a stall park that recorded a reapFailure (OBS-1202), → approve or recheck; review gate-fail →
89
+ * waive/uphold/recheck; other gate-fail → waive/recheck; a
88
90
  * gate-fail park with no failed-gate evidence, or a tombstone → nothing (a diagnostic, never a
89
91
  * fabricated verb). The refusals in `approve` below enforce the same table; this is the one place a
90
92
  * surface may read it from, so what a menu offers and what the command accepts cannot drift.
@@ -99,6 +101,10 @@ export function permittedDecisionVerbs(park) {
99
101
  }
100
102
  if (park.kind === "infra")
101
103
  return ["approve", "recheck"];
104
+ // OBS-1202: an unreadable reap census parks finished work; recheck re-verifies that census and gates the
105
+ // harvested commits with no worker. An ordinary stall recorded no census failure: approve alone.
106
+ if (park.kind === "stall" && park.reapFailure !== undefined)
107
+ return ["approve", "recheck"];
102
108
  // OBS-1084: an authoring park whose newest gate row is a red tool gate is a runner report (a load
103
109
  // flake in an unowned suite), so the battery re-runs on the preserved ref with no worker.
104
110
  if (park.kind === "authoring" && isToolGate(park.failedGate))
@@ -196,6 +202,7 @@ export async function approve(argv, cwd = process.cwd()) {
196
202
  const gateFailPark = park?.kind === "gate-fail";
197
203
  const infraPark = park?.kind === "infra" || park?.kind === "diff-cap";
198
204
  const authoringRunnerPark = park?.kind === "authoring" && isToolGate(park.failedGate);
205
+ const reapFailurePark = park?.kind === "stall" && park.reapFailure !== undefined;
199
206
  const failedGate = gateFailPark || authoringRunnerPark ? park?.failedGate : undefined;
200
207
  const parkBinding = requireBound(runId, taskId, bound, park?.ts === undefined ? undefined : { line: park.line, ts: park.ts }, "park");
201
208
  if (namedGate !== undefined && namedGate !== park?.failedGate) {
@@ -263,7 +270,10 @@ export async function approve(argv, cwd = process.cwd()) {
263
270
  return disposition(cwd, runId, "fund-fixed-attempt", `upheld the reviewer for ${taskId} in ${runId} — by ${by}`, serialization.contended);
264
271
  }
265
272
  if (recheck) {
266
- if ((!gateFailPark || !failedGate) && !infraPark && !authoringRunnerPark) {
273
+ if (park?.kind === "stall" && !reapFailurePark) {
274
+ throw new Error(`--recheck applies to a stall park only when it recorded a reapFailure (census recovery); ${taskId}'s stall park recorded none — plain approve dispatches a worker; refusing`);
275
+ }
276
+ if ((!gateFailPark || !failedGate) && !infraPark && !authoringRunnerPark && !reapFailurePark) {
267
277
  throw new Error(`--recheck applies to a gate-fail, infra, diff-cap or red-tool-gate authoring park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
268
278
  }
269
279
  journal.append("task-approved", taskId, {
@@ -276,9 +286,11 @@ export async function approve(argv, cwd = process.cwd()) {
276
286
  // carry gate so their durable release row identifies the red tool gate that made recheck admissible.
277
287
  ...(failedGate ? { failedGate } : {}),
278
288
  ...(authoringRunnerPark && failedGate ? { gate: failedGate } : {}),
289
+ // OBS-1202: the daemon re-verifies the parked attempt's owned census before any gate runs.
290
+ ...(reapFailurePark ? { reapFailure: park.reapFailure } : {}),
279
291
  ...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
280
292
  });
281
- return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` : `${park.kind} park`}; no gate marked satisfied`, serialization.contended);
293
+ return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` : reapFailurePark ? "reapFailure stall park; the owned census is re-verified before the gates" : `${park.kind} park`}; no gate marked satisfied`, serialization.contended);
282
294
  }
283
295
  if (waive) {
284
296
  if (!gateFailPark || !failedGate) {
@@ -173,6 +173,8 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
173
173
  tipTest?: string | undefined;
174
174
  lint?: string | undefined;
175
175
  diffCap?: number | undefined;
176
+ evidenceQuotaBytes?: number | undefined;
177
+ repairSelection?: boolean | undefined;
176
178
  byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
177
179
  [x: string]: unknown;
178
180
  acceptance?: boolean | undefined;
@@ -206,8 +208,8 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
206
208
  };
207
209
  executionPolicy?: {
208
210
  boundedInfrastructure: boolean;
209
- repairSelection: boolean;
210
211
  taskExecutionLimitMs: number;
212
+ repairSelection?: boolean | undefined;
211
213
  } | undefined;
212
214
  setup?: string | undefined;
213
215
  cost?: {
@@ -298,6 +300,8 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
298
300
  tipTest?: string | undefined;
299
301
  lint?: string | undefined;
300
302
  diffCap?: number | undefined;
303
+ evidenceQuotaBytes?: number | undefined;
304
+ repairSelection?: boolean | undefined;
301
305
  byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
302
306
  [x: string]: unknown;
303
307
  acceptance?: boolean | undefined;
@@ -331,8 +335,8 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
331
335
  };
332
336
  executionPolicy?: {
333
337
  boundedInfrastructure: boolean;
334
- repairSelection: boolean;
335
338
  taskExecutionLimitMs: number;
339
+ repairSelection?: boolean | undefined;
336
340
  } | undefined;
337
341
  setup?: string | undefined;
338
342
  cost?: {
@@ -5,7 +5,7 @@ import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthE
5
5
  import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
6
6
  import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
7
7
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
8
- import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, lowerLayerModelOverrides, overlayBytesLoadError, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
8
+ import { ConfigError, fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, lowerLayerModelOverrides, overlayBytesLoadError, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, } from "../../config/config.js";
9
9
  import { exclusionReason, projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
10
10
  import { SHAPES, TIERS } from "../../graph/schema.js";
11
11
  import { doctor } from "./doctor.js";
@@ -346,8 +346,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
346
346
  const channels = discoverChannels(cfg, adapters, health);
347
347
  const profile = loadRoutingProfile(cwd, cfg, { preview: true });
348
348
  const stagedDenyOf = (editable) => Object.fromEntries(DENY_SCOPES.map((scope) => [stagedDenyKeyOf(scope), editable[scope.key] ?? []]));
349
+ // OBS-1188: previews stage no effort, so both sides drop efforts — the writer then needs no lower layers
350
+ const previewInitial = { ...structuredClone(initial), efforts: undefined };
349
351
  const stagedEditable = (map, deny) => ({
350
- ...structuredClone(initial),
352
+ ...structuredClone(previewInitial),
351
353
  ...Object.fromEntries(DENY_SCOPES.map((scope) => [scope.key, deny[stagedDenyKeyOf(scope)]])),
352
354
  // OBS-1046: the allow complement is staged beside the deny lists, never folded into them
353
355
  allowOut: deny.allowOut ?? initial.allowOut,
@@ -362,7 +364,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
362
364
  let preview;
363
365
  try {
364
366
  const bytes = renderFleetOverlayWrite(currentRepoOverlayText(cwd), {
365
- initial,
367
+ initial: previewInitial,
366
368
  edited: stagedEditable(map, deny),
367
369
  universe,
368
370
  ...(mode !== rm.mode.mode ? { mode } : {}),
@@ -701,8 +703,25 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
701
703
  const initialJudge = `${cfg.judge.adapter}:${cfg.judge.model}`;
702
704
  let pendingWrite = null;
703
705
  // OBS-1182: the layers under the repo overlay, read raw, so clearing an effort masks an inherited
704
- // one even when those layers only validate beside repo fields.
705
- const lowerOverrides = lowerLayerModelOverrides({ globalDir });
706
+ // one even when those layers only validate beside repo fields. OBS-1188: re-read at every review
707
+ // and save, never an assembly-time snapshot; an unreadable read keeps the last good one for the
708
+ // preview only — the save guard below refuses it.
709
+ let lowerOverrides = {};
710
+ // OBS-1188: the reviewed bytes are the only bytes that may land. Re-rendered from the repo overlay
711
+ // and lower layers as they are NOW; any difference is a stale preview. Null = still current.
712
+ const stalePreview = (write, reviewed, prior = currentRepoOverlayText(cwd)) => {
713
+ const lower = lowerLayerModelOverrides({ globalDir });
714
+ if (!lower.ok)
715
+ return lower.error;
716
+ try {
717
+ if (renderFleetOverlayWrite(prior, { ...write, lowerOverrides: lower.overrides }) === reviewed)
718
+ return null;
719
+ }
720
+ catch (error) {
721
+ return error.message;
722
+ }
723
+ return "stale preview — the repo overlay or a lower config layer changed after the review diff was rendered; press w to review what would be written now";
724
+ };
706
725
  const reviewOverlay = (state) => {
707
726
  const staged = structuredClone(initial);
708
727
  // OBS-994/FL-1, OBS-1099 add.1: every schema-enumerated deny scope rides the same review/write funnel
@@ -737,6 +756,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
737
756
  pendingWrite = null;
738
757
  return { kind: "empty" };
739
758
  }
759
+ const lower = lowerLayerModelOverrides({ globalDir });
760
+ if (lower.ok)
761
+ lowerOverrides = lower.overrides;
740
762
  const write = {
741
763
  initial,
742
764
  edited: staged,
@@ -762,17 +784,20 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
762
784
  catch {
763
785
  poolNotes = [];
764
786
  }
787
+ const notes = [...(lower.ok ? [] : [`${lower.error} — y will be refused`]), ...poolNotes];
765
788
  return {
766
789
  kind: "diff",
767
790
  before,
768
791
  after,
769
792
  diff: unifiedYamlDiff(before, after, path),
770
793
  path,
771
- ...(poolNotes.length ? { notes: poolNotes } : {}),
794
+ ...(notes.length ? { notes } : {}),
772
795
  };
773
796
  };
774
- const reloadGuard = io.reloadGuard
797
+ const loaderGuard = io.reloadGuard
775
798
  ?? ((bytes) => overlayBytesLoadError(cwd, bytes, { globalDir }));
799
+ // OBS-1188: a stale preview or an unreadable lower layer answers on the same error channel as the loader
800
+ const reloadGuard = (bytes) => (pendingWrite && stalePreview(pendingWrite, bytes)) || loaderGuard(bytes);
776
801
  const props = {
777
802
  ageMs: doctorAgeMs(cwd),
778
803
  adapters,
@@ -820,7 +845,20 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
820
845
  const write = pendingWrite;
821
846
  if (!write)
822
847
  throw new Error("fleet write reached confirmation without a staged overlay mutation");
823
- writeFleetOverlay(result.review.path, (prior) => renderFleetOverlayWrite(prior, write));
848
+ // OBS-1188: re-checked against the bytes on disk at write time; a refusal throws before the temp file exists
849
+ try {
850
+ writeFleetOverlay(result.review.path, (prior) => {
851
+ const stale = stalePreview(write, result.review.after, prior);
852
+ if (stale !== null)
853
+ throw new ConfigError(stale);
854
+ return result.review.after;
855
+ });
856
+ }
857
+ catch (error) {
858
+ if (error instanceof ConfigError)
859
+ return `fleet: nothing written — ${error.message}`;
860
+ throw error;
861
+ }
824
862
  // OBS-529: a freshly classified model has no probe verdict (doctor probes CONFIGURED models,
825
863
  // and it was not configured at probe time), so it stays unroutable — invisible in every
826
864
  // picker — until the next probe. Name the step, or the classify flow reads as broken.
@@ -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";
@@ -12,6 +13,7 @@ import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChann
12
13
  import { cellsOf, cellSummary } from "../../route/profile.js";
13
14
  import { Journal, loadRoutingProfile } from "../../run/journal.js";
14
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.
@@ -194,7 +234,7 @@ const fingerprintClauses = (data) => {
194
234
  + `; forgiven vs baseline: ${formatFingerprints(forgiven)} — ${formatBaselineProvenance(baselineProvenanceOf(data.baselineProvenance))}`;
195
235
  };
196
236
  // VIS-07 / REC-01: derived only from the run journal, telemetry, and local configuration.
197
- export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
237
+ export function renderMarkdownRecord(runId, events, prices = [], rows = [], suite) {
198
238
  const runStart = events.find((e) => e.event === "run-start");
199
239
  const runEnd = [...events].reverse().find((e) => e.event === "run-end");
200
240
  const baseRef = typeof runStart?.data.baseRef === "string" ? runStart.data.baseRef : EM;
@@ -242,6 +282,11 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
242
282
  `- **consults:** ${events.filter((e) => e.event === "consult-verdict").length}`,
243
283
  `- **escalations:** ${events.filter((e) => e.event === "escalation").length}`,
244
284
  "",
285
+ "## Wall budget",
286
+ "",
287
+ ...wallFacts(events).map(([label, fact]) => `- **${label}:** ${fact}`),
288
+ `- **suite telemetry (baseline test capture):** ${suiteTelemetry(suite)}`,
289
+ "",
245
290
  ];
246
291
  lines.push("## Channels", "");
247
292
  const operatorRecords = buildOperatorRecord(events, prices);
@@ -326,7 +371,7 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
326
371
  }
327
372
  return lines.join("\n").trimEnd() + "\n";
328
373
  }
329
- function textReport(runId, events, rows, cwd) {
374
+ function textReport(runId, events, rows, cwd, suite) {
330
375
  // one group per adapter:model, carrying channel + the folded usage across its rows
331
376
  const groups = new Map();
332
377
  for (const r of rows) {
@@ -388,6 +433,10 @@ function textReport(runId, events, rows, cwd) {
388
433
  `tickmark rate: ${gateRan ? Math.round((100 * gatePass) / gateRan) : 0}% (${gatePass}/${gateRan})${declinedCount ? ` · declined: ${declinedCount}` : ""}`,
389
434
  `escalations: ${escalations} · National Office consults: ${consults} · quota failovers: ${failovers}`,
390
435
  "",
436
+ "wall budget — exposed wall, disjoint:",
437
+ ...wallFacts(events).map(([label, fact]) => ` ${label.padEnd(14)} ${fact}`),
438
+ ` ${"suite".padEnd(14)} ${suiteTelemetry(suite)}`,
439
+ "",
391
440
  "spend — tokens (measured where observed):",
392
441
  ...tokenLines,
393
442
  "spend — money:",
@@ -407,7 +456,7 @@ const stylizeReport = (out) => {
407
456
  return out;
408
457
  return out
409
458
  .replace(/^.*$/m, (first) => `${title(first)}\n${rule()}`) // non-global /m ⇒ first line only
410
- .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));
411
460
  };
412
461
  export async function report(argv, cwd = process.cwd()) {
413
462
  const { values, positionals } = parseArgs({
@@ -428,6 +477,7 @@ export async function report(argv, cwd = process.cwd()) {
428
477
  const events = j.read();
429
478
  const rows = j.readTelemetry();
430
479
  const cfg = loadConfig(cwd);
480
+ const suite = baselineTestEntry(j.dir);
431
481
  let bundleNote = "";
432
482
  if (values.bundle) {
433
483
  // Local-only write of the pure proof packet — no network path exists in buildProofBundle.
@@ -454,7 +504,7 @@ export async function report(argv, cwd = process.cwd()) {
454
504
  comparison = "\n" + outcome.text;
455
505
  }
456
506
  if (values.md) {
457
- return bundleNote + renderMarkdownRecord(runId, events, estimateCosts(rows, cfg.cost), rows) + comparison;
507
+ return bundleNote + renderMarkdownRecord(runId, events, estimateCosts(rows, cfg.cost), rows, suite) + comparison;
458
508
  }
459
- return bundleNote + stylizeReport(textReport(runId, events, rows, cwd)) + comparison;
509
+ return bundleNote + stylizeReport(textReport(runId, events, rows, cwd, suite)) + comparison;
460
510
  }
@@ -17,6 +17,7 @@ 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";
@@ -1155,6 +1156,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
1155
1156
  ...(hotPhase ? { hotPhase } : {}),
1156
1157
  ...(runId ? { runId } : {}),
1157
1158
  workerPhases: [...phases.values()].filter((phase) => phase.phase === "worker"),
1159
+ events,
1158
1160
  };
1159
1161
  }
1160
1162
  // TTY: the operator-approved task table. The daemon gives this surface a ~110-column pane first;
@@ -1364,8 +1366,22 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
1364
1366
  ...(hotPhase ? { hotPhase } : {}),
1365
1367
  ...(runId ? { runId } : {}),
1366
1368
  workerPhases: [...phases.values()].filter((phase) => phase.phase === "worker"),
1369
+ events,
1367
1370
  };
1368
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
+ };
1369
1385
  /**
1370
1386
  * The compact form a statusline calls, so journal interpretation happens ONCE, inside the product.
1371
1387
  * (An operator-local bash reimplementation counted `tip-verify` events instead of reading the
@@ -1401,7 +1417,9 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1401
1417
  // The task-table frame owns its own two-row brand lockup; the four-row global banner would
1402
1418
  // create the second header the approved replacement removes.
1403
1419
  if (!argv.includes("--watch")) {
1404
- 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;
1405
1423
  }
1406
1424
  const eventStream = argv.some((arg) => arg === "--events" || arg === "--jsonl" || arg === "--decision-events");
1407
1425
  const webhookUrl = opts.webhookUrl
@@ -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
@@ -181,13 +181,13 @@ export declare function effectiveReviewPolicy(files: ReadonlyArray<string>, revi
181
181
  export declare function criticalPathHits(files: ReadonlyArray<string>, criticalPaths?: ReadonlyArray<string>): string[];
182
182
  export declare const ExecutionPolicySchema: z.ZodObject<{
183
183
  boundedInfrastructure: z.ZodDefault<z.ZodBoolean>;
184
- repairSelection: z.ZodDefault<z.ZodBoolean>;
184
+ repairSelection: z.ZodOptional<z.ZodBoolean>;
185
185
  taskExecutionLimitMs: z.ZodNumber;
186
186
  }, z.core.$strict>;
187
187
  export declare const TickmarkrConfigSchema: z.ZodObject<{
188
188
  executionPolicy: z.ZodOptional<z.ZodObject<{
189
189
  boundedInfrastructure: z.ZodDefault<z.ZodBoolean>;
190
- repairSelection: z.ZodDefault<z.ZodBoolean>;
190
+ repairSelection: z.ZodOptional<z.ZodBoolean>;
191
191
  taskExecutionLimitMs: z.ZodNumber;
192
192
  }, z.core.$strict>>;
193
193
  concurrency: z.ZodNumber;
@@ -309,6 +309,8 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
309
309
  tipTest: z.ZodOptional<z.ZodString>;
310
310
  lint: z.ZodOptional<z.ZodString>;
311
311
  diffCap: z.ZodOptional<z.ZodNumber>;
312
+ evidenceQuotaBytes: z.ZodOptional<z.ZodNumber>;
313
+ repairSelection: z.ZodOptional<z.ZodBoolean>;
312
314
  byShape: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodEnum<{
313
315
  plan: "plan";
314
316
  spec: "spec";
@@ -374,6 +376,7 @@ export type TickmarkrConfig = z.infer<typeof TickmarkrConfigSchema>;
374
376
  export declare class ConfigError extends Error {
375
377
  constructor(message: string);
376
378
  }
379
+ export declare const DEFAULT_EVIDENCE_QUOTA_BYTES: number;
377
380
  export declare const DEFAULT_CONFIG: TickmarkrConfig;
378
381
  export declare function globalConfigDir(): string;
379
382
  export declare function overlayPreferShapes(repoRoot: string, opts?: {
@@ -395,12 +398,22 @@ export declare function loadConfigWithMode(repoRoot: string, opts?: {
395
398
  cfg: TickmarkrConfig;
396
399
  mode: ModeResolution;
397
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>>>;
398
403
  /** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the repo overlay (defaults + global)
399
404
  * merge them. Read raw, never schema-validated: a lower layer may only validate beside repo fields
400
- * (global `vendor: null` completed by a repo vendor) and its override metadata is still inherited. */
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. */
401
408
  export declare function lowerLayerModelOverrides(opts?: {
402
409
  globalDir?: string;
403
- }): Record<string, Record<string, Record<string, unknown>>>;
410
+ }): {
411
+ ok: true;
412
+ overrides: LowerLayerModelOverrides;
413
+ } | {
414
+ ok: false;
415
+ error: string;
416
+ };
404
417
  export declare function loadConfig(repoRoot: string, opts?: {
405
418
  globalDir?: string;
406
419
  }): TickmarkrConfig;