tickmarkr 2.4.2 → 2.5.1

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 (89) hide show
  1. package/README.md +110 -24
  2. package/dist/adapters/fake.d.ts +1 -0
  3. package/dist/adapters/fake.js +2 -1
  4. package/dist/adapters/opencode.d.ts +1 -0
  5. package/dist/adapters/opencode.js +4 -1
  6. package/dist/adapters/types.d.ts +14 -0
  7. package/dist/adapters/types.js +25 -0
  8. package/dist/cli/commands/approve.d.ts +42 -0
  9. package/dist/cli/commands/approve.js +80 -13
  10. package/dist/cli/commands/doctor.d.ts +234 -0
  11. package/dist/cli/commands/doctor.js +139 -5
  12. package/dist/cli/commands/init.d.ts +2 -0
  13. package/dist/cli/commands/init.js +45 -5
  14. package/dist/cli/commands/plan.js +58 -6
  15. package/dist/cli/commands/report.js +18 -45
  16. package/dist/cli/commands/run.js +3 -0
  17. package/dist/cli/commands/scope.js +36 -6
  18. package/dist/cli/commands/stats.d.ts +2 -0
  19. package/dist/cli/commands/stats.js +42 -23
  20. package/dist/cli/commands/status.js +20 -4
  21. package/dist/cli/commands/ui.js +42 -53
  22. package/dist/cli/commands/unlock.d.ts +1 -1
  23. package/dist/cli/commands/unlock.js +59 -9
  24. package/dist/cli/help.d.ts +219 -0
  25. package/dist/cli/help.js +212 -0
  26. package/dist/cli/index.d.ts +42 -2
  27. package/dist/cli/index.js +23 -8
  28. package/dist/drivers/herdr.d.ts +7 -2
  29. package/dist/drivers/herdr.js +78 -48
  30. package/dist/drivers/orca.d.ts +3 -2
  31. package/dist/drivers/orca.js +43 -4
  32. package/dist/drivers/subprocess.d.ts +1 -0
  33. package/dist/drivers/subprocess.js +3 -0
  34. package/dist/drivers/types.d.ts +14 -0
  35. package/dist/gates/artifact-manifest.d.ts +50 -0
  36. package/dist/gates/artifact-manifest.js +23 -0
  37. package/dist/gates/llm.d.ts +5 -0
  38. package/dist/gates/llm.js +188 -3
  39. package/dist/gates/review.d.ts +6 -3
  40. package/dist/gates/review.js +79 -33
  41. package/dist/gates/run-gates.d.ts +3 -0
  42. package/dist/gates/run-gates.js +14 -4
  43. package/dist/plan/scope.d.ts +25 -0
  44. package/dist/plan/scope.js +92 -12
  45. package/dist/report/operator-record.d.ts +49 -0
  46. package/dist/report/operator-record.js +137 -0
  47. package/dist/run/daemon.d.ts +5 -5
  48. package/dist/run/daemon.js +166 -44
  49. package/dist/run/lock.d.ts +59 -2
  50. package/dist/run/lock.js +184 -26
  51. package/dist/run/operator-state.d.ts +86 -0
  52. package/dist/run/operator-state.js +165 -0
  53. package/dist/run/supervision.d.ts +32 -0
  54. package/dist/run/supervision.js +138 -17
  55. package/dist/tui/cockpit/capture.d.ts +19 -0
  56. package/dist/tui/cockpit/capture.js +89 -1
  57. package/dist/tui/cockpit/components.d.ts +15 -1
  58. package/dist/tui/cockpit/components.js +79 -9
  59. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  60. package/dist/tui/cockpit/decision-actions.js +315 -0
  61. package/dist/tui/cockpit/derive.d.ts +1 -1
  62. package/dist/tui/cockpit/derive.js +2 -0
  63. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  64. package/dist/tui/cockpit/evidence-view.js +210 -0
  65. package/dist/tui/cockpit/home-view.d.ts +88 -0
  66. package/dist/tui/cockpit/home-view.js +240 -0
  67. package/dist/tui/cockpit/keys.d.ts +125 -0
  68. package/dist/tui/cockpit/keys.js +31 -0
  69. package/dist/tui/cockpit/layout.d.ts +14 -0
  70. package/dist/tui/cockpit/layout.js +15 -0
  71. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  72. package/dist/tui/cockpit/live-runtime.js +683 -0
  73. package/dist/tui/cockpit/live-store.d.ts +289 -0
  74. package/dist/tui/cockpit/live-store.js +308 -0
  75. package/dist/tui/cockpit/live.d.ts +21 -1
  76. package/dist/tui/cockpit/live.js +12 -1
  77. package/dist/tui/cockpit/run-view.d.ts +100 -0
  78. package/dist/tui/cockpit/run-view.js +202 -0
  79. package/dist/tui/cockpit/shell.d.ts +50 -0
  80. package/dist/tui/cockpit/shell.js +74 -0
  81. package/dist/tui/cockpit/theme.d.ts +27 -0
  82. package/dist/tui/cockpit/theme.js +21 -0
  83. package/dist/tui/ink/init-app.d.ts +4 -0
  84. package/dist/tui/ink/init-app.js +18 -7
  85. package/package.json +1 -1
  86. package/skills/tickmarkr-auto/SKILL.md +10 -1
  87. package/skills/tickmarkr-loop/SKILL.md +72 -1
  88. package/skills/tickmarkr-overseer/SKILL.md +12 -0
  89. package/skills/tickmarkr-overseer/scripts/watch-context.sh +13 -2
@@ -96,5 +96,239 @@ export declare function selfShadowFinding(ownVersion: string, cwd?: string, reso
96
96
  * check) reads it and bumps the constant.
97
97
  */
98
98
  export declare function liveBenchStalenessFinding(now: Date): string | undefined;
99
+ /**
100
+ * A no-call disclosure for the action that normal doctor performs. This is deliberately built
101
+ * from config alone: opening the disclosure must not turn a cache reader into a model purchase.
102
+ */
103
+ export declare function doctorProbePreflight(cwd?: string, cfg?: {
104
+ concurrency: number;
105
+ driver: "auto" | "herdr" | "subprocess" | "orca";
106
+ integrationBranchPrefix: string;
107
+ taskTimeoutMinutes: number;
108
+ contextWarnTokens: number;
109
+ routing: {
110
+ map: Record<string, {
111
+ pin?: {
112
+ via: string;
113
+ model: string;
114
+ } | undefined;
115
+ pool?: {
116
+ mode: "any" | "ordered";
117
+ channels: string[];
118
+ } | undefined;
119
+ tier?: undefined;
120
+ prefer?: string[] | undefined;
121
+ escalate?: boolean | undefined;
122
+ }>;
123
+ floors: Record<string, "cheap" | "mid" | "frontier">;
124
+ learned: "on" | "off";
125
+ allowUnverifiedModels: boolean;
126
+ mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
127
+ learnedTuning?: {
128
+ halfLifeRuns?: number | undefined;
129
+ availWeight?: number | undefined;
130
+ } | undefined;
131
+ explore?: {
132
+ mode?: "on" | "off" | undefined;
133
+ excludeShapes?: string[] | undefined;
134
+ excludeComplexityAtOrAbove?: number | null | undefined;
135
+ cap?: number | undefined;
136
+ } | undefined;
137
+ sla?: Record<string, number> | undefined;
138
+ allow?: {
139
+ adapters?: string[] | undefined;
140
+ models?: string[] | undefined;
141
+ } | undefined;
142
+ deny?: {
143
+ adapters?: string[] | undefined;
144
+ models?: string[] | undefined;
145
+ workers?: {
146
+ adapters?: string[] | undefined;
147
+ models?: string[] | undefined;
148
+ } | undefined;
149
+ } | undefined;
150
+ };
151
+ tiers: Record<string, {
152
+ vendor: string | null;
153
+ channel: "sub" | "api";
154
+ models: Record<string, "cheap" | "mid" | "frontier">;
155
+ modelOverrides?: Record<string, {
156
+ vendor?: string | undefined;
157
+ channel?: "sub" | "api" | undefined;
158
+ }> | undefined;
159
+ windows?: Record<string, number> | undefined;
160
+ }>;
161
+ pricing: Record<string, number>;
162
+ gates: {
163
+ build?: string | undefined;
164
+ test?: string | undefined;
165
+ lint?: string | undefined;
166
+ diffCap?: number | undefined;
167
+ byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
168
+ [x: string]: unknown;
169
+ acceptance?: boolean | undefined;
170
+ review?: boolean | undefined;
171
+ }>> | undefined;
172
+ };
173
+ judge: {
174
+ adapter: string;
175
+ model: string;
176
+ };
177
+ review: {
178
+ complexityThreshold: number;
179
+ timeoutMs: number;
180
+ required: boolean;
181
+ prefer?: string[] | undefined;
182
+ policy?: "full" | "judge-only" | undefined;
183
+ criticalPaths?: string[] | undefined;
184
+ };
185
+ consult: {
186
+ adapter: string;
187
+ model: string;
188
+ stallMinutes: number;
189
+ prefer?: string[] | undefined;
190
+ };
191
+ visibility: {
192
+ llm: "pane" | "headless";
193
+ keepPanes: "run" | "attempt" | "forever";
194
+ worker: "interactive" | "print";
195
+ workersPerTab: number;
196
+ };
197
+ setup?: string | undefined;
198
+ cost?: {
199
+ models?: Record<string, {
200
+ inPerMtok: number;
201
+ outPerMtok: number;
202
+ cacheReadPerMtok?: number | undefined;
203
+ rateDate?: string | undefined;
204
+ }> | undefined;
205
+ subs?: Record<string, {
206
+ planMonthly: number;
207
+ windowsPerMonthLow: number;
208
+ windowsPerMonthHigh: number;
209
+ }> | undefined;
210
+ } | undefined;
211
+ scope?: {
212
+ allowDeviations?: string[] | undefined;
213
+ } | undefined;
214
+ }, adapters?: WorkerAdapter[]): string;
215
+ /**
216
+ * Health surfaces use this cache-only reader. It intentionally receives adapters only to render
217
+ * their configured identities; it never invokes adapter.probe(), listModels(), or a model command.
218
+ */
219
+ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerAdapter[], cfg?: {
220
+ concurrency: number;
221
+ driver: "auto" | "herdr" | "subprocess" | "orca";
222
+ integrationBranchPrefix: string;
223
+ taskTimeoutMinutes: number;
224
+ contextWarnTokens: number;
225
+ routing: {
226
+ map: Record<string, {
227
+ pin?: {
228
+ via: string;
229
+ model: string;
230
+ } | undefined;
231
+ pool?: {
232
+ mode: "any" | "ordered";
233
+ channels: string[];
234
+ } | undefined;
235
+ tier?: undefined;
236
+ prefer?: string[] | undefined;
237
+ escalate?: boolean | undefined;
238
+ }>;
239
+ floors: Record<string, "cheap" | "mid" | "frontier">;
240
+ learned: "on" | "off";
241
+ allowUnverifiedModels: boolean;
242
+ mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
243
+ learnedTuning?: {
244
+ halfLifeRuns?: number | undefined;
245
+ availWeight?: number | undefined;
246
+ } | undefined;
247
+ explore?: {
248
+ mode?: "on" | "off" | undefined;
249
+ excludeShapes?: string[] | undefined;
250
+ excludeComplexityAtOrAbove?: number | null | undefined;
251
+ cap?: number | undefined;
252
+ } | undefined;
253
+ sla?: Record<string, number> | undefined;
254
+ allow?: {
255
+ adapters?: string[] | undefined;
256
+ models?: string[] | undefined;
257
+ } | undefined;
258
+ deny?: {
259
+ adapters?: string[] | undefined;
260
+ models?: string[] | undefined;
261
+ workers?: {
262
+ adapters?: string[] | undefined;
263
+ models?: string[] | undefined;
264
+ } | undefined;
265
+ } | undefined;
266
+ };
267
+ tiers: Record<string, {
268
+ vendor: string | null;
269
+ channel: "sub" | "api";
270
+ models: Record<string, "cheap" | "mid" | "frontier">;
271
+ modelOverrides?: Record<string, {
272
+ vendor?: string | undefined;
273
+ channel?: "sub" | "api" | undefined;
274
+ }> | undefined;
275
+ windows?: Record<string, number> | undefined;
276
+ }>;
277
+ pricing: Record<string, number>;
278
+ gates: {
279
+ build?: string | undefined;
280
+ test?: string | undefined;
281
+ lint?: string | undefined;
282
+ diffCap?: number | undefined;
283
+ byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
284
+ [x: string]: unknown;
285
+ acceptance?: boolean | undefined;
286
+ review?: boolean | undefined;
287
+ }>> | undefined;
288
+ };
289
+ judge: {
290
+ adapter: string;
291
+ model: string;
292
+ };
293
+ review: {
294
+ complexityThreshold: number;
295
+ timeoutMs: number;
296
+ required: boolean;
297
+ prefer?: string[] | undefined;
298
+ policy?: "full" | "judge-only" | undefined;
299
+ criticalPaths?: string[] | undefined;
300
+ };
301
+ consult: {
302
+ adapter: string;
303
+ model: string;
304
+ stallMinutes: number;
305
+ prefer?: string[] | undefined;
306
+ };
307
+ visibility: {
308
+ llm: "pane" | "headless";
309
+ keepPanes: "run" | "attempt" | "forever";
310
+ worker: "interactive" | "print";
311
+ workersPerTab: number;
312
+ };
313
+ setup?: string | undefined;
314
+ cost?: {
315
+ models?: Record<string, {
316
+ inPerMtok: number;
317
+ outPerMtok: number;
318
+ cacheReadPerMtok?: number | undefined;
319
+ rateDate?: string | undefined;
320
+ }> | undefined;
321
+ subs?: Record<string, {
322
+ planMonthly: number;
323
+ windowsPerMonthLow: number;
324
+ windowsPerMonthHigh: number;
325
+ }> | undefined;
326
+ } | undefined;
327
+ scope?: {
328
+ allowDeviations?: string[] | undefined;
329
+ } | undefined;
330
+ }): string;
331
+ /** Repair is an actuator, but never a reason to run the paid diagnostic sensor. */
332
+ export declare function doctorFixOnly(cwd?: string): string;
99
333
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
100
334
  export {};
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { detectPackageManager, turboContinueFindings } from "../../gates/baseline.js";
6
6
  import { version } from "./version.js";
7
- import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
7
+ import { allAdapters, binaryShadowWarnings, detectCandidateClis, doctorAgeMs, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, readDoctor, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
8
8
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
9
9
  import { shq } from "../../adapters/types.js";
10
10
  import { BANNER, compactTokens, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
@@ -347,8 +347,142 @@ export function liveBenchStalenessFinding(now) {
347
347
  return undefined;
348
348
  return `LiveBench table pinned at ${LIVEBENCH_TABLE_DATE} is ${days} days old (>${LIVEBENCH_TABLE_MAX_AGE_DAYS}d) — list ${LIVEBENCH_RELEASES_URL} for a newer table_<date>.csv and bump LIVEBENCH_TABLE_DATE (advisory — tickmarkr never fetches that listing)`;
349
349
  }
350
+ function configuredModels(cfg, adapters) {
351
+ const allowed = adapters ? new Set(adapters.map((adapter) => adapter.id)) : undefined;
352
+ return Object.entries(cfg.tiers).flatMap(([adapter, tier]) => allowed && !allowed.has(adapter) ? [] :
353
+ Object.keys(tier.models ?? {}).map((model) => ({ adapter, model })));
354
+ }
355
+ function cacheAge(ageMs) {
356
+ return ageMs === null ? "unavailable" : `${Math.floor(ageMs / 60_000)}m`;
357
+ }
358
+ /**
359
+ * A no-call disclosure for the action that normal doctor performs. This is deliberately built
360
+ * from config alone: opening the disclosure must not turn a cache reader into a model purchase.
361
+ */
362
+ export function doctorProbePreflight(cwd = process.cwd(), cfg = loadConfig(cwd), adapters = allAdapters()) {
363
+ const models = configuredModels(cfg, adapters);
364
+ const scope = models.length
365
+ ? models.map(({ adapter, model }) => `${adapter}:${model}`).join(", ")
366
+ : "none (no configured models)";
367
+ const state = stateDirName(cwd);
368
+ return [
369
+ "tickmarkr doctor — probe preflight:",
370
+ `configured model-call scope: ${models.length} model${models.length === 1 ? "" : "s"} — ${scope}`,
371
+ `cache policy: explicit probing replaces ${state}/doctor.json; each installed configured model receives one bounded call, with a serial retry only after a first failure (a prior timeout skips that retry)`,
372
+ `affected destinations: ${state}/doctor.json (adapter and model verdict cache); temporary probe prompts are removed`,
373
+ "catalog-only alternative: tickmarkr doctor --refresh-catalog refreshes the catalog cache without adapter or model probes",
374
+ ].join("\n");
375
+ }
376
+ /**
377
+ * Health surfaces use this cache-only reader. It intentionally receives adapters only to render
378
+ * their configured identities; it never invokes adapter.probe(), listModels(), or a model command.
379
+ */
380
+ export function cachedDoctorDiagnostics(cwd = process.cwd(), adapters = allAdapters(), cfg = loadConfig(cwd)) {
381
+ const health = readDoctor(cwd);
382
+ const age = doctorAgeMs(cwd);
383
+ const configured = configuredModels(cfg, adapters);
384
+ const state = stateDirName(cwd);
385
+ const knownAdapters = new Set(adapters.map((adapter) => adapter.id));
386
+ const rows = configured.map(({ adapter, model }) => {
387
+ const cachedAdapter = health?.[adapter];
388
+ const cachedModel = cachedAdapter?.modelAuth?.[model];
389
+ const status = !health
390
+ ? "unknown (cache unavailable)"
391
+ : !knownAdapters.has(adapter)
392
+ ? "unknown (adapter unavailable to this reader)"
393
+ : !cachedAdapter
394
+ ? "unknown (adapter absent from cache)"
395
+ : !cachedAdapter.installed
396
+ ? "unavailable (adapter not installed)"
397
+ : !cachedAdapter.authed
398
+ ? "unavailable (adapter not authenticated)"
399
+ : !cachedModel
400
+ ? "unknown (never probed)"
401
+ : cachedModel.authed
402
+ ? `passed (cached ${cachedModel.probedAt})`
403
+ : `failed (cached ${cachedModel.reason ?? "probe failed"})`;
404
+ return ` ${adapter}:${model} ${status}`;
405
+ });
406
+ return [
407
+ "tickmarkr doctor — cached diagnostics:",
408
+ `cache source: ${health ? `${state}/doctor.json` : "unavailable"}`,
409
+ `cache age: ${cacheAge(age)}`,
410
+ "cache policy: read-only; adapter and model probes were not called",
411
+ ...(rows.length ? ["configured model status:", ...rows] : ["configured model status: none (no configured models)"]),
412
+ ].join("\n");
413
+ }
414
+ const RUNNER_CONFIG_PATHS = [
415
+ "jest.config.js",
416
+ "jest.config.cjs",
417
+ "jest.config.mjs",
418
+ "jest.config.ts",
419
+ "jest.config.json",
420
+ "vitest.config.ts",
421
+ "vitest.config.js",
422
+ "vitest.config.mts",
423
+ "vitest.config.mjs",
424
+ "vitest.config.cts",
425
+ ];
426
+ function runnerConfigSnapshot(cwd) {
427
+ return new Map(RUNNER_CONFIG_PATHS.flatMap((path) => {
428
+ const absolute = join(cwd, path);
429
+ return existsSync(absolute) ? [[path, readFileSync(absolute, "utf8")]] : [];
430
+ }));
431
+ }
432
+ function actualTextDiff(path, before, after) {
433
+ const oldLines = before.split("\n");
434
+ const newLines = after.split("\n");
435
+ let start = 0;
436
+ while (start < oldLines.length && start < newLines.length && oldLines[start] === newLines[start])
437
+ start += 1;
438
+ let oldEnd = oldLines.length;
439
+ let newEnd = newLines.length;
440
+ while (oldEnd > start && newEnd > start && oldLines[oldEnd - 1] === newLines[newEnd - 1]) {
441
+ oldEnd -= 1;
442
+ newEnd -= 1;
443
+ }
444
+ return [
445
+ `--- ${path} (before)`,
446
+ `+++ ${path} (after)`,
447
+ ...oldLines.slice(start, oldEnd).map((line) => `-${line}`),
448
+ ...newLines.slice(start, newEnd).map((line) => `+${line}`),
449
+ ].join("\n");
450
+ }
451
+ /** Repair is an actuator, but never a reason to run the paid diagnostic sensor. */
452
+ export function doctorFixOnly(cwd = process.cwd()) {
453
+ const before = runnerConfigSnapshot(cwd);
454
+ const finding = runnerIgnoreFinding(cwd);
455
+ const repair = finding?.verdict === "warn"
456
+ ? applyRunnerIgnore(cwd)
457
+ : { action: "none", detail: finding?.detail ?? "no recognized test runner" };
458
+ const verified = runnerIgnoreFinding(cwd);
459
+ const diffs = [...before].flatMap(([path, original]) => {
460
+ const current = readFileSync(join(cwd, path), "utf8");
461
+ return current === original ? [] : [actualTextDiff(path, original, current)];
462
+ });
463
+ const result = repair.action === "wrote" && verified?.verdict !== "pass"
464
+ ? "invalid repair result (verification did not pass)"
465
+ : `${repair.action} — ${repair.detail}`;
466
+ return [
467
+ "tickmarkr doctor --fix-only — repair-only:",
468
+ "model probes: skipped (repair-only; adapters were not called)",
469
+ "catalog: untouched",
470
+ `repair result: ${result}`,
471
+ `repair verification: ${verified?.verdict ?? "unavailable"}`,
472
+ "repair diff:",
473
+ ...(diffs.length ? diffs : ["(no runner-config bytes changed)"]),
474
+ ].join("\n");
475
+ }
350
476
  export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
351
- if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
477
+ if (_argv.includes("--fix-only"))
478
+ return doctorFixOnly(cwd);
479
+ if (_argv.includes("--cached") || _argv.includes("--cached-only")) {
480
+ return cachedDoctorDiagnostics(cwd, adapters);
481
+ }
482
+ if (_argv.includes("--probe-preflight") || _argv.includes("--preflight")) {
483
+ return doctorProbePreflight(cwd, loadConfig(cwd), adapters);
484
+ }
485
+ if (_argv.length === 1 && (_argv[0] === "--refresh-catalog" || _argv[0] === "--catalog-only")) {
352
486
  const refreshed = await refreshCatalogCommand({ repoRoot: cwd, fetcher: opts.catalogFetcher, now: opts.catalogNow });
353
487
  return refreshed.updated
354
488
  ? `tickmarkr doctor --refresh-catalog: model catalog refreshed — ${formatCatalogRefreshLegs(refreshed.legs)}${refreshed.warning ? `; ${refreshed.warning}` : ""}`
@@ -378,9 +512,9 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
378
512
  // banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
379
513
  if (opts.banner !== false && visual())
380
514
  process.stdout.write(BANNER);
381
- // stderr, live: auth probes are real LLM calls (up to 30s per configured model) and the CLI
382
- // otherwise prints nothing until the end — silence here reads as a hang (v1.33.1)
383
- console.error("probing installed agent CLIs — one short LLM call per configured model, may take a minute...");
515
+ // stderr, live: disclose the paid boundary before the first adapter/model call. The separate
516
+ // --probe-preflight route exposes these same bytes without crossing that boundary.
517
+ console.error(`${doctorProbePreflight(cwd, cfg, adapters)}\nprobing installed agent CLIs — one short LLM call per configured model, may take a minute...`);
384
518
  const probeProgressTTY = process.stderr.isTTY === true;
385
519
  const health = await probeAll(adapters, { cwd });
386
520
  const kimiAdapter = adapters.find((a) => a.id === kimi.id);
@@ -1,3 +1,5 @@
1
+ export type DetectedHost = "herdr" | "orca" | "subprocess";
2
+ export declare function detectedHost(env?: NodeJS.ProcessEnv): DetectedHost;
1
3
  /** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
2
4
  export type InitIO = {
3
5
  input?: NodeJS.ReadStream;
@@ -4,6 +4,7 @@ import { join, relative } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { parseArgs } from "node:util";
7
+ import { parse as parseYaml } from "yaml";
7
8
  import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReuse } from "../../adapters/registry.js";
8
9
  import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
9
10
  import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
@@ -218,7 +219,14 @@ const describeDrift = (d) => [
218
219
  // should not be asked again just because a copy drifted. Staleness is a different question with a
219
220
  // different answer (`--agent --force`), and it is reported by name on every init path below.
220
221
  const skillsInstalled = (cwd) => hostTargets(cwd).every((t) => AGENT_SKILLS.every((s) => existsSync(join(t.skillsDir, s, "SKILL.md"))));
221
- const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : orcaHostDetected() ? "orca" : "auto";
222
+ export function detectedHost(env = process.env) {
223
+ if (env.HERDR_ENV === "1")
224
+ return "herdr";
225
+ if (orcaHostDetected(env))
226
+ return "orca";
227
+ return "subprocess";
228
+ }
229
+ const wizardDriverDefault = () => "auto";
222
230
  function packageName(cwd) {
223
231
  try {
224
232
  const parsed = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
@@ -323,6 +331,7 @@ async function runInitWizard(cwd, input, output) {
323
331
  visibilityLlm: DEFAULT_CONFIG.visibility.llm,
324
332
  offerSkills: !skillsInstalled(cwd),
325
333
  skillsDefault: existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd),
334
+ detectedHost: detectedHost(),
326
335
  },
327
336
  input,
328
337
  output,
@@ -368,8 +377,37 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
368
377
  }
369
378
  const repoConfigPath = join(tickmarkrDir(cwd), "config.yaml");
370
379
  const repoConfigExists = existsSync(repoConfigPath);
371
- if (repoConfigExists)
372
- notes.push(`kept existing ${repoConfigPath}`);
380
+ if (repoConfigExists) {
381
+ if (values.force) {
382
+ const currentRaw = readFileSync(repoConfigPath, "utf8");
383
+ let parsedDriver;
384
+ try {
385
+ const parsed = parseYaml(currentRaw);
386
+ if (typeof parsed?.driver === "string")
387
+ parsedDriver = parsed.driver;
388
+ }
389
+ catch {
390
+ // malformed: fall back to regex
391
+ }
392
+ if (!parsedDriver) {
393
+ const m = currentRaw.match(/^\s*driver:\s*([^\s#]+)/m);
394
+ if (m)
395
+ parsedDriver = m[1];
396
+ }
397
+ const host = detectedHost();
398
+ if (parsedDriver && parsedDriver === host) {
399
+ const rewritten = currentRaw.replace(new RegExp(`^(\\s*driver:\\s*)${parsedDriver}\\b.*$`, "m"), `$1auto # auto — resolves to ${host} here`);
400
+ writeFileSync(repoConfigPath, rewritten);
401
+ notes.push(`rewrote driver: ${parsedDriver} to auto in ${repoConfigPath}`);
402
+ }
403
+ else {
404
+ notes.push(`kept existing ${repoConfigPath}`);
405
+ }
406
+ }
407
+ else {
408
+ notes.push(`kept existing ${repoConfigPath}`);
409
+ }
410
+ }
373
411
  const specPath = join(cwd, SCAFFOLD_SPEC);
374
412
  if (existsSync(specPath)) {
375
413
  notes.push(`kept existing ${specPath}`);
@@ -417,7 +455,9 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
417
455
  // so costs one clause; everything else stops here. Esc and ctrl+c both land here.
418
456
  return `init: wizard quit — nothing further run (scaffolding kept: ${notes.filter((n) => n.startsWith("wrote")).length} file(s) written above). Re-run tickmarkr init to continue.`;
419
457
  }
420
- writeFileSync(repoConfigPath, configTemplate(wizard.overlay));
458
+ const host = detectedHost();
459
+ const renderedConfig = configTemplate(wizard.overlay).replace(/^driver: auto$/m, `driver: auto # auto — resolves to ${host} here`);
460
+ writeFileSync(repoConfigPath, renderedConfig);
421
461
  notes.push(`wrote ${repoConfigPath}`);
422
462
  if (wizard.installSkills) {
423
463
  await installAgentFiles(cwd, values.force ?? false, wizard.installDocs || (values.docs ?? false), notes);
@@ -462,7 +502,7 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
462
502
  const next = nextSteps(cwd, SCAFFOLD_SPEC);
463
503
  if (!visual())
464
504
  return `${notes.join("\n")}\n${doc}\n${next}\n${ENVIRONMENTS_FOOTER}`;
465
- const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
505
+ const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended|rewrote)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
466
506
  const footerRows = ENVIRONMENTS_FOOTER.split("\n").slice(1).map((line) => {
467
507
  const separator = line.indexOf(" — ");
468
508
  return kvRow(line.slice(2, separator), line.slice(separator + 1), 12);
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
@@ -7,7 +7,7 @@ import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
7
7
  import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
8
8
  import { classifyContextPath } from "../../compile/native.js";
9
9
  import { DEFAULT_CONFIG, effectiveReviewPolicy, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
10
- import { loadGraph } from "../../graph/graph.js";
10
+ import { graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
11
11
  import { renderAcceptanceItem } from "../../graph/schema.js";
12
12
  import { resolveRunMode } from "../../run/daemon.js";
13
13
  import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
@@ -15,7 +15,7 @@ import { staffLedEvidence } from "../../route/profile.js";
15
15
  import { route, RoutingError } from "../../route/router.js";
16
16
  import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
17
17
  import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
18
- import { Journal, loadRoutingProfile } from "../../run/journal.js";
18
+ import { Journal, loadRoutingProfile, recordedGraphDefinitionHash } from "../../run/journal.js";
19
19
  import { harnessLine, resolveHarness } from "../harness.js";
20
20
  import { channelKey, shq } from "../../adapters/types.js";
21
21
  import { shGit } from "../../run/git.js";
@@ -40,7 +40,10 @@ const stylizePlan = (out) => {
40
40
  const fleetCanCrossVendorReview = (channels) => {
41
41
  for (let i = 0; i < channels.length; i++)
42
42
  for (let j = 0; j < channels.length; j++)
43
- if (i !== j && channels[i].vendor !== channels[j].vendor && modelId(channels[i].model) !== modelId(channels[j].model))
43
+ if (i !== j
44
+ && channels[i].vendor !== channels[j].vendor
45
+ && modelProvider(channels[i].model, channels[i].vendor) !== modelProvider(channels[j].model, channels[j].vendor)
46
+ && modelId(channels[i].model) !== modelId(channels[j].model))
44
47
  return true;
45
48
  return false;
46
49
  };
@@ -121,6 +124,43 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
121
124
  throw new Error(`--mode must be one of ${ROUTING_MODES.join(" | ")} (got ${values.mode})`);
122
125
  }
123
126
  const g = loadGraph(cwd);
127
+ const gHash = graphDefinitionHash(g);
128
+ const runsRoot = join(cwd, stateDirName(cwd), "runs");
129
+ let matchingEvents;
130
+ let matchingRunId;
131
+ if (existsSync(runsRoot)) {
132
+ const locked = runLockRunId(cwd);
133
+ const candidates = readdirSync(runsRoot)
134
+ .filter((d) => d.startsWith("run-") && existsSync(join(runsRoot, d, "journal.jsonl")))
135
+ .sort();
136
+ const ordered = locked && candidates.includes(locked)
137
+ ? [locked, ...candidates.filter((id) => id !== locked).reverse()]
138
+ : candidates.reverse();
139
+ for (const runId of ordered) {
140
+ try {
141
+ const events = Journal.open(cwd, runId).read();
142
+ if (recordedGraphDefinitionHash(events) === gHash) {
143
+ matchingEvents = events;
144
+ matchingRunId = runId;
145
+ break;
146
+ }
147
+ }
148
+ catch {
149
+ // ignore unreadable or torn journals
150
+ }
151
+ }
152
+ }
153
+ const hasRun = matchingEvents !== undefined;
154
+ const reviewHistory = matchingEvents
155
+ ? matchingEvents
156
+ .filter((event) => event.event === "gate-result" && event.data.gate === "review" && typeof event.data.reviewer === "string")
157
+ .map((event) => event.data.reviewer)
158
+ : [];
159
+ // OBS-947 repair: a resumed run re-dispatches only tasks whose replayed status is pending (or that the
160
+ // journal never mentions). done, failed and human are all skipped alike, so rendering their preview
161
+ // picks must not advance the shared reviewHistory — a phantom draw wraps the LRU and makes a later
162
+ // pending task print the ranked-first seat while the journal's actual next draw is a later channel.
163
+ const taskStatuses = matchingRunId ? Journal.open(cwd, matchingRunId).replayStatuses() : new Map();
124
164
  const oracleRefusals = new Map();
125
165
  const oracleAdvisories = new Map();
126
166
  if (g.tasks.some((task) => task.acceptance.some((item) => typeof item === "object" && item.oracle === "test"))) {
@@ -306,10 +346,22 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
306
346
  const policy = effectiveReviewPolicy(task.files, cfg.review);
307
347
  if (policy === "judge-only")
308
348
  return { line: "none — reviewPolicy judge-only: declared leaf paths", cost: 0 };
309
- const reviewer = pickReviewer(author, pools.review, [], cfg.review.prefer ?? [], task.routingHints?.floor);
349
+ let rotationSeat = 1;
350
+ let rotationCount = 0;
351
+ const reviewer = pickReviewer(author, pools.review, [], cfg.review.prefer ?? [], task.routingHints?.floor, reviewHistory, (seat, count) => { rotationSeat = seat; rotationCount = count; });
310
352
  if (!reviewer)
311
353
  return { line: "none — no cross-vendor reviewer available", cost: 0 };
312
- return { line: `${channelKey(reviewer)} — reviewPolicy full`, cost: seatCost(reviewer) };
354
+ // Advance the simulated rotation only for tasks the run would actually dispatch next — the same
355
+ // rule as readyTasks (src/graph/graph.ts): pending, with every dependency already done. A pending
356
+ // task behind a parked/failed/pending dependency cannot run, so it consumes no draw (RULING-229-14).
357
+ const isPending = (id) => { const s = taskStatuses.get(id); return s === undefined || s === "pending"; };
358
+ const ready = isPending(task.id) && task.deps.every((d) => taskStatuses.get(d) === "done");
359
+ if (hasRun && ready)
360
+ reviewHistory.push(channelKey(reviewer));
361
+ const rotationNote = rotationCount > 1
362
+ ? (hasRun ? ` (seat ${rotationSeat} of ${rotationCount})` : ` (seat 1 of ${rotationCount} — rotation applies at run time)`)
363
+ : "";
364
+ return { line: `${channelKey(reviewer)}${rotationNote} — reviewPolicy full`, cost: seatCost(reviewer) };
313
365
  };
314
366
  const routed = [];
315
367
  for (const t of g.tasks) {