tickmarkr 1.90.6 → 1.90.8

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.
@@ -8,6 +8,9 @@ export type DoctorOpts = {
8
8
  resolveClaudeAliasIdentity?: (cwd: string, alias: ClaudeAlias) => string | undefined;
9
9
  catalog?: CatalogReadResult;
10
10
  catalogNow?: () => Date;
11
+ /** init's between-acts surface: status rows only — the model matrix and inline drift stay
12
+ * behind `tickmarkr doctor` (files are still written; only the RETURNED string shrinks). */
13
+ compact?: boolean;
11
14
  };
12
15
  export declare function runnerIgnoreFinding(cwd: string): {
13
16
  verdict: "pass" | "warn";
@@ -2,7 +2,7 @@ import { writeFileSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { detectPackageManager } from "../../gates/baseline.js";
5
+ import { detectPackageManager, turboContinueFindings } from "../../gates/baseline.js";
6
6
  import { version } from "./version.js";
7
7
  import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
8
8
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
@@ -212,6 +212,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
212
212
  ? `tickmarkr doctor --refresh-catalog: model catalog refreshed${refreshed.warning ? `; ${refreshed.warning}` : ""}`
213
213
  : `tickmarkr doctor --refresh-catalog: catalog refresh unavailable — ${refreshed.warning ?? "unknown failure"}; retained ${refreshed.catalog.source} catalog`;
214
214
  }
215
+ // Operator directive 2026-08-12 (declutter): long per-model lists render only when asked for.
216
+ const listAllModels = _argv.includes("--models");
215
217
  const cfg = loadConfig(cwd);
216
218
  // T17: synchronous/cache-only by operator ruling. The only network-bearing catalog function is the
217
219
  // separately invoked refreshCatalogCommand; ordinary doctor never calls it.
@@ -300,6 +302,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
300
302
  const pmRow = packageManagerFinding(cwd);
301
303
  if (pmRow)
302
304
  rows.push(alignedStatusRow(pmRow.verdict, "package-manager", pmRow.detail));
305
+ // GATE-FIX-4 §3 (dossier, 2026-08-13): an early-abort runner leaves later packages unverified
306
+ // yet baseline-forgiven — surface it BEFORE a run pays for it.
307
+ for (const f of turboContinueFindings(cwd, cfg)) {
308
+ rows.push(alignedStatusRow("warn", `gates.${f.gate}`, f.detail));
309
+ }
303
310
  // Q142s: tickmarkr's own binary gets the shadow check every adapter binary already had.
304
311
  const shadowRow = selfShadowFinding(await version(), cwd);
305
312
  if (shadowRow)
@@ -436,9 +443,24 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
436
443
  // Keep the historical count as an index, but it is no longer the whole report: every model gets
437
444
  // a cache-backed evidence or explicit uncovered line. Neither branch changes cfg, health, or route().
438
445
  rows.push(` ${dim(`(${unclassified.length} more listed, unclassified)`)}`);
439
- for (const model of unclassified) {
440
- const advisory = catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model));
446
+ const advisories = unclassified.map((model) => catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model)));
447
+ // Q128s exhibit #4 / operator directive 2026-08-12: ~600 per-model advisory rows buried the six
448
+ // decision-grade flags on a real machine. Default view keeps ONLY rows that ask the operator for a
449
+ // decision (tier suggestions); the no-decision bulk (uncovered + covered evidence-only) compresses
450
+ // to one counted line per adapter. `doctor --models` restores every row — asked-for, not ambient.
451
+ const decisive = advisories.filter((adv) => adv.suggestion);
452
+ const bulk = advisories.filter((adv) => !adv.suggestion);
453
+ const shown = listAllModels ? advisories : decisive;
454
+ for (const advisory of shown)
441
455
  rows.push(` ${dim(`catalog · ${advisory.display}`)}`);
456
+ if (!listAllModels && bulk.length) {
457
+ const uncovered = bulk.filter((adv) => adv.coverage === "uncovered").length;
458
+ const evidenceOnly = bulk.length - uncovered;
459
+ const parts = [
460
+ ...(uncovered ? [`${uncovered} uncovered by ${catalog.source === "cache" ? "cached catalogs" : "vendored catalog"}`] : []),
461
+ ...(evidenceOnly ? [`${evidenceOnly} covered without a tier suggestion`] : []),
462
+ ];
463
+ rows.push(` ${dim(`catalog · ${parts.join(" · ")} — tickmarkr doctor --models lists each`)}`);
442
464
  }
443
465
  }
444
466
  return rows;
@@ -449,5 +471,10 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
449
471
  const header = visual()
450
472
  ? `${statusRow("pass", `${title("tickmarkr doctor")} ${legend("· capability matrix")}`)}\n${rule()}`
451
473
  : "tickmarkr doctor — capability matrix:";
474
+ if (opts.compact) {
475
+ // Operator field report 2026-08-13: the full matrix mid-init-journey is clutter between two
476
+ // TUIs. Every file side effect above already happened; the pointer line is the contract.
477
+ return `${header}\n${rows.join("\n")}\n ${dim(`model matrix elided — \`tickmarkr doctor\` prints it in full${drift ? "; drift overlay written" : ""}`)}\nwrote ${stateDirName(cwd)}/doctor.json`;
478
+ }
452
479
  return `${header}\n${rows.join("\n")}${modelSummary}${drift}\nwrote ${stateDirName(cwd)}/doctor.json`;
453
480
  }
@@ -1,4 +1,6 @@
1
1
  import type { WorkerAdapter } from "../../adapters/types.js";
2
+ import type { FleetEditorResult } from "../../tui/ink/fleet-app.js";
3
+ type FleetEditorProps = Parameters<typeof import("../../tui/ink/fleet-app.js").runFleetInkEditor>[0];
2
4
  export type FleetInput = NodeJS.ReadableStream & {
3
5
  isTTY?: boolean;
4
6
  setRawMode?: (mode: boolean) => unknown;
@@ -26,3 +28,19 @@ export declare function fleet(argv: string[], cwd?: string, adapters?: WorkerAda
26
28
  out: string;
27
29
  code: number;
28
30
  }>;
31
+ /** Everything between the doctor-reuse gate and the Ink render, packaged for reuse: `tickmarkr init`
32
+ * runs the same editor (preset-first) without re-owning the assembly or the write funnel. `commit`
33
+ * remains the single config actuator — it maps editor results to the command's exact result strings
34
+ * and performs the one guarded overlay write. */
35
+ export declare function assembleFleetEditor(cwd: string, adapters: WorkerAdapter[], io: FleetIO, opts: {
36
+ globalDir: string;
37
+ entry?: "presets" | "probe";
38
+ fresh?: boolean;
39
+ }): Promise<{
40
+ props: FleetEditorProps;
41
+ commit: (result: FleetEditorResult) => string;
42
+ renderWhy: () => string;
43
+ } | {
44
+ unavailable: string;
45
+ }>;
46
+ export {};
@@ -104,12 +104,57 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
104
104
  }
105
105
  if (!why && !interactive)
106
106
  return { out: NON_TTY_MSG, code: 1 };
107
- const fresh = values.fresh ?? false;
108
- const { reuse, health: cached } = initDoctorReuse(cwd, fresh);
107
+ const assembled = await assembleFleetEditor(cwd, adapters, io, {
108
+ globalDir,
109
+ fresh: values.fresh ?? false,
110
+ });
111
+ if ("unavailable" in assembled)
112
+ return { out: assembled.unavailable, code: 1 };
113
+ if (why)
114
+ return assembled.renderWhy();
115
+ // Keep Ink out of print, non-TTY, and missing-probe paths: the component runtime belongs
116
+ // exclusively to the interactive editor. The capture window brackets only the dynamic Ink
117
+ // import — keys typed while the module loads land in props.initialInput, never get dropped.
118
+ const initialInput = [];
119
+ const productionInput = input;
120
+ const captureStartupInput = typeof productionInput.ref === "function"
121
+ && typeof productionInput.unref === "function";
122
+ const onStartupInput = (chunk) => {
123
+ initialInput.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
124
+ };
125
+ if (captureStartupInput) {
126
+ input.on("data", onStartupInput);
127
+ input.resume();
128
+ }
129
+ let runFleetInkEditor;
130
+ try {
131
+ ({ runFleetInkEditor } = await import("../../tui/ink/fleet-app.js"));
132
+ }
133
+ catch (error) {
134
+ if (captureStartupInput) {
135
+ input.off("data", onStartupInput);
136
+ input.pause();
137
+ }
138
+ throw error;
139
+ }
140
+ if (captureStartupInput) {
141
+ input.off("data", onStartupInput);
142
+ input.pause();
143
+ }
144
+ assembled.props.initialInput = initialInput;
145
+ const result = await runFleetInkEditor(assembled.props);
146
+ return assembled.commit(result);
147
+ }
148
+ /** Everything between the doctor-reuse gate and the Ink render, packaged for reuse: `tickmarkr init`
149
+ * runs the same editor (preset-first) without re-owning the assembly or the write funnel. `commit`
150
+ * remains the single config actuator — it maps editor results to the command's exact result strings
151
+ * and performs the one guarded overlay write. */
152
+ export async function assembleFleetEditor(cwd, adapters, io, opts) {
153
+ const globalDir = opts.globalDir;
154
+ const { reuse, health: cached } = initDoctorReuse(cwd, opts.fresh ?? false);
109
155
  if (!reuse || !cached) {
110
156
  return {
111
- out: "tickmarkr fleet: probe data missing or stale — run `tickmarkr doctor` first (fleet never re-probes; doctor is the sensor)",
112
- code: 1,
157
+ unavailable: "tickmarkr fleet: probe data missing or stale — run `tickmarkr doctor` first (fleet never re-probes; doctor is the sensor)",
113
158
  };
114
159
  }
115
160
  const rm = resolveRunMode(cwd, { globalDir });
@@ -231,8 +276,6 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
231
276
  return projectFleetWhy(values, { repoRoot: cwd, globalDir });
232
277
  };
233
278
  const routedShapeRows = (mode, map) => projectedShapeRows(mode, map).map(({ id, label }) => ({ id, label }));
234
- if (why)
235
- return renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map));
236
279
  const candidatesForShape = (shape, mode, map) => shapeCandidates(previewTask(shape), previewCfg(mode, map), channels, profile).map((candidate) => ({
237
280
  id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
238
281
  label: candidateRow(candidate, cfg.pricing),
@@ -255,6 +298,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
255
298
  const discovered = which === "review" ? [...reviewAdapters, ...seats] : seats;
256
299
  return [...discovered, ...current.filter((entry) => !discovered.includes(entry))];
257
300
  };
301
+ // The judge is one seat by schema (config.judge: { adapter, model }); the editor offers the same
302
+ // discovered seats universe plus a keep-default row, and a write is staged only on a real change.
303
+ const initialJudge = `${cfg.judge.adapter}:${cfg.judge.model}`;
258
304
  let pendingWrite = null;
259
305
  const reviewOverlay = (state) => {
260
306
  const staged = structuredClone(initial);
@@ -275,7 +321,10 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
275
321
  const path = repoOverlayPath(cwd);
276
322
  const modeChanged = state.selectedMode !== rm.mode.mode;
277
323
  const steeringChanged = ["review", "consult"].some((key) => JSON.stringify(state.steering[key]) !== JSON.stringify(initialSteering[key]));
278
- if (!modeChanged && !steeringChanged && fleetEditableEquals(initial, staged)) {
324
+ const judgeSeat = state.judgeSeat;
325
+ const judgeChanged = judgeSeat !== undefined
326
+ && `${judgeSeat.adapter}:${judgeSeat.model}` !== initialJudge;
327
+ if (!modeChanged && !steeringChanged && !judgeChanged && fleetEditableEquals(initial, staged)) {
279
328
  pendingWrite = null;
280
329
  return { kind: "empty" };
281
330
  }
@@ -283,6 +332,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
283
332
  initial,
284
333
  edited: staged,
285
334
  ...(modeChanged ? { mode: state.selectedMode } : {}),
335
+ ...(judgeChanged && judgeSeat ? { judge: judgeSeat } : {}),
286
336
  steering: { initial: initialSteering, edited: state.steering },
287
337
  };
288
338
  const after = renderFleetOverlayWrite(before, write);
@@ -301,35 +351,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
301
351
  };
302
352
  const reloadGuard = io.reloadGuard
303
353
  ?? ((bytes) => overlayBytesLoadError(cwd, bytes, { globalDir }));
304
- // Keep Ink out of print, non-TTY, and missing-probe paths: the component runtime belongs
305
- // exclusively to the interactive editor.
306
- const initialInput = [];
307
- const productionInput = input;
308
- const captureStartupInput = typeof productionInput.ref === "function"
309
- && typeof productionInput.unref === "function";
310
- const onStartupInput = (chunk) => {
311
- initialInput.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
312
- };
313
- if (captureStartupInput) {
314
- input.on("data", onStartupInput);
315
- input.resume();
316
- }
317
- let runFleetInkEditor;
318
- try {
319
- ({ runFleetInkEditor } = await import("../../tui/ink/fleet-app.js"));
320
- }
321
- catch (error) {
322
- if (captureStartupInput) {
323
- input.off("data", onStartupInput);
324
- input.pause();
325
- }
326
- throw error;
327
- }
328
- if (captureStartupInput) {
329
- input.off("data", onStartupInput);
330
- input.pause();
331
- }
332
- const result = await runFleetInkEditor({
354
+ const props = {
333
355
  ageMs: doctorAgeMs(cwd),
334
356
  adapters,
335
357
  health,
@@ -350,25 +372,35 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
350
372
  steeringOptionsFor,
351
373
  reviewOverlay,
352
374
  reloadGuard,
353
- initialInput,
354
- input: input,
355
- output: output,
375
+ entry: opts.entry,
376
+ initialJudge,
377
+ judgeSeats: seats,
378
+ initialInput: [],
379
+ input: (io.input ?? process.stdin),
380
+ output: (io.output ?? process.stdout),
356
381
  debug: io.debug,
357
- });
358
- if (result.kind === "quit")
359
- return QUIT;
360
- if (result.kind === "refresh") {
361
- return "fleet: run `tickmarkr doctor` to refresh probe data, then re-run `tickmarkr fleet` (doctor is the sensor; fleet never re-probes)";
362
- }
363
- if (result.kind === "no-changes")
364
- return "fleet: no overlay changes (empty diff)";
365
- if (result.kind === "discard")
366
- return "fleet: discarded overlay changes";
367
- // The command remains the single config actuator. Every interactive edit reaches this
368
- // one write only after the component-rendered diff confirm and the production reload guard.
369
- const write = pendingWrite;
370
- if (!write)
371
- throw new Error("fleet write reached confirmation without a staged overlay mutation");
372
- writeFleetOverlay(result.review.path, (prior) => renderFleetOverlayWrite(prior, write));
373
- return `fleet: wrote ${result.review.path}`;
382
+ };
383
+ const commit = (result) => {
384
+ if (result.kind === "quit")
385
+ return QUIT;
386
+ if (result.kind === "refresh") {
387
+ return "fleet: run `tickmarkr doctor` to refresh probe data, then re-run `tickmarkr fleet` (doctor is the sensor; fleet never re-probes)";
388
+ }
389
+ if (result.kind === "no-changes")
390
+ return "fleet: no overlay changes (empty diff)";
391
+ if (result.kind === "discard")
392
+ return "fleet: discarded overlay changes";
393
+ // The command remains the single config actuator. Every interactive edit reaches this
394
+ // one write only after the component-rendered diff confirm and the production reload guard.
395
+ const write = pendingWrite;
396
+ if (!write)
397
+ throw new Error("fleet write reached confirmation without a staged overlay mutation");
398
+ writeFleetOverlay(result.review.path, (prior) => renderFleetOverlayWrite(prior, write));
399
+ return `fleet: wrote ${result.review.path}`;
400
+ };
401
+ return {
402
+ props,
403
+ commit,
404
+ renderWhy: () => renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map)),
405
+ };
374
406
  }
@@ -1 +1,6 @@
1
- export declare function init(argv: string[], cwd?: string): Promise<string>;
1
+ /** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
2
+ export type InitIO = {
3
+ input?: NodeJS.ReadStream;
4
+ output?: NodeJS.WriteStream;
5
+ };
6
+ export declare function init(argv: string[], cwd?: string, io?: InitIO): Promise<string>;
@@ -10,6 +10,7 @@ import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
10
10
  import { tickmarkrDir } from "../../graph/graph.js";
11
11
  import { Journal } from "../../run/journal.js";
12
12
  import { doctor } from "./doctor.js";
13
+ import { assembleFleetEditor } from "./fleet.js";
13
14
  const SCAFFOLD_SPEC = "tickmarkr.spec.md";
14
15
  // Operator-approved (2026-07-17) environments footer — three rows; no npm install for herdr
15
16
  // (npm package "herdr" is a reserved 0.0.0 placeholder as of that date).
@@ -214,6 +215,11 @@ async function installAgentFiles(cwd, force, docs, notes) {
214
215
  const confirm = async (question) => {
215
216
  if (!interactive)
216
217
  return false;
218
+ // Post-wizard hardening: an Ink act that ran earlier in this process leaves stdin paused and
219
+ // unref'd on unmount, and readline revives neither — the process exits mid-question (found by
220
+ // the consolidated-init smoke). Revive explicitly before asking.
221
+ process.stdin.resume();
222
+ process.stdin.ref();
217
223
  prompt ??= createInterface({ input: process.stdin, output: process.stdout });
218
224
  return /^(?:y|yes)$/i.test((await prompt.question(`${question} [y/N] `)).trim());
219
225
  };
@@ -268,43 +274,31 @@ async function installAgentFiles(cwd, force, docs, notes) {
268
274
  prompt?.close();
269
275
  }
270
276
  }
271
- const askDefault = async (rl, label, def) => {
272
- const answer = (await rl.question(`${label} [${def}] `)).trim();
273
- return answer || def;
274
- };
275
- const askYesNo = async (rl, label, def) => {
276
- const hint = def ? "Y/n" : "y/N";
277
- const answer = (await rl.question(`${label} [${hint}] `)).trim();
278
- if (!answer)
279
- return def;
280
- return /^(?:y|yes)$/i.test(answer);
281
- };
282
- async function runInitWizard(cwd) {
283
- const rl = createInterface({ input: process.stdin, output: process.stdout });
284
- try {
285
- const driverDef = wizardDriverDefault();
286
- const driverRaw = await askDefault(rl, "Driver (auto|herdr|subprocess)", driverDef);
287
- const driver = ["auto", "herdr", "subprocess"].includes(driverRaw)
288
- ? driverRaw
289
- : driverDef;
290
- const concRaw = await askDefault(rl, "Concurrency", String(DEFAULT_CONFIG.concurrency));
291
- const parsed = Number.parseInt(concRaw, 10);
292
- const concurrency = Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_CONFIG.concurrency;
293
- const llmDef = DEFAULT_CONFIG.visibility.llm;
294
- const llmRaw = await askDefault(rl, "visibility.llm (pane|headless)", llmDef);
295
- const llm = llmRaw === "pane" || llmRaw === "headless" ? llmRaw : llmDef;
296
- let installSkills = false;
297
- if (!skillsInstalled(cwd)) {
298
- const skillsDef = existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd);
299
- installSkills = await askYesNo(rl, "Install agent skills (tickmarkr-loop/tickmarkr-auto/tickmarkr-overseer)?", skillsDef);
300
- }
301
- return { overlay: { driver, concurrency, visibility: { llm } }, installSkills };
302
- }
303
- finally {
304
- rl.close();
305
- }
277
+ // The consolidated wizard (operator directive 2026-08-12): init walks three acts in one
278
+ // command — preferences (Ink form) → discovery (doctor, the sensor) → fleet (presets or the
279
+ // full editor). The readline asks this replaced live on in git history; the Ink act keeps the
280
+ // same four questions and the same defaults, so `--yes` and non-TTY paths are unchanged.
281
+ async function runInitWizard(cwd, input, output) {
282
+ // Dynamic import, deliberately (same seam as fleet.ts): the Ink/React runtime must stay out of
283
+ // the non-TTY and --yes paths — init runs in CI and agent shells where importing Ink at module
284
+ // load would drag a renderer into paths that never draw.
285
+ const { runInitWizardApp } = await import("../../tui/ink/init-app.js");
286
+ const result = await runInitWizardApp({
287
+ fields: {
288
+ driver: wizardDriverDefault(),
289
+ concurrency: DEFAULT_CONFIG.concurrency,
290
+ visibilityLlm: DEFAULT_CONFIG.visibility.llm,
291
+ offerSkills: !skillsInstalled(cwd),
292
+ skillsDefault: existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd),
293
+ },
294
+ input,
295
+ output,
296
+ });
297
+ if (result.kind === "quit")
298
+ return null;
299
+ return { overlay: result.overlay, installSkills: result.installSkills, installDocs: result.installDocs };
306
300
  }
307
- export async function init(argv, cwd = process.cwd()) {
301
+ export async function init(argv, cwd = process.cwd(), io = {}) {
308
302
  let bannerEmitted = false;
309
303
  const emitBanner = () => {
310
304
  if (visual() && !bannerEmitted) {
@@ -363,26 +357,60 @@ export async function init(argv, cwd = process.cwd()) {
363
357
  notes.push(`stale ${join(d.dir, "SKILL.md")} — ${describeDrift(d)}; run tickmarkr init --agent --force to refresh it`);
364
358
  }
365
359
  }
366
- const fresh = values.fresh ?? false;
367
- const { reuse, ageMs, health } = initDoctorReuse(cwd, fresh);
368
- const doc = reuse && health && ageMs !== null
369
- ? `using probe results from ${formatDoctorAgeForInit(ageMs)} ago — run tickmarkr doctor to refresh (or init --fresh)\n${formatDoctorReport(cwd, loadConfig(cwd), health, allAdapters(), { wrote: false })}`
370
- : await doctor([], cwd, undefined, { banner: false });
371
- const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !(values.yes ?? false);
360
+ const input = io.input ?? process.stdin;
361
+ const output = io.output ?? process.stdout;
362
+ const interactive = input.isTTY === true && output.isTTY === true && !(values.yes ?? false);
363
+ // Act 1 — preferences. Only when the repo config does not exist yet: init never rewrites an
364
+ // operator's config; re-tuning is `tickmarkr fleet` (act 3 below still runs and writes overlays).
372
365
  if (!repoConfigExists) {
373
366
  if (interactive) {
374
367
  emitBanner();
375
- const wizard = await runInitWizard(cwd);
368
+ const wizard = await runInitWizard(cwd, input, output);
369
+ if (wizard === null) {
370
+ // Operator field report 2026-08-13: quit means QUIT — no probe, no report wall, no fleet
371
+ // act, one line out. Scaffolding above already happened (spec/global config), and saying
372
+ // so costs one clause; everything else stops here. Esc and ctrl+c both land here.
373
+ 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.`;
374
+ }
376
375
  writeFileSync(repoConfigPath, configTemplate(wizard.overlay));
377
376
  notes.push(`wrote ${repoConfigPath}`);
378
- if (wizard.installSkills)
379
- await installAgentFiles(cwd, values.force ?? false, values.docs ?? false, notes);
377
+ if (wizard.installSkills) {
378
+ await installAgentFiles(cwd, values.force ?? false, wizard.installDocs || (values.docs ?? false), notes);
379
+ }
380
380
  }
381
381
  else {
382
382
  writeFileSync(repoConfigPath, configTemplate());
383
383
  notes.push(`wrote ${repoConfigPath}`);
384
384
  }
385
385
  }
386
+ // Act 2 — discovery. Doctor is the sensor (fleet never re-probes); runs after preferences so the
387
+ // probe reads the config the operator just chose. Reuse keeps init fast; --fresh forces the probe.
388
+ // Interactive journeys get the COMPACT surface (status rows + pointer) — the full matrix between
389
+ // two TUIs was the operator's clutter report; non-TTY keeps the complete machine surface.
390
+ const fresh = values.fresh ?? false;
391
+ const { reuse, ageMs, health } = initDoctorReuse(cwd, fresh);
392
+ let doc = reuse && health && ageMs !== null
393
+ ? interactive
394
+ ? `using probe results from ${formatDoctorAgeForInit(ageMs)} ago — full matrix: tickmarkr doctor · refresh: init --fresh`
395
+ : `using probe results from ${formatDoctorAgeForInit(ageMs)} ago — run tickmarkr doctor to refresh (or init --fresh)\n${formatDoctorReport(cwd, loadConfig(cwd), health, allAdapters(), { wrote: false })}`
396
+ : await doctor([], cwd, undefined, { banner: false, compact: interactive });
397
+ // Act 3 — fleet. The compact discovery surface prints BETWEEN the acts so the operator reads
398
+ // what the presets screen ranks with; the final summary then carries a pointer, not a repeat.
399
+ if (interactive) {
400
+ output.write(`${doc}\n`);
401
+ doc = "discovery shown above — full matrix any time with `tickmarkr doctor`";
402
+ // Same lazy-Ink seam as fleet.ts: load the editor runtime up front so the assembler's
403
+ // returned props need no startup-input capture window on this path.
404
+ const { runFleetInkEditor } = await import("../../tui/ink/fleet-app.js");
405
+ const editor = await assembleFleetEditor(cwd, allAdapters(), { input, output }, { globalDir: gdir, entry: "presets" });
406
+ if ("unavailable" in editor) {
407
+ notes.push(`fleet: ${editor.unavailable}`);
408
+ }
409
+ else {
410
+ const result = await runFleetInkEditor(editor.props);
411
+ notes.push(editor.commit(result));
412
+ }
413
+ }
386
414
  if (values.agent)
387
415
  await installAgentFiles(cwd, values.force ?? false, values.docs ?? false, notes);
388
416
  const next = nextSteps(cwd, SCAFFOLD_SPEC);
@@ -67,8 +67,9 @@ export async function run(argv, cwd = process.cwd()) {
67
67
  console.warn(`tickmarkr: !! ${conflict}`);
68
68
  }
69
69
  // OBS-107 (v1.67 T5): advisory only — a stale compiled graph with prior terminal statuses and no
70
- // live daemon will otherwise finish with zero work and confuse the operator. The recompile remedy
71
- // is named; dispatch proceeds unchanged.
70
+ // live daemon will otherwise confuse the operator. The recompile remedy is named; dispatch
71
+ // proceeds unchanged when anything is still dispatchable. When NOTHING is (all-terminal graph),
72
+ // runDaemon's no-op refusal (GATE-FIX-4 defect 4) turns the old zero-work "finished" into an error.
72
73
  const TERMINAL_STATUSES = ["done", "failed", "human"];
73
74
  if (graph.tasks.some((t) => TERMINAL_STATUSES.includes(t.status)) && !isRunLockLive(cwd)) {
74
75
  console.warn("tickmarkr: compiled graph carries terminal statuses from a prior run and no daemon is active — "
@@ -3,6 +3,7 @@ import { type AcceptanceItem } from "../../graph/schema.js";
3
3
  export declare function parseCriteria(text: string): AcceptanceItem[];
4
4
  export declare const HUMAN_CHANNEL: BillingChannel;
5
5
  export declare const HUMAN_AUTHOR: Assignment;
6
+ export declare function verifyStateDir(cwd: string): string;
6
7
  export declare function verify(argv: string[], cwd?: string): Promise<{
7
8
  out: string;
8
9
  code: number;
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { parseArgs } from "node:util";
@@ -42,6 +42,16 @@ export function parseCriteria(text) {
42
42
  // verify models the author as a "human" vendor channel — resolvable, excludes nothing real.
43
43
  export const HUMAN_CHANNEL = { adapter: "human", vendor: "human", model: "human", channel: "sub", tier: "frontier" };
44
44
  export const HUMAN_AUTHOR = { adapter: "human", model: "human", channel: "sub", tier: "frontier" };
45
+ // GATE-FIX-4 defect 1 (false-RED on macOS): os.tmpdir() returns /var/folders/…, a symlink into
46
+ // /private/var — so a baseline captured under the repo path and a head battery run under the tmp
47
+ // path disagree on every path-bearing fingerprint, and verify reds a green diff. graph.ts's
48
+ // saveGraph carries the standing precedent ("never os.tmpdir()" — rename(2) atomicity there, path
49
+ // identity here): tmpdir is fine for verify's disposable state, but only through realpathSync so
50
+ // every path verify hands to gates is already canonical. Exported for the unit test that pins the
51
+ // realpath (CI cannot rely on the macOS symlink).
52
+ export function verifyStateDir(cwd) {
53
+ return join(realpathSync(tmpdir()), "tickmarkr-verify", createHash("sha256").update(cwd).digest("hex").slice(0, 12));
54
+ }
45
55
  export async function verify(argv, cwd = process.cwd()) {
46
56
  const { values } = parseArgs({
47
57
  args: argv,
@@ -100,7 +110,7 @@ export async function verify(argv, cwd = process.cwd()) {
100
110
  // ALL verify state (cache, base worktree, artifacts) lives OUTSIDE the repo: verify gates the repo
101
111
  // root itself, so any file it wrote there would trip the battery's own dirty-worktree refusal.
102
112
  // ponytail: tmpdir means the baseline cache dies on reboot/cleanup — worst case is one re-capture.
103
- const stateDir = join(tmpdir(), "tickmarkr-verify", createHash("sha256").update(cwd).digest("hex").slice(0, 12));
113
+ const stateDir = verifyStateDir(cwd);
104
114
  mkdirSync(stateDir, { recursive: true });
105
115
  let baseline;
106
116
  const cachePath = join(stateDir, `baseline-${mergeBase.slice(0, 12)}.json`);
@@ -5,6 +5,10 @@ export type FleetOverlayWrite = {
5
5
  initial: FleetEditable;
6
6
  edited: FleetEditable;
7
7
  mode?: RoutingMode;
8
+ judge?: {
9
+ adapter: string;
10
+ model: string;
11
+ };
8
12
  steering?: {
9
13
  initial: {
10
14
  review?: string[];
@@ -170,6 +170,10 @@ export function renderFleetOverlayWrite(priorBytes, write) {
170
170
  if (write.mode !== undefined) {
171
171
  setScalarPreservingComment(doc, ["routing", "mode"], write.mode);
172
172
  }
173
+ if (write.judge) {
174
+ setScalarPreservingComment(doc, ["judge", "adapter"], write.judge.adapter);
175
+ setScalarPreservingComment(doc, ["judge", "model"], write.judge.model);
176
+ }
173
177
  if (write.steering) {
174
178
  for (const key of ["review", "consult"]) {
175
179
  const before = write.steering.initial[key];
@@ -47,6 +47,21 @@ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
47
47
  */
48
48
  export declare function detectPackageManager(repoRoot: string): PackageManager;
49
49
  export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
50
+ /**
51
+ * Dossier GATE-FIX-4 §3 (2026-08-13): turbo schedules per-package tasks and ABORTS the whole
52
+ * run at the first failing package, so every package after the abort goes unverified — and the
53
+ * baseline gate then forgives the truncated output as "matching". The class appeared in three
54
+ * gates and four syntaxes on one repo (turbo's scheduler, `&&`, a JS for-loop process.exit, and
55
+ * turbo nested inside two of them). This preflight names only what it can see textually: the
56
+ * resolved gate command, and — for the synthesized `<pm> run <script>` form — that script's own
57
+ * body. The remedy deliberately demands a forwarding check first: the same repo's lint wrapper
58
+ * branched on argv.length, so a blind `-- --continue` silently replaced the gate with a
59
+ * different command (the trap their overseer caught by reading, not by symmetry).
60
+ */
61
+ export declare function turboContinueFindings(repoRoot: string, cfg: TickmarkrConfig): Array<{
62
+ gate: string;
63
+ detail: string;
64
+ }>;
50
65
  /**
51
66
  * The ceiling a battery runs under: whichever is larger of the shipped constant and the slack applied
52
67
  * to what this command actually measured. A pre-v1.90 baseline (no measurement) gets exactly the