tickmarkr 2.5.4 → 2.5.6

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 (78) hide show
  1. package/README.md +3 -1
  2. package/dist/adapters/registry.js +6 -1
  3. package/dist/adapters/types.d.ts +3 -0
  4. package/dist/cli/commands/approve.d.ts +1 -0
  5. package/dist/cli/commands/approve.js +118 -9
  6. package/dist/cli/commands/doctor.d.ts +10 -0
  7. package/dist/cli/commands/doctor.js +44 -29
  8. package/dist/cli/commands/fleet.js +199 -33
  9. package/dist/cli/commands/init.js +196 -6
  10. package/dist/cli/commands/plan.js +20 -4
  11. package/dist/cli/commands/resume.js +4 -2
  12. package/dist/cli/commands/run.js +11 -2
  13. package/dist/cli/commands/verify.js +102 -81
  14. package/dist/cli/help.d.ts +2 -0
  15. package/dist/cli/help.js +3 -1
  16. package/dist/config/config.d.ts +25 -8
  17. package/dist/config/config.js +41 -29
  18. package/dist/config/fleet-overlay.d.ts +3 -9
  19. package/dist/config/fleet-overlay.js +114 -19
  20. package/dist/config/fleet-why.d.ts +7 -0
  21. package/dist/config/fleet-why.js +5 -0
  22. package/dist/drivers/index.d.ts +15 -1
  23. package/dist/drivers/index.js +39 -10
  24. package/dist/drivers/orca.d.ts +119 -10
  25. package/dist/drivers/orca.js +781 -110
  26. package/dist/gates/baseline.d.ts +17 -3
  27. package/dist/gates/baseline.js +63 -15
  28. package/dist/gates/cache.d.ts +101 -0
  29. package/dist/gates/cache.js +401 -0
  30. package/dist/gates/llm.d.ts +3 -0
  31. package/dist/gates/llm.js +11 -0
  32. package/dist/gates/review.d.ts +28 -3
  33. package/dist/gates/review.js +106 -14
  34. package/dist/gates/run-gates.d.ts +9 -1
  35. package/dist/gates/run-gates.js +407 -102
  36. package/dist/gates/test-manifest.d.ts +128 -0
  37. package/dist/gates/test-manifest.js +463 -0
  38. package/dist/gates/test-reporter.d.ts +4 -0
  39. package/dist/gates/test-reporter.js +57 -0
  40. package/dist/graph/graph.d.ts +2 -0
  41. package/dist/graph/graph.js +45 -1
  42. package/dist/route/preference.d.ts +22 -1
  43. package/dist/route/preference.js +123 -25
  44. package/dist/route/router.js +31 -6
  45. package/dist/run/consult.js +5 -4
  46. package/dist/run/daemon.d.ts +15 -0
  47. package/dist/run/daemon.js +638 -167
  48. package/dist/run/execution-budget.d.ts +25 -0
  49. package/dist/run/execution-budget.js +142 -0
  50. package/dist/run/git.d.ts +50 -1
  51. package/dist/run/git.js +131 -12
  52. package/dist/run/journal.d.ts +16 -2
  53. package/dist/run/journal.js +115 -25
  54. package/dist/run/lease.d.ts +58 -0
  55. package/dist/run/lease.js +310 -0
  56. package/dist/run/merge.d.ts +2 -0
  57. package/dist/run/merge.js +91 -3
  58. package/dist/run/operator-state.d.ts +11 -0
  59. package/dist/run/operator-state.js +17 -3
  60. package/dist/run/recovery.d.ts +8 -0
  61. package/dist/run/recovery.js +25 -0
  62. package/dist/run/repair-selection.d.ts +12 -0
  63. package/dist/run/repair-selection.js +56 -0
  64. package/dist/run/stall.d.ts +6 -1
  65. package/dist/run/stall.js +60 -3
  66. package/dist/tui/cockpit/board.d.ts +96 -0
  67. package/dist/tui/cockpit/board.js +346 -0
  68. package/dist/tui/cockpit/decision-actions.js +2 -0
  69. package/dist/tui/cockpit/layout.d.ts +5 -1
  70. package/dist/tui/cockpit/layout.js +8 -3
  71. package/dist/tui/cockpit/live-runtime.js +71 -21
  72. package/dist/tui/cockpit/run-view.d.ts +7 -5
  73. package/dist/tui/cockpit/run-view.js +12 -11
  74. package/dist/tui/ink/fleet-app.d.ts +49 -27
  75. package/dist/tui/ink/fleet-app.js +229 -38
  76. package/package.json +2 -2
  77. package/skills/tickmarkr-overseer/SKILL.md +173 -113
  78. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { allAdapters, discoverChannels, probeAll, readDoctor } from "../../adapters/registry.js";
3
3
  import { ROUTING_MODES } from "../../config/config.js";
4
- import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
4
+ import { classifyHost, parseDriverOverride, pickDriver, preflightHostDriver } from "../../drivers/index.js";
5
5
  import { loadGraph, onDiskSpecHash, readCompileRefusal } from "../../graph/graph.js";
6
6
  import { formatSummary, resolveRunMode, runDaemon } from "../../run/daemon.js";
7
7
  import { isRunLockLive } from "../../run/lock.js";
@@ -106,6 +106,7 @@ export const RAIL_ROWS = {
106
106
  "gate-fingerprint-cap": { label: "repeat capped", tone: "attention" },
107
107
  // a scope red every collateral prediction already named: the daemon parks the task WITHOUT
108
108
  // charging an attempt, which is the most consequential unchargeable decision it makes
109
+ "scope-request": { label: "scope requested", tone: "attention" },
109
110
  "scope-authoring": { label: "authoring defect", tone: "attention" },
110
111
  "session-reset": { label: "session reset", tone: "attention" },
111
112
  "worker-mode-fallback": { label: "dispatch mode", tone: "attention" },
@@ -131,6 +132,9 @@ export const RAIL_ROWS = {
131
132
  "phase-start": { label: "gate start", tone: "active" },
132
133
  "gate-result": { label: "gate", tone: "pass" },
133
134
  "baseline-wait": { label: "baseline wait", tone: "active" },
135
+ "suite-wait": { label: "suite wait", tone: "active" },
136
+ "suite-wait-ceiling": { label: "suite wait ceiling", tone: "attention" },
137
+ "worker-reaped-before-harvest": { label: "worker reaped", tone: "attention" },
134
138
  "suite-budget": { label: "suite budget", tone: "attention" },
135
139
  "gate-replayed": { label: "gate replayed", tone: "attention" },
136
140
  "gate-reused": { label: "gate reused", tone: "neutral" },
@@ -320,6 +324,7 @@ const RAIL_PROJECTION = {
320
324
  ? `to ${d.chosen}${typeof d.static === "string" ? ` over ${d.static}` : ""}`
321
325
  : undefined,
322
326
  // both rehash hashes: the ladder reaches `from` (null on an unbound journal) and never `to`
327
+ "scope-request": (d) => Array.isArray(d.paths) ? `files[]: ${d.paths.join(", ")}` : undefined,
323
328
  "graph-rehash": (d) => (typeof d.to === "string" ? `to ${d.to.slice(0, 12)}` : undefined),
324
329
  "repair-dispatch": (d) => typeof d.diffBytes === "number" ? `diff ${d.diffBytes}B${d.capped === true ? ", capped" : ""}` : undefined,
325
330
  "tip-moved": (d) => typeof d.gatedCommit === "string" && typeof d.branchTip === "string"
@@ -328,6 +333,8 @@ const RAIL_PROJECTION = {
328
333
  "trust-auto-answer": (d) => typeof d.adapter === "string" ? `${d.adapter}${typeof d.phase === "string" ? ` ${d.phase}` : ""}` : undefined,
329
334
  // SB-1: the census the ceiling released beside, and the budget the round ran under versus the one it
330
335
  // would have had on an empty census — none of `{count, occupancyCap, conservativeCap}` is on the ladder
336
+ "suite-wait": (d) => typeof d.count === "number" ? `waiting for ${d.count} command(s)` : undefined,
337
+ "suite-wait-ceiling": (d) => typeof d.count === "number" ? `${d.count} still live after ${d.waitedMs}ms` : undefined,
331
338
  "suite-budget": (d) => typeof d.count === "number" && typeof d.conservativeCap === "number" && typeof d.occupancyCap === "number"
332
339
  ? `beside ${d.count}, cap ${d.conservativeCap} not ${d.occupancyCap}`
333
340
  : undefined,
@@ -503,6 +510,8 @@ export async function run(argv, cwd = process.cwd()) {
503
510
  throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
504
511
  }
505
512
  await assertRefsWritable(cwd, "run");
513
+ const host = classifyHost();
514
+ preflightHostDriver(cfg, driverOverride, host);
506
515
  // The run id is minted HERE rather than inside the daemon, because the narration sink has to know
507
516
  // which run it is narrating before the first event arrives (the daemon's `narrate` callback is
508
517
  // handed an event and nothing else, and `run-start` carries no run id). `runDaemon` uses the id
@@ -512,7 +521,7 @@ export async function run(argv, cwd = process.cwd()) {
512
521
  const s = await runDaemon(cwd, {
513
522
  runId,
514
523
  concurrency: values.concurrency ? Number(values.concurrency) : undefined,
515
- driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
524
+ driver: bindNarration(pickDriver(cfg, driverOverride, host), narrate),
516
525
  mode: flagMode,
517
526
  supersedes: values.supersedes,
518
527
  narrate,
@@ -6,13 +6,15 @@ import { parseArgs } from "node:util";
6
6
  import { allAdapters, probeAll, readDoctor, rolePools } from "../../adapters/registry.js";
7
7
  import { channelKey } from "../../adapters/types.js";
8
8
  import { loadConfig } from "../../config/config.js";
9
- import { captureBaseline, detectGateCommands } from "../../gates/baseline.js";
9
+ import { captureBaseline, detectGateCommands, staleFileCountCommands } from "../../gates/baseline.js";
10
10
  import { modelProvider } from "../../gates/review.js";
11
11
  import { runGates } from "../../gates/run-gates.js";
12
12
  import { getTask, loadGraph } from "../../graph/graph.js";
13
13
  import { GATE_NAMES } from "../../graph/schema.js";
14
+ import { executionSignal } from "../../run/execution-budget.js";
14
15
  import { linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.js";
15
16
  import { Journal } from "../../run/journal.js";
17
+ import { withRepositoryLease } from "../../run/lease.js";
16
18
  /**
17
19
  * tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
18
20
  *
@@ -249,95 +251,114 @@ export async function verify(argv, cwd = process.cwd()) {
249
251
  // ponytail: tmpdir means the baseline cache dies on reboot/cleanup — worst case is one re-capture.
250
252
  const stateDir = verifyStateDir(cwd);
251
253
  mkdirSync(stateDir, { recursive: true });
252
- let baseline;
253
- const cachePath = baselineCachePath(cwd, mergeBase, commands);
254
- const verdictlessMarker = `${cachePath}.verdictless`;
255
- mkdirSync(dirname(cachePath), { recursive: true });
256
- if (values.baseline) {
257
- baseline = JSON.parse(readFileSync(values.baseline, "utf8"));
258
- }
259
- else {
260
- let cached;
261
- if (existsSync(cachePath)) {
262
- cached = JSON.parse(readFileSync(cachePath, "utf8"));
263
- const missing = verdictlessCommands(cached, commands);
264
- if (missing.length) {
265
- console.error(`verify: cached baseline recorded no verdict for ${missing.join(", ")}; it was not reusable and will be recaptured`);
266
- rmSync(cachePath, { force: true });
267
- cached = undefined;
268
- }
269
- }
270
- if (cached) {
271
- console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${cachePath})`);
272
- baseline = cached;
254
+ const recordJournal = values.record ? Journal.open(stateRoot, values.record) : undefined;
255
+ // OBS-1042: one runner root per repository across every linked worktree — the capture and the
256
+ // battery below run under a reservation keyed on the common git dir, and a waiter says who holds it.
257
+ return withRepositoryLease(cwd, () => leasedVerify(), {
258
+ signal: executionSignal(),
259
+ onWait: (holder) => {
260
+ console.error(`verify: waiting for the repository's runner lease held by pid ${holder.pid} in ${holder.cwd}`);
261
+ recordJournal?.append("suite-wait", values.task ?? "VERIFY", { holderPid: holder.pid, holderCwd: holder.cwd });
262
+ },
263
+ });
264
+ async function leasedVerify() {
265
+ let baseline;
266
+ const cachePath = baselineCachePath(cwd, mergeBase, commands);
267
+ const verdictlessMarker = `${cachePath}.verdictless`;
268
+ mkdirSync(dirname(cachePath), { recursive: true });
269
+ if (values.baseline) {
270
+ baseline = JSON.parse(readFileSync(values.baseline, "utf8"));
273
271
  }
274
272
  else {
275
- if (existsSync(verdictlessMarker)) {
276
- console.error(`verify: prior baseline recorded no verdict and was not cached; recapturing merge-base ${mergeBase.slice(0, 12)}`);
277
- }
278
- else {
279
- console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} (cached by base, lockfile and command hashes at ${cachePath})`);
280
- }
281
- const baseDir = join(stateDir, `base-${mergeBase.slice(0, 12)}`);
282
- await shGit(`git worktree remove --force '${baseDir}'`, cwd); // stale leftover from an interrupted run
283
- await shGitOk(`git worktree add --detach '${baseDir}' '${mergeBase}'`, cwd);
284
- try {
285
- linkNodeModules(cwd, baseDir, { force: true });
286
- baseline = await captureBaseline(baseDir, commands);
287
- const missing = verdictlessCommands(baseline, commands);
273
+ let cached;
274
+ if (existsSync(cachePath)) {
275
+ cached = JSON.parse(readFileSync(cachePath, "utf8"));
276
+ const missing = verdictlessCommands(cached, commands);
288
277
  if (missing.length) {
289
- writeFileSync(verdictlessMarker, JSON.stringify({ base: mergeBase, commands: missing }) + "\n");
278
+ console.error(`verify: cached baseline recorded no verdict for ${missing.join(", ")}; it was not reusable and will be recaptured`);
290
279
  rmSync(cachePath, { force: true });
280
+ cached = undefined;
291
281
  }
292
- else {
293
- writeFileSync(cachePath, JSON.stringify(baseline, null, 2));
294
- rmSync(verdictlessMarker, { force: true });
282
+ // OBS-1044: a cached vitest count that is a stdout sum may be inflated by nested runner echo;
283
+ // applying it would manufacture a deficit. Only a manifest-derived count is a floor.
284
+ const stale = cached ? staleFileCountCommands(cached, commands, cwd) : [];
285
+ if (stale.length) {
286
+ console.error(`verify: cached baseline's file count for ${stale.join(", ")} is not manifest-derived; it will be recaptured`);
287
+ rmSync(cachePath, { force: true });
288
+ cached = undefined;
295
289
  }
296
290
  }
297
- finally {
298
- await removeWorktree(cwd, baseDir);
291
+ if (cached) {
292
+ console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${cachePath})`);
293
+ baseline = cached;
294
+ }
295
+ else {
296
+ if (existsSync(verdictlessMarker)) {
297
+ console.error(`verify: prior baseline recorded no verdict and was not cached; recapturing merge-base ${mergeBase.slice(0, 12)}`);
298
+ }
299
+ else {
300
+ console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} (cached by base, lockfile and command hashes at ${cachePath})`);
301
+ }
302
+ const baseDir = join(stateDir, `base-${mergeBase.slice(0, 12)}`);
303
+ await shGit(`git worktree remove --force '${baseDir}'`, cwd); // stale leftover from an interrupted run
304
+ await shGitOk(`git worktree add --detach '${baseDir}' '${mergeBase}'`, cwd);
305
+ try {
306
+ linkNodeModules(cwd, baseDir, { force: true });
307
+ baseline = await captureBaseline(baseDir, commands);
308
+ const missing = verdictlessCommands(baseline, commands);
309
+ if (missing.length) {
310
+ writeFileSync(verdictlessMarker, JSON.stringify({ base: mergeBase, commands: missing }) + "\n");
311
+ rmSync(cachePath, { force: true });
312
+ }
313
+ else {
314
+ writeFileSync(cachePath, JSON.stringify(baseline, null, 2));
315
+ rmSync(verdictlessMarker, { force: true });
316
+ }
317
+ }
318
+ finally {
319
+ await removeWorktree(cwd, baseDir);
320
+ }
299
321
  }
300
322
  }
301
- }
302
- const recordJournal = values.record ? Journal.open(stateRoot, values.record) : undefined;
303
- const artifactDir = join(stateDir, new Date().toISOString().replace(/[:.]/g, "-"));
304
- mkdirSync(artifactDir, { recursive: true });
305
- const { results } = await runGates(task, {
306
- worktree: cwd, baseRef: mergeBase,
307
- result: { ok: true, summary: "standalone verify — no worker claims to trust", deviations: [], raw: "" },
308
- author, commands, baseline, channels, ...(judgeChannels ? { judgeChannels } : {}), adapters, cfg,
309
- pipeline: "v185", artifactDir,
310
- onGate: (e) => {
311
- if (e.phase === "start")
312
- console.error(`verify: → ${e.gate} (${e.index}/${e.total})`);
313
- else if (e.phase === "note")
314
- console.error(`verify: note ${e.gate} ${e.name} ${JSON.stringify(e.payload)}`);
315
- else
316
- console.error(`verify: ${e.result.pass ? "✓" : "✗"} ${e.result.gate} — ${e.result.details.split("\n")[0] ?? ""}`);
317
- },
318
- });
319
- const green = results.length > 0 && results.every((r) => r.pass || r.meta?.skipped === true);
320
- const reviewRows = results.filter((result) => result.gate === "review");
321
- const reviewFindings = reviewRows.flatMap((result) => result.details.split("\n").flatMap((line) => {
322
- const match = /^- \[([^\]]+)\] (.*)$/.exec(line);
323
- return match ? [{ classification: match[1], note: match[2], reviewer: result.meta?.reviewer }] : [];
324
- }));
325
- const artifactPath = join(artifactDir, "verify-results.json");
326
- writeFileSync(artifactPath, JSON.stringify({ base: baseTip, head, mergeBase, green, gateRows: results, reviewFindings }, null, 2) + "\n");
327
- console.error(`verify: artifacts written to ${artifactPath}`);
328
- const review = reviewRows.at(-1);
329
- if (recordJournal && review) {
330
- recordJournal.append("review-leg2", values.task ?? "VERIFY", {
331
- base: baseTip, head, mergeBase, author: channelKey(author), artifactPath,
332
- ...review,
323
+ const artifactDir = join(stateDir, new Date().toISOString().replace(/[:.]/g, "-"));
324
+ mkdirSync(artifactDir, { recursive: true });
325
+ const { results } = await runGates(task, {
326
+ worktree: cwd, baseRef: mergeBase,
327
+ result: { ok: true, summary: "standalone verify — no worker claims to trust", deviations: [], raw: "" },
328
+ author, commands, baseline, channels, ...(judgeChannels ? { judgeChannels } : {}), adapters, cfg,
329
+ verificationScope: "standalone", artifactDir, stateDir: join(stateRoot, ".tickmarkr"),
330
+ onGate: (e) => {
331
+ if (e.phase === "start")
332
+ console.error(`verify: → ${e.gate} (${e.index}/${e.total})`);
333
+ else if (e.phase === "note")
334
+ console.error(`verify: note ${e.gate} ${e.name} ${JSON.stringify(e.payload)}`);
335
+ else
336
+ console.error(`verify: ${e.result.pass ? "✓" : "✗"} ${e.result.gate} — ${e.result.details.split("\n")[0] ?? ""}`);
337
+ },
333
338
  });
339
+ const green = results.length > 0 && results.every((r) => r.pass || r.meta?.skipped === true);
340
+ const reviewRows = results.filter((result) => result.gate === "review");
341
+ const reviewFindings = reviewRows.flatMap((result) => result.details.split("\n").flatMap((line) => {
342
+ const match = /^- \[([^\]]+)\] (.*)$/.exec(line);
343
+ return match ? [{ classification: match[1], note: match[2], reviewer: result.meta?.reviewer }] : [];
344
+ }));
345
+ const artifactPath = join(artifactDir, "verify-results.json");
346
+ writeFileSync(artifactPath, JSON.stringify({ base: baseTip, head, mergeBase, green, gateRows: results, reviewFindings }, null, 2) + "\n");
347
+ console.error(`verify: artifacts written to ${artifactPath}`);
348
+ const review = reviewRows.at(-1);
349
+ if (recordJournal && review) {
350
+ recordJournal.append("review-leg2", values.task ?? "VERIFY", {
351
+ base: baseTip, head, mergeBase, author: channelKey(author), artifactPath,
352
+ ...review,
353
+ });
354
+ }
355
+ if (values.json) {
356
+ return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, results }, null, 2), code: green ? 0 : 2 };
357
+ }
358
+ const lines = results.map((r) => `${r.pass ? "PASS" : "FAIL"} ${r.gate}\n${r.details.split("\n").map((l) => ` ${l}`).join("\n")}`);
359
+ const verdict = green
360
+ ? `verify GREEN — ${results.length} gate(s) passed on ${mergeBase.slice(0, 12)}..${head.slice(0, 12)} (merge is a human decision; artifacts: ${artifactPath})`
361
+ : `verify RED — first failure decides; artifacts: ${artifactPath}`;
362
+ return { out: [...lines, "", verdict].join("\n"), code: green ? 0 : 2 };
334
363
  }
335
- if (values.json) {
336
- return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, results }, null, 2), code: green ? 0 : 2 };
337
- }
338
- const lines = results.map((r) => `${r.pass ? "PASS" : "FAIL"} ${r.gate}\n${r.details.split("\n").map((l) => ` ${l}`).join("\n")}`);
339
- const verdict = green
340
- ? `verify GREEN — ${results.length} gate(s) passed on ${mergeBase.slice(0, 12)}..${head.slice(0, 12)} (merge is a human decision; artifacts: ${artifactPath})`
341
- : `verify RED — first failure decides; artifacts: ${artifactPath}`;
342
- return { out: [...lines, "", verdict].join("\n"), code: green ? 0 : 2 };
343
364
  }
@@ -15,6 +15,7 @@ export declare const COMMAND_HELP: {
15
15
  "--docs": string;
16
16
  "--fresh": string;
17
17
  "--yes": string;
18
+ "--remove": string;
18
19
  };
19
20
  examples: string[];
20
21
  };
@@ -160,6 +161,7 @@ export declare const COMMAND_HELP: {
160
161
  usage: string;
161
162
  description: string;
162
163
  options: {
164
+ "--files <glob,\u2026>": string;
163
165
  "--by <name>": string;
164
166
  "--reason <text>": string;
165
167
  "--waive": string;
package/dist/cli/help.js CHANGED
@@ -10,7 +10,8 @@ export const COMMAND_HELP = {
10
10
  "--docs": "Include agent guidance documents with --agent.",
11
11
  "--fresh": "Force fresh adapter probes instead of reusing recent health.",
12
12
  "--yes": "Skip the interactive setup wizard and use defaults.",
13
- }, examples: ["init --yes", "init --agent --force --docs"],
13
+ "--remove": "Reverse init without scaffolding: delete state, skills, docs block, unedited spec, and prune worktrees.",
14
+ }, examples: ["init --yes", "init --agent --force --docs", "init --remove"],
14
15
  },
15
16
  doctor: {
16
17
  usage: "doctor [options]", description: "Probe configured adapters and models and print diagnostics. Default probing may make model calls and refresh a stale catalog.",
@@ -128,6 +129,7 @@ export const COMMAND_HELP = {
128
129
  approve: {
129
130
  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.",
130
131
  options: {
132
+ "--files <glob,…>": "Extend files[] for a scope-request park with comma-separated repository-relative globs; required for scope approval.",
131
133
  "--by <name>": "Name the actor (default: current OS user).",
132
134
  "--reason <text>": "Record the decision reason.",
133
135
  "--waive": "Waive only the identified failed gate.",
@@ -132,7 +132,17 @@ export declare function effectiveReviewPolicy(files: ReadonlyArray<string>, revi
132
132
  * under-flagged one skips the only gate that exists for it.
133
133
  */
134
134
  export declare function criticalPathHits(files: ReadonlyArray<string>, criticalPaths?: ReadonlyArray<string>): string[];
135
+ export declare const ExecutionPolicySchema: z.ZodObject<{
136
+ boundedInfrastructure: z.ZodDefault<z.ZodBoolean>;
137
+ repairSelection: z.ZodDefault<z.ZodBoolean>;
138
+ taskExecutionLimitMs: z.ZodNumber;
139
+ }, z.core.$strict>;
135
140
  export declare const TickmarkrConfigSchema: z.ZodObject<{
141
+ executionPolicy: z.ZodOptional<z.ZodObject<{
142
+ boundedInfrastructure: z.ZodDefault<z.ZodBoolean>;
143
+ repairSelection: z.ZodDefault<z.ZodBoolean>;
144
+ taskExecutionLimitMs: z.ZodNumber;
145
+ }, z.core.$strict>>;
136
146
  concurrency: z.ZodNumber;
137
147
  driver: z.ZodEnum<{
138
148
  auto: "auto";
@@ -364,24 +374,31 @@ export type FleetTierAssignment = {
364
374
  export type FleetEditable = {
365
375
  denyAdapters: string[];
366
376
  denyModels: string[];
377
+ denyWorkersAdapters?: string[];
378
+ denyWorkersModels?: string[];
379
+ allowOut?: string[];
367
380
  tiers: Record<string, Record<string, FleetTierAssignment | null>>;
368
381
  map: Record<string, MapEntry>;
369
382
  floors: Record<string, Tier>;
370
383
  };
371
384
  export declare function repoOverlayPath(repoRoot: string): string;
372
385
  export declare function readOverlayFile(path: string): Record<string, unknown>;
386
+ /** A discovered fleet universe row: the served models of one adapter and, per model, the resolved
387
+ * identity doctor recorded for a floating alias (LEG2-T3 round 2 finding 3). */
388
+ export type FleetUniverseRow = {
389
+ adapter: string;
390
+ models: string[];
391
+ identities?: Record<string, string>;
392
+ };
393
+ /** Does a deny/allow entry name this universe channel? Grammar A1 (adapter id, bare model id,
394
+ * adapter:model) plus the recorded identity (bare or adapter:identity) — the collector's authority. */
395
+ export declare function universeEntryMatches(row: FleetUniverseRow, model: string, entry: string): boolean;
373
396
  /** Grammar A1 membership test: does any discovered universe row cover this deny/allow entry
374
397
  * (adapter id, bare model id, or adapter:model)? Uncovered entries name channels the probe
375
398
  * did not serve this session (failed auth, rate limit, retired sku) — they are NOT editable
376
399
  * membership state and must survive a membership write verbatim, never be re-derived away. */
377
- export declare function universeCovers(universe: {
378
- adapter: string;
379
- models: string[];
380
- }[], entry: string): boolean;
381
- export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?: {
382
- adapter: string;
383
- models: string[];
384
- }[]): FleetEditable;
400
+ export declare function universeCovers(universe: FleetUniverseRow[], entry: string): boolean;
401
+ export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?: FleetUniverseRow[]): FleetEditable;
385
402
  /** Resolve which layer last set a dotted fleet path (defaults < global < repo). */
386
403
  export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
387
404
  globalDir?: string;
@@ -264,7 +264,14 @@ const ShapeGateParticipationSchema = z
264
264
  });
265
265
  }
266
266
  });
267
+ export const ExecutionPolicySchema = z.object({
268
+ boundedInfrastructure: z.boolean().default(false),
269
+ repairSelection: z.boolean().default(false),
270
+ taskExecutionLimitMs: z.number().int().positive(),
271
+ }).strict();
267
272
  export const TickmarkrConfigSchema = z.object({
273
+ // Opt-in recovery experiment. The original run snapshot owns this across resumes.
274
+ executionPolicy: ExecutionPolicySchema.optional(),
268
275
  concurrency: z.number().int().positive(),
269
276
  driver: z.enum(["auto", "herdr", "subprocess", "orca"]),
270
277
  integrationBranchPrefix: z
@@ -887,6 +894,14 @@ export function readOverlayFile(path) {
887
894
  throw new ConfigError(`invalid overlay at ${path}`);
888
895
  return raw;
889
896
  }
897
+ /** Does a deny/allow entry name this universe channel? Grammar A1 (adapter id, bare model id,
898
+ * adapter:model) plus the recorded identity (bare or adapter:identity) — the collector's authority. */
899
+ export function universeEntryMatches(row, model, entry) {
900
+ if (entry === row.adapter || entry === model || entry === `${row.adapter}:${model}`)
901
+ return true;
902
+ const identity = row.identities?.[model];
903
+ return identity !== undefined && (entry === identity || entry === `${row.adapter}:${identity}`);
904
+ }
890
905
  /** Grammar A1 membership test: does any discovered universe row cover this deny/allow entry
891
906
  * (adapter id, bare model id, or adapter:model)? Uncovered entries name channels the probe
892
907
  * did not serve this session (failed auth, rate limit, retired sku) — they are NOT editable
@@ -894,10 +909,13 @@ export function readOverlayFile(path) {
894
909
  export function universeCovers(universe, entry) {
895
910
  return universe.some((row) => entry === row.adapter || row.models.some((m) => entry === m || entry === `${row.adapter}:${m}`));
896
911
  }
897
- // v1.92 fleet membership: with a discovered `universe` (classified models only) the deny sets become
898
- // the EFFECTIVE out-of-fleet complement — anything routing.allow does not admit (fail-closed) UNION
899
- // routing.deny adapters/models scopes (never workers). Absent universe ⇒ deny arrays verbatim, so
900
- // non-UI callers stay byte-identical.
912
+ // v1.92 fleet membership: with a discovered `universe` (classified models only) the routing.allow
913
+ // complement (fail-closed) rides as `allowOut` — adapter ids and adapter:model keys — BESIDE the
914
+ // authored routing.deny adapters/models entries, which ride VERBATIM in their own lists (never
915
+ // workers). LEG2-T3 round 2 finding 2: authored entries are never collapsed into a membership key;
916
+ // OBS-1046: nor is an allow exclusion folded into a deny list — the two reasons stay distinct so
917
+ // one Space press clears one; finding 3: allow admission honours recorded identity.
918
+ // Absent universe ⇒ deny arrays verbatim and no allowOut, so non-UI callers stay byte-identical.
901
919
  export function fleetEditableFromConfig(cfg, universe) {
902
920
  const tiers = {};
903
921
  for (const [adapter, entry] of Object.entries(cfg.tiers)) {
@@ -906,46 +924,36 @@ export function fleetEditableFromConfig(cfg, universe) {
906
924
  tiers[adapter][model] = { tier };
907
925
  }
908
926
  }
909
- let denyAdapters = [...(cfg.routing.deny?.adapters ?? [])].sort();
910
- let denyModels = [...(cfg.routing.deny?.models ?? [])].sort();
927
+ const denyAdapters = [...(cfg.routing.deny?.adapters ?? [])].sort();
928
+ const denyModels = [...(cfg.routing.deny?.models ?? [])].sort();
929
+ let allowOut;
911
930
  if (universe !== undefined) {
912
- const { allow, deny } = cfg.routing;
913
- // Grammar A1 (disallowedBy precedent): entries match adapter id, bare model id, or adapter:model.
914
- // Inlined rather than imported — route/preference.ts imports this module.
915
- const denyEntries = [...(deny?.adapters ?? []), ...(deny?.models ?? [])];
931
+ const { allow } = cfg.routing;
916
932
  const allowEntries = allow ? [...(allow.adapters ?? []), ...(allow.models ?? [])] : null;
917
- const outOfFleet = (adapter, model) => {
918
- const matches = (e) => e === adapter || e === model || e === `${adapter}:${model}`;
919
- if (denyEntries.some(matches))
920
- return true;
921
- return allowEntries !== null && !allowEntries.some(matches);
922
- };
933
+ const notAdmitted = (row, model) => allowEntries !== null && !allowEntries.some((entry) => universeEntryMatches(row, model, entry));
923
934
  const adaptersOut = [];
924
935
  const modelsOut = [];
925
936
  for (const row of universe) {
926
- const out = row.models.filter((m) => outOfFleet(row.adapter, m));
927
- if (row.models.length ? out.length === row.models.length : outOfFleet(row.adapter, "")) {
937
+ const out = row.models.filter((m) => notAdmitted(row, m));
938
+ if (row.models.length ? out.length === row.models.length : notAdmitted(row, "")) {
928
939
  adaptersOut.push(row.adapter);
929
940
  }
930
941
  else {
931
942
  modelsOut.push(...out.map((m) => `${row.adapter}:${m}`));
932
943
  }
933
944
  }
934
- // OBS-517: deny entries the probe universe does not cover (failed/rate-limited probes drop the
935
- // channel from discoverChannels) are preserved VERBATIM — deriving membership purely from the
936
- // universe silently un-denied them on the next write (fail-open on the operator's own ban list).
937
- denyAdapters = [...new Set([
938
- ...adaptersOut,
939
- ...(deny?.adapters ?? []).filter((e) => !universeCovers(universe, e)),
940
- ])].sort();
941
- denyModels = [...new Set([
942
- ...modelsOut,
943
- ...(deny?.models ?? []).filter((e) => !universeCovers(universe, e)),
944
- ])].sort();
945
+ // OBS-517: authored deny entries — covered by the probe universe or not — ride verbatim (above),
946
+ // so a transient probe failure can never un-deny one on the next write (fail-open on the ban list).
947
+ allowOut = [...new Set([...adaptersOut, ...modelsOut])].sort();
945
948
  }
946
949
  return {
947
950
  denyAdapters,
948
951
  denyModels,
952
+ ...(allowOut !== undefined ? { allowOut } : {}),
953
+ // OBS-994: workers-only deny is never folded into the all-seats complement above — it is
954
+ // returned verbatim beside the flat scopes so the fleet browser can show and toggle it.
955
+ denyWorkersAdapters: [...(cfg.routing.deny?.workers?.adapters ?? [])].sort(),
956
+ denyWorkersModels: [...(cfg.routing.deny?.workers?.models ?? [])].sort(),
949
957
  tiers,
950
958
  map: structuredClone(cfg.routing.map),
951
959
  floors: { ...cfg.routing.floors },
@@ -988,6 +996,10 @@ export function formatFleetPrint(repoRoot, opts = {}) {
988
996
  annotate(["routing", "deny", "adapters"]);
989
997
  if (effective.routing.deny?.models !== undefined)
990
998
  annotate(["routing", "deny", "models"]);
999
+ if (effective.routing.deny?.workers?.adapters !== undefined)
1000
+ annotate(["routing", "deny", "workers", "adapters"]);
1001
+ if (effective.routing.deny?.workers?.models !== undefined)
1002
+ annotate(["routing", "deny", "workers", "models"]);
991
1003
  for (const shape of Object.keys(effective.routing.map))
992
1004
  annotate(["routing", "map", shape]);
993
1005
  annotate(["routing", "floors"]);
@@ -1,4 +1,4 @@
1
- import { type FleetEditable, type RoutingMode } from "./config.js";
1
+ import { type FleetEditable, type FleetUniverseRow, type RoutingMode } from "./config.js";
2
2
  /** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
3
3
  export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
4
4
  export type FleetOverlayWrite = {
@@ -19,10 +19,7 @@ export type FleetOverlayWrite = {
19
19
  consult?: string[];
20
20
  };
21
21
  };
22
- universe?: {
23
- adapter: string;
24
- models: string[];
25
- }[];
22
+ universe?: FleetUniverseRow[];
26
23
  };
27
24
  export type FleetFirstTouch = {
28
25
  vendor: string;
@@ -33,10 +30,7 @@ export declare function fleetFirstTouchProvenance(note: string, firstTouch: Flee
33
30
  * keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
34
31
  export declare function renderFleetOverlayWrite(priorBytes: string, write: FleetOverlayWrite): string;
35
32
  /** Build the repo overlay fragment fleet would write for edits since session start. */
36
- export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>, universe?: {
37
- adapter: string;
38
- models: string[];
39
- }[]): Record<string, unknown>;
33
+ export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>, universe?: FleetUniverseRow[]): Record<string, unknown>;
40
34
  export declare function repoOverlayYaml(overlay: Record<string, unknown>): string;
41
35
  export declare function serializeFleetOverlay(overlay: Record<string, unknown>): string;
42
36
  export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;