tickmarkr 2.6.2 → 2.6.4

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 (79) hide show
  1. package/README.md +12 -2
  2. package/dist/adapters/codex.d.ts +2 -1
  3. package/dist/adapters/codex.js +26 -5
  4. package/dist/adapters/model-lints.d.ts +1 -0
  5. package/dist/adapters/model-lints.js +48 -1
  6. package/dist/adapters/model-windows.d.ts +2 -1
  7. package/dist/adapters/model-windows.js +18 -7
  8. package/dist/adapters/types.d.ts +6 -0
  9. package/dist/cli/commands/approve.d.ts +6 -3
  10. package/dist/cli/commands/approve.js +43 -6
  11. package/dist/cli/commands/doctor.d.ts +8 -2
  12. package/dist/cli/commands/doctor.js +6 -1
  13. package/dist/cli/commands/fleet.js +264 -59
  14. package/dist/cli/commands/init.js +5 -3
  15. package/dist/cli/commands/plan.js +13 -8
  16. package/dist/cli/commands/report.d.ts +40 -2
  17. package/dist/cli/commands/report.js +278 -11
  18. package/dist/cli/commands/resume.js +2 -4
  19. package/dist/cli/commands/run.d.ts +11 -0
  20. package/dist/cli/commands/run.js +33 -5
  21. package/dist/cli/commands/status.js +49 -4
  22. package/dist/cli/commands/verify.js +332 -121
  23. package/dist/compile/native.js +137 -0
  24. package/dist/config/config.d.ts +40 -9
  25. package/dist/config/config.js +133 -15
  26. package/dist/config/fleet-overlay.d.ts +13 -3
  27. package/dist/config/fleet-overlay.js +12 -8
  28. package/dist/drivers/herdr.d.ts +12 -0
  29. package/dist/drivers/herdr.js +51 -0
  30. package/dist/drivers/orca.d.ts +9 -1
  31. package/dist/drivers/orca.js +29 -7
  32. package/dist/drivers/types.d.ts +2 -0
  33. package/dist/drivers/types.js +2 -2
  34. package/dist/gates/acceptance.d.ts +7 -0
  35. package/dist/gates/acceptance.js +27 -5
  36. package/dist/gates/baseline.d.ts +20 -1
  37. package/dist/gates/baseline.js +100 -20
  38. package/dist/gates/cache.d.ts +8 -0
  39. package/dist/gates/cache.js +12 -2
  40. package/dist/gates/llm.d.ts +6 -0
  41. package/dist/gates/llm.js +27 -8
  42. package/dist/gates/review.d.ts +6 -1
  43. package/dist/gates/review.js +122 -32
  44. package/dist/gates/run-gates.d.ts +54 -3
  45. package/dist/gates/run-gates.js +331 -45
  46. package/dist/gates/test-manifest.d.ts +42 -0
  47. package/dist/gates/test-manifest.js +69 -10
  48. package/dist/route/router.d.ts +23 -1
  49. package/dist/route/router.js +54 -16
  50. package/dist/run/consult.d.ts +3 -1
  51. package/dist/run/consult.js +4 -2
  52. package/dist/run/daemon.d.ts +2 -1
  53. package/dist/run/daemon.js +349 -79
  54. package/dist/run/interactive-seed.d.ts +4 -0
  55. package/dist/run/interactive-seed.js +35 -9
  56. package/dist/run/journal.d.ts +123 -1
  57. package/dist/run/journal.js +480 -17
  58. package/dist/run/lease.d.ts +13 -0
  59. package/dist/run/lease.js +45 -0
  60. package/dist/run/protocol.d.ts +15 -0
  61. package/dist/run/protocol.js +11 -1
  62. package/dist/run/receipt-resolver.d.ts +22 -0
  63. package/dist/run/receipt-resolver.js +40 -1
  64. package/dist/run/repair-selection.d.ts +11 -1
  65. package/dist/run/repair-selection.js +17 -9
  66. package/dist/run/wall-budget.d.ts +48 -0
  67. package/dist/run/wall-budget.js +280 -0
  68. package/dist/tui/cockpit/live-store.d.ts +6 -0
  69. package/dist/tui/cockpit/live-store.js +36 -11
  70. package/dist/tui/cockpit/run-cockpit.js +2 -2
  71. package/dist/tui/cockpit/run-view.d.ts +2 -1
  72. package/dist/tui/cockpit/run-view.js +13 -9
  73. package/dist/tui/cockpit/setup-cockpit.d.ts +2 -0
  74. package/dist/tui/cockpit/setup-cockpit.js +4 -0
  75. package/package.json +2 -1
  76. package/schema/config.schema.json +8 -1
  77. package/skills/tickmarkr-auto/SKILL.md +2 -2
  78. package/skills/tickmarkr-loop/SKILL.md +17 -5
  79. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +4 -1
package/README.md CHANGED
@@ -183,8 +183,16 @@ can enact the release at its next task boundary; a different live run must end b
183
183
  one resumes. Keep any resume refusal and its remediation visible, including a deny/prefer
184
184
  config conflict; repair the source/config as directed, never edit the compiled graph to
185
185
  force success. After resume, CURRENT TIP is PENDING until fresh evidence arrives. Completion
186
- requires the latest run-end, a nonfailed known tip result, and empty `failed`, `human`,
187
- `blocked` and `pending` buckets; the completed case has 3/3 recorded merges. An unrelated
186
+ requires the latest run-end, a nonfailed known tip result, empty `failed`, `human`,
187
+ `blocked` and `pending` buckets, and owed checks outstanding empty AND known in CURRENT
188
+ `tickmarkr status <runId>` (`outstanding 0`); the completed case has 3/3 recorded merges.
189
+ Empty buckets alone are not completion (D-660): 3/3 merged with every bucket empty and the
190
+ tip passed, after the operator waived T2's review, leaves one accepted-risk review check owed.
191
+ `run`/`resume` exit 0 on execution alone, their final line says
192
+ `execution complete; outstanding 1 (T2 review)` rather than `verified`, and status reads
193
+ `outstanding 1`. That run completes only when `tickmarkr verify --record <runId>` discharges
194
+ the check and CURRENT status reads `outstanding 0` — a discharge after run-end counts, while the
195
+ run-end record keeps its old count; `outstanding unknown` never completes. An unrelated
188
196
  graph says “not comparable” and supplies no borrowed denominator. Historical GATES RAN
189
197
  does not establish current completion.
190
198
 
@@ -208,6 +216,8 @@ worker. Other parks have different permitted decisions:
208
216
  | Human gate / other non-gate park | Plain approve records permission to dispatch. |
209
217
  | Attempt cap | Plain approve grants a fresh attempt budget; prior routing exclusions remain. |
210
218
  | Infrastructure | Plain approve or `--recheck`; recheck reruns the declared battery and satisfies no gate. |
219
+ | 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. |
220
+ | Ordinary stall (no `reapFailure`) | Plain approve only — it dispatches a worker; `--recheck` refuses. |
211
221
  | Failed review gate | `--waive` satisfies only that identified gate; `--uphold` funds one fixed attempt carrying findings; `--recheck` reruns the battery. |
212
222
  | Other failed gate | `--waive` or `--recheck`; plain approve refuses. |
213
223
  | Tombstone / gate failure without identifying evidence | Diagnostic only; no invented decision. |
@@ -1,7 +1,8 @@
1
- import { type TrustDialog, type TrustVerdict, type WorkerAdapter } from "./types.js";
1
+ import { type ModelRetirement, type TrustDialog, type TrustVerdict, type WorkerAdapter } from "./types.js";
2
2
  export declare function readCodexModelsCache(path?: string): {
3
3
  models: string[];
4
4
  fetchedAt?: string;
5
+ retirements?: Record<string, ModelRetirement | null>;
5
6
  };
6
7
  export declare const CODEX_TRUST_DIALOG: TrustDialog;
7
8
  export declare const CODEX_INPUT_BOX: import("./types.js").InputBox;
@@ -61,20 +61,39 @@ function codexSessionDayDirs(sessions, sinceMs) {
61
61
  // Fails OPEN to { models: [] } on missing/corrupt cache. Version drift observed (installed CLI
62
62
  // 0.143.0 vs cache client_version 0.144.0, 2026-07-10) — hence the defensive try/catch and no shape
63
63
  // assumptions. CODEX_HOME is codex's own relocation env. fetchedAt is the honest codex knowledge age.
64
+ // v2.6.4 T2 (B1a): each listed entry's `upgrade` is the CLI's own retirement notice (0.159.0 cache,
65
+ // 2026-09-29: gpt-5.5 → {model: gpt-5.6-sol, retirement_at: 2026-10-14T19:00:00Z}; every other listed
66
+ // id carries upgrade: null). null records known clean; a malformed or absent `upgrade` records nothing,
67
+ // so that id stays unknown. An unreadable cache returns no retirements at all (unknown, never clean).
64
68
  export function readCodexModelsCache(path) {
65
69
  const p = path ?? join(process.env.CODEX_HOME || join(homedir(), ".codex"), "models_cache.json");
66
70
  try {
67
71
  const d = JSON.parse(readFileSync(p, "utf8"));
68
- const models = (d.models ?? [])
69
- .filter((m) => m?.visibility === "list" && typeof m.slug === "string" && m.slug.length > 0)
70
- .map((m) => m.slug)
71
- .filter((id) => MODEL_ID_RE.test(id));
72
- return { models, fetchedAt: typeof d.fetched_at === "string" ? d.fetched_at : undefined };
72
+ const listed = (d.models ?? [])
73
+ .filter((m) => m?.visibility === "list" && typeof m.slug === "string" && m.slug.length > 0 && MODEL_ID_RE.test(m.slug));
74
+ const retirements = {};
75
+ for (const m of listed) {
76
+ const notice = codexRetirement(m.upgrade);
77
+ if (notice !== undefined)
78
+ retirements[m.slug] = notice;
79
+ }
80
+ return { models: listed.map((m) => m.slug), fetchedAt: typeof d.fetched_at === "string" ? d.fetched_at : undefined, retirements };
73
81
  }
74
82
  catch {
75
83
  return { models: [] };
76
84
  }
77
85
  }
86
+ function codexRetirement(upgrade) {
87
+ if (upgrade === null)
88
+ return null;
89
+ const u = upgrade;
90
+ if (typeof u?.retirement_at !== "string" || !Number.isFinite(Date.parse(u.retirement_at)))
91
+ return undefined;
92
+ if (u.model === undefined)
93
+ return { retiresAt: u.retirement_at };
94
+ // the successor id comes from an external file and is rendered in lints — same charset gate as slugs
95
+ return typeof u.model === "string" && MODEL_ID_RE.test(u.model) ? { retiresAt: u.retirement_at, successor: u.model } : undefined;
96
+ }
78
97
  // tickmarkr worktrees keep their gitdir under the MAIN repo's .git/worktrees/<name> — outside the
79
98
  // workspace-write sandbox root — so git commit dies on index.lock (incident run-20260709-104447
80
99
  // P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the pane shell at the
@@ -272,6 +291,8 @@ export const codex = {
272
291
  // v1.5 MODEL-05: codex reads an offline cache, so "now" would re-stamp an ancient cache fresh and the
273
292
  // 30-day staleness lint could never fire. Surface the cache's own fetched_at so doctor stamps the real age.
274
293
  listModelsFetchedAt: () => readCodexModelsCache().fetchedAt,
294
+ // v2.6.4 T2 (B1a): the cache's per-model retirement notices, persisted beside models in doctor.json.
295
+ listModelsRetirements: () => readCodexModelsCache().retirements,
275
296
  collectUsage(cwd, sinceMs) {
276
297
  try {
277
298
  const real = realpathSync(cwd);
@@ -81,6 +81,7 @@ export declare function modelLints(cfg: TickmarkrConfig, health: Record<string,
81
81
  tty?: boolean;
82
82
  stateDir?: string;
83
83
  overlayPreferShapes?: ReadonlySet<string>;
84
+ now?: Date;
84
85
  }): string[];
85
86
  export declare function formatModelAuthLine(excluded: {
86
87
  key: string;
@@ -478,10 +478,55 @@ export function preferEntryLints(cfg, health, mapShapes = new Set()) {
478
478
  sweep("consult.prefer", cfg.consult.prefer);
479
479
  return lints;
480
480
  }
481
+ // v2.6.4 T2 (B1a): doctor.json is read without a schema (compat), so a stored notice is re-checked here —
482
+ // anything but null (known clean) or a dated notice reads as unknown, never as clean.
483
+ function storedRetirement(record, model) {
484
+ if (!record || typeof record !== "object")
485
+ return undefined;
486
+ const r = record[model];
487
+ if (r === null)
488
+ return null;
489
+ const n = r;
490
+ if (typeof n?.retiresAt !== "string" || !Number.isFinite(Date.parse(n.retiresAt)))
491
+ return undefined;
492
+ if (n.successor === undefined)
493
+ return { retiresAt: n.retiresAt };
494
+ return typeof n.successor === "string" && MODEL_ID_RE.test(n.successor) ? { retiresAt: n.retiresAt, successor: n.successor } : undefined;
495
+ }
496
+ // Configured AND listed models only — a configured id the CLI stopped listing already carries the
497
+ // tombstone advisory. The CLI's notice is quoted as-is: its successor is the CLI's upgrade target, never
498
+ // a tier or benchmark claim, and no model name or visibility is read as one. Advisory: routing unchanged.
499
+ function retirementLints(id, listedConfigured, record, nowMs) {
500
+ const lints = [];
501
+ const unknown = [];
502
+ for (const model of listedConfigured) {
503
+ const notice = storedRetirement(record, model);
504
+ if (notice === undefined) {
505
+ unknown.push(model);
506
+ continue;
507
+ }
508
+ if (!notice)
509
+ continue; // listed with no notice: known clean
510
+ const successor = notice.successor
511
+ ? `successor ${notice.successor} (named by the CLI — not a tier claim; classify it per benchmark policy)`
512
+ : "no successor named";
513
+ lints.push(nowMs >= Date.parse(notice.retiresAt)
514
+ ? `${id}: ${model} retired ${notice.retiresAt} per the CLI's notice; ${successor} — tombstone it (${model}: null overlay) or verify the id (advisory — routing unchanged)`
515
+ : `${id}: ${model} retires ${notice.retiresAt} per the CLI's notice; ${successor} — tombstone it (${model}: null overlay) before then (advisory — routing unchanged)`);
516
+ }
517
+ if (unknown.length) {
518
+ const why = record && typeof record === "object"
519
+ ? "the CLI's notice is absent or unreadable"
520
+ : "doctor record carries no CLI retirement notices; rerun tickmarkr doctor";
521
+ lints.push(`${id}: retirement unknown for ${unknown.join(", ")} — ${why}`);
522
+ }
523
+ return lints;
524
+ }
481
525
  // Diffs detected models (doctor.json) against configured tiers, both directions, per installed adapter.
482
526
  // No ` ! ` prefix here — the consumer (doctor rows / plan lints) owns that. Pre-v1.5 doctor.json (models:[], no
483
527
  // modelsDetectedAt) is the compat baseline: `?.`/`?? []` everywhere, no zod (would reject old files).
484
528
  export function modelLints(cfg, health, adapters, opts) {
529
+ const nowMs = (opts?.now ?? new Date()).getTime();
485
530
  const cap = opts?.tty ? TTY_LINT_CAP : LINT_CAP;
486
531
  const doctorRef = opts?.tty ? doctorJsonRef(opts.stateDir ?? DEFAULT_STATE_DIR) : "";
487
532
  const lints = [];
@@ -517,6 +562,8 @@ export function modelLints(cfg, health, adapters, opts) {
517
562
  lints.push(`${id}: tiers lists ${model} — CLI no longer reports it; tombstone it (${model}: null overlay) or verify the id`);
518
563
  }
519
564
  }
565
+ if (adapter.listModelsRetirements)
566
+ lints.push(...retirementLints(id, configured.filter((m) => detected.includes(m)), h?.modelRetirements, nowMs));
520
567
  const extra = collapseUnclassified(detected, new Set(configured));
521
568
  if (extra.length) {
522
569
  const shown = extra.slice(0, cap).map((row) => row.model).join(", ");
@@ -525,7 +572,7 @@ export function modelLints(cfg, health, adapters, opts) {
525
572
  }
526
573
  const at = h?.modelsDetectedAt;
527
574
  if (at) {
528
- const days = Math.floor((Date.now() - Date.parse(at)) / DAY_MS); // completed days — never overstate age
575
+ const days = Math.floor((nowMs - Date.parse(at)) / DAY_MS); // completed days — never overstate age
529
576
  if (days >= MODEL_STALE_DAYS)
530
577
  lints.push(`${id}: model knowledge is ${days} days old — rerun tickmarkr doctor`);
531
578
  }
@@ -15,7 +15,8 @@ export type ModelWindowResolution = {
15
15
  };
16
16
  /**
17
17
  * Validate cited model-window claims before production code can consume them.
18
- * These values are vendor-published claims, not measurements made by tickmarkr.
18
+ * These values are vendor-published claims (a vendor page, or a vendor CLI's own model cache), not
19
+ * measurements made by tickmarkr.
19
20
  */
20
21
  export declare function loadModelWindowClaims(input: unknown): readonly ModelWindowClaim[];
21
22
  /** The closed, production-readable catalog of cited context-window claims. */
@@ -9,7 +9,8 @@ const ModelWindowClaimSchema = z
9
9
  .strict();
10
10
  /**
11
11
  * Validate cited model-window claims before production code can consume them.
12
- * These values are vendor-published claims, not measurements made by tickmarkr.
12
+ * These values are vendor-published claims (a vendor page, or a vendor CLI's own model cache), not
13
+ * measurements made by tickmarkr.
13
14
  */
14
15
  export function loadModelWindowClaims(input) {
15
16
  const claims = z.array(ModelWindowClaimSchema).parse(input);
@@ -22,8 +23,15 @@ export function loadModelWindowClaims(input) {
22
23
  return Object.freeze(claims.map((claim) => Object.freeze(claim)));
23
24
  }
24
25
  const ANTHROPIC_SOURCE = "https://platform.claude.com/docs/en/about-claude/models/overview";
25
- const OPENAI_SOURCE = "https://developers.openai.com/api/docs/models";
26
- const OPENAI_GPT_55_SOURCE = "https://developers.openai.com/api/docs/models/gpt-5.5";
26
+ // v2.6.4 T3 (D-717): the Codex rows cite the installed CLI's own models_cache.json snapshot, not the API
27
+ // docs. That file has no public URL, so the source names it in its own scheme, snapshot instant included.
28
+ // Every listed entry there carried context_window=272000 — the budget the codex CLI launches with. The
29
+ // API docs' 1,050,000 (formerly cited here) describes the API surface; it is superseded for these CLI
30
+ // seeds, not called false. These windows are the CLI's self-report, not a tickmarkr measurement. Only the
31
+ // window comes from this snapshot: the codex tiers seeded beside it (config.ts) rest on the provider's
32
+ // descriptions and the retained workhorse tier policy, not on any measured benchmark.
33
+ const CODEX_CLI_CACHE_SOURCE = "codex-cli:models_cache.json#fetched_at=2026-09-29T16:09Z";
34
+ const CODEX_CLI_CACHE_READ_DATE = "2026-09-29";
27
35
  const CURSOR_SOURCE = "https://cursor.com/help/ai-features/max-mode";
28
36
  const ZAI_SOURCE = "https://z.ai/blog/glm-5.2";
29
37
  const XAI_SOURCE = "https://docs.x.ai/developers/models/grok-4.5";
@@ -37,10 +45,13 @@ const VENDORED_MODEL_WINDOW_CLAIMS = [
37
45
  { modelId: "opus", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
38
46
  { modelId: "sonnet", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
39
47
  { modelId: "haiku", window: 200_000, source: ANTHROPIC_SOURCE, readDate: READ_DATE },
40
- { modelId: "gpt-5.6-sol", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
41
- { modelId: "gpt-5.5", window: 1_050_000, source: OPENAI_GPT_55_SOURCE, readDate: READ_DATE },
42
- { modelId: "gpt-5.6-terra", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
43
- { modelId: "gpt-5.6-luna", window: 1_050_000, source: OPENAI_SOURCE, readDate: READ_DATE },
48
+ { modelId: "gpt-6-astra", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
49
+ { modelId: "gpt-6-sol", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
50
+ { modelId: "gpt-6-luna", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
51
+ { modelId: "gpt-5.6-sol", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
52
+ { modelId: "gpt-5.5", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
53
+ { modelId: "gpt-5.6-terra", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
54
+ { modelId: "gpt-5.6-luna", window: 272_000, source: CODEX_CLI_CACHE_SOURCE, readDate: CODEX_CLI_CACHE_READ_DATE },
44
55
  { modelId: "composer-2.5", window: 200_000, source: CURSOR_SOURCE, readDate: READ_DATE },
45
56
  { modelId: "composer-2.5-fast", window: 200_000, source: CURSOR_SOURCE, readDate: READ_DATE },
46
57
  { modelId: "claude-fable-5-1", window: 1_000_000, source: ANTHROPIC_SOURCE, readDate: OBS_871_READ_DATE },
@@ -36,6 +36,10 @@ export interface ModelAuth {
36
36
  probedAt: string;
37
37
  identity?: string;
38
38
  }
39
+ export interface ModelRetirement {
40
+ retiresAt: string;
41
+ successor?: string;
42
+ }
39
43
  export interface AuthHealth {
40
44
  installed: boolean;
41
45
  authed: boolean;
@@ -46,6 +50,7 @@ export interface AuthHealth {
46
50
  servable?: string[];
47
51
  modelAuth?: Record<string, ModelAuth>;
48
52
  modelIdentities?: Record<string, string>;
53
+ modelRetirements?: Record<string, ModelRetirement | null>;
49
54
  }
50
55
  export declare function modelAuthed(health: AuthHealth | undefined, model: string, allowUnverifiedModels?: boolean): boolean;
51
56
  export interface Invocation {
@@ -190,6 +195,7 @@ export interface WorkerAdapter {
190
195
  parse(output: string, nonce: string): WorkerResult;
191
196
  listModels?(): Promise<string[]>;
192
197
  listModelsFetchedAt?(): string | undefined;
198
+ listModelsRetirements?(): Record<string, ModelRetirement | null> | undefined;
193
199
  collectUsage?(cwd: string, sinceMs: number): TokenUsage | undefined;
194
200
  contextUsage?(session: SessionRef): ContextUsage | null;
195
201
  readSessionTranscript?(session: SessionRef): {
@@ -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";
@@ -1,11 +1,13 @@
1
1
  import { graphDefinitionHash, loadGraph, saveGraph, taskDefinitionFingerprint } from "../../graph/graph.js";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { userInfo } from "node:os";
4
+ import { allAdapters } from "../../adapters/registry.js";
5
+ import { channelKey } from "../../adapters/types.js";
4
6
  import { loadConfig } from "../../config/config.js";
5
7
  import { integrationBranch } from "../../run/merge.js";
6
8
  import { separabilityErrors, surfaceErrors, taskBudgetErrors } from "../../compile/collateral.js";
7
9
  import { GATE_NAMES } from "../../graph/schema.js";
8
- import { applyScopeAmendments, bindingToken, engagementComparable, engagementReleased, failedGateBeforePark, parseBindingToken, physicalLine, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
10
+ import { applyScopeAmendments, bindingToken, captureOwedCheck, engagementComparable, engagementReleased, failedGateBeforePark, parseBindingToken, physicalLine, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
9
11
  export const APPROVAL_DISPOSITIONS = ["dispatch", "waive-gate", "re-dispatch", "fund-fixed-attempt", "fresh-budget"];
10
12
  /**
11
13
  * What each disposition's release actually buys — the clause every operator-facing sentence about
@@ -59,6 +61,7 @@ sourceIndexes) {
59
61
  index: i, line: sourceIndexes?.[i] === undefined ? physicalLine(events, i) : sourceIndexes[i] + 1, ts: typeof e.ts === "string" ? e.ts : undefined, kind, reason,
60
62
  approveCommand: typeof e.data.approveCommand === "string" ? e.data.approveCommand : undefined,
61
63
  failedGate: failedGateBeforePark(events, taskId, i), tombstone: isTombstonePark(kind, reason),
64
+ ...(kind === "stall" && typeof e.data.reapFailure === "string" ? { reapFailure: e.data.reapFailure } : {}),
62
65
  };
63
66
  }
64
67
  return undefined;
@@ -83,8 +86,9 @@ export function newestFailure(events, taskId, sourceIndexes) {
83
86
  return undefined;
84
87
  }
85
88
  /**
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
89
+ * FINAL §3.3's decision menu as data: human-gate/attempt-cap/other non-gate parks → approve; infra, and
90
+ * a stall park that recorded a reapFailure (OBS-1202), → approve or recheck; review gate-fail →
91
+ * waive/uphold/recheck; other gate-fail → waive/recheck; a
88
92
  * gate-fail park with no failed-gate evidence, or a tombstone → nothing (a diagnostic, never a
89
93
  * fabricated verb). The refusals in `approve` below enforce the same table; this is the one place a
90
94
  * surface may read it from, so what a menu offers and what the command accepts cannot drift.
@@ -99,6 +103,10 @@ export function permittedDecisionVerbs(park) {
99
103
  }
100
104
  if (park.kind === "infra")
101
105
  return ["approve", "recheck"];
106
+ // OBS-1202: an unreadable reap census parks finished work; recheck re-verifies that census and gates the
107
+ // harvested commits with no worker. An ordinary stall recorded no census failure: approve alone.
108
+ if (park.kind === "stall" && park.reapFailure !== undefined)
109
+ return ["approve", "recheck"];
102
110
  // OBS-1084: an authoring park whose newest gate row is a red tool gate is a runner report (a load
103
111
  // flake in an unowned suite), so the battery re-runs on the preserved ref with no worker.
104
112
  if (park.kind === "authoring" && isToolGate(park.failedGate))
@@ -196,6 +204,7 @@ export async function approve(argv, cwd = process.cwd()) {
196
204
  const gateFailPark = park?.kind === "gate-fail";
197
205
  const infraPark = park?.kind === "infra" || park?.kind === "diff-cap";
198
206
  const authoringRunnerPark = park?.kind === "authoring" && isToolGate(park.failedGate);
207
+ const reapFailurePark = park?.kind === "stall" && park.reapFailure !== undefined;
199
208
  const failedGate = gateFailPark || authoringRunnerPark ? park?.failedGate : undefined;
200
209
  const parkBinding = requireBound(runId, taskId, bound, park?.ts === undefined ? undefined : { line: park.line, ts: park.ts }, "park");
201
210
  if (namedGate !== undefined && namedGate !== park?.failedGate) {
@@ -263,7 +272,10 @@ export async function approve(argv, cwd = process.cwd()) {
263
272
  return disposition(cwd, runId, "fund-fixed-attempt", `upheld the reviewer for ${taskId} in ${runId} — by ${by}`, serialization.contended);
264
273
  }
265
274
  if (recheck) {
266
- if ((!gateFailPark || !failedGate) && !infraPark && !authoringRunnerPark) {
275
+ if (park?.kind === "stall" && !reapFailurePark) {
276
+ 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`);
277
+ }
278
+ if ((!gateFailPark || !failedGate) && !infraPark && !authoringRunnerPark && !reapFailurePark) {
267
279
  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
280
  }
269
281
  journal.append("task-approved", taskId, {
@@ -276,14 +288,38 @@ export async function approve(argv, cwd = process.cwd()) {
276
288
  // carry gate so their durable release row identifies the red tool gate that made recheck admissible.
277
289
  ...(failedGate ? { failedGate } : {}),
278
290
  ...(authoringRunnerPark && failedGate ? { gate: failedGate } : {}),
291
+ // OBS-1202: the daemon re-verifies the parked attempt's owned census before any gate runs.
292
+ ...(reapFailurePark ? { reapFailure: park.reapFailure } : {}),
279
293
  ...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
280
294
  });
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);
295
+ 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
296
  }
283
297
  if (waive) {
284
298
  if (!gateFailPark || !failedGate) {
285
299
  throw new Error(`--waive applies to a gate-fail park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
286
300
  }
301
+ // C1: the waiver's owed check rides the SAME row — one append, captured synchronously so a live
302
+ // daemon's boundary never observes the decision without its obligation (or a second write).
303
+ // The seats a review discharge may resolve to are this repository's declared channels, recorded on
304
+ // the row so every later fold reads the same table whatever process or checkout it runs in.
305
+ let integrationRef = "unknown";
306
+ let declared;
307
+ let owedTask;
308
+ try {
309
+ const cfg = loadConfig(cwd);
310
+ integrationRef = integrationBranch(cfg, runId);
311
+ declared = allAdapters().flatMap((a) => a.channels(cfg)).map((c) => ({ key: channelKey(c), vendor: c.vendor }));
312
+ }
313
+ catch { /* an unreadable config binds no range and declares no seat: unknown */ }
314
+ try {
315
+ owedTask = loadGraph(cwd).tasks.find((t) => t.id === taskId);
316
+ }
317
+ catch { /* recorded as unknown criteria and scope */ }
318
+ const obligation = captureOwedCheck({
319
+ cwd, runId, taskId, gate: failedGate, events, acceptance: owedTask?.acceptance, files: owedTask?.files, declared,
320
+ cause: reason ?? `operator waived failed ${failedGate} gate`, // the row's own park field binds the park
321
+ taskRef: `${integrationRef}--${taskId}`, integrationRef,
322
+ });
287
323
  journal.append("task-approved", taskId, {
288
324
  by,
289
325
  ...(reason ? { reason } : {}),
@@ -292,8 +328,9 @@ export async function approve(argv, cwd = process.cwd()) {
292
328
  gate: failedGate,
293
329
  park: parkBinding,
294
330
  ...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
331
+ obligation,
295
332
  });
296
- return disposition(cwd, runId, "waive-gate", `waived failed gate ${failedGate} for ${taskId} in ${runId} — by ${by}`, serialization.contended);
333
+ return disposition(cwd, runId, "waive-gate", `waived failed gate ${failedGate} for ${taskId} in ${runId} — by ${by}; owes ${failedGate} check ${obligation.id}${obligation.known ? "" : " (debt unknown)"}`, serialization.contended);
297
334
  }
298
335
  if (gateFailPark) {
299
336
  const choices = [`--waive (disposition waive-gate)`, `--recheck (disposition re-dispatch)`];
@@ -34,6 +34,8 @@ export type DoctorOpts = {
34
34
  resolveOrcaBinary?: (cwd: string) => string | undefined;
35
35
  /** Test seam for the runner-owned JSON listing used by the acceptance-oracle report row. */
36
36
  listTests?: (cwd: string) => Promise<VitestListResult>;
37
+ /** Clock for model-knowledge lints (retirement notices, knowledge age); absent = the wall clock. */
38
+ now?: () => Date;
37
39
  };
38
40
  type OrcaCapability = {
39
41
  verdict: "pass" | "fail" | "warn";
@@ -173,6 +175,8 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
173
175
  tipTest?: string | undefined;
174
176
  lint?: string | undefined;
175
177
  diffCap?: number | undefined;
178
+ evidenceQuotaBytes?: number | undefined;
179
+ repairSelection?: boolean | undefined;
176
180
  byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
177
181
  [x: string]: unknown;
178
182
  acceptance?: boolean | undefined;
@@ -206,8 +210,8 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
206
210
  };
207
211
  executionPolicy?: {
208
212
  boundedInfrastructure: boolean;
209
- repairSelection: boolean;
210
213
  taskExecutionLimitMs: number;
214
+ repairSelection?: boolean | undefined;
211
215
  } | undefined;
212
216
  setup?: string | undefined;
213
217
  cost?: {
@@ -298,6 +302,8 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
298
302
  tipTest?: string | undefined;
299
303
  lint?: string | undefined;
300
304
  diffCap?: number | undefined;
305
+ evidenceQuotaBytes?: number | undefined;
306
+ repairSelection?: boolean | undefined;
301
307
  byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
302
308
  [x: string]: unknown;
303
309
  acceptance?: boolean | undefined;
@@ -331,8 +337,8 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
331
337
  };
332
338
  executionPolicy?: {
333
339
  boundedInfrastructure: boolean;
334
- repairSelection: boolean;
335
340
  taskExecutionLimitMs: number;
341
+ repairSelection?: boolean | undefined;
336
342
  } | undefined;
337
343
  setup?: string | undefined;
338
344
  cost?: {
@@ -590,6 +590,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
590
590
  // no fetch timestamp, so it still stamps now — its staleness lint is best-effort until it surfaces one.
591
591
  if (health[a.id].models.length)
592
592
  health[a.id].modelsDetectedAt = a.listModelsFetchedAt?.() ?? new Date().toISOString();
593
+ // v2.6.4 T2 (B1a): the CLI's retirement notices ride into doctor.json beside the list they describe;
594
+ // an unreadable source leaves the field absent, which every reader treats as unknown.
595
+ const retirements = a.listModelsRetirements?.();
596
+ if (retirements)
597
+ health[a.id].modelRetirements = retirements;
593
598
  }
594
599
  catch { /* fail open: leave models as-is, doctor stays healthy */ }
595
600
  }
@@ -784,7 +789,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
784
789
  const liveBenchStale = liveBenchStalenessFinding(opts.catalogNow?.() ?? new Date());
785
790
  if (liveBenchStale)
786
791
  lintRows.push(attentionRow(liveBenchStale));
787
- lintRows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map(attentionRow));
792
+ lintRows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd), now: opts.now?.() }).map(attentionRow));
788
793
  const excluded = excludedChannels(cfg, adapters, health);
789
794
  if (excluded.length)
790
795
  lintRows.push(attentionRow(exclusionLine(excluded)));