@tokenfactory/acc-runner 0.44.1 → 0.44.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +20 -1
  2. package/dist/bin-resolve.d.ts +39 -3
  3. package/dist/bin-resolve.d.ts.map +1 -1
  4. package/dist/bin-resolve.js +176 -12
  5. package/dist/bin-resolve.js.map +1 -1
  6. package/dist/build-fingerprint.d.ts +66 -0
  7. package/dist/build-fingerprint.d.ts.map +1 -0
  8. package/dist/build-fingerprint.js +70 -0
  9. package/dist/build-fingerprint.js.map +1 -0
  10. package/dist/build-info.generated.d.ts +3 -0
  11. package/dist/build-info.generated.d.ts.map +1 -0
  12. package/dist/build-info.generated.js +3 -0
  13. package/dist/build-info.generated.js.map +1 -0
  14. package/dist/capacity/resume-controller.d.ts +5 -3
  15. package/dist/capacity/resume-controller.d.ts.map +1 -1
  16. package/dist/capacity/resume-controller.js +9 -0
  17. package/dist/capacity/resume-controller.js.map +1 -1
  18. package/dist/chat-delegation-guard.d.ts +23 -0
  19. package/dist/chat-delegation-guard.d.ts.map +1 -0
  20. package/dist/chat-delegation-guard.js +62 -0
  21. package/dist/chat-delegation-guard.js.map +1 -0
  22. package/dist/chat-lane.d.ts +17 -6
  23. package/dist/chat-lane.d.ts.map +1 -1
  24. package/dist/chat-lane.js +29 -8
  25. package/dist/chat-lane.js.map +1 -1
  26. package/dist/companion-identity.d.ts +37 -0
  27. package/dist/companion-identity.d.ts.map +1 -1
  28. package/dist/companion-identity.js +98 -29
  29. package/dist/companion-identity.js.map +1 -1
  30. package/dist/companion-run.d.ts +1 -50
  31. package/dist/companion-run.d.ts.map +1 -1
  32. package/dist/companion-run.js +48 -105
  33. package/dist/companion-run.js.map +1 -1
  34. package/dist/config.d.ts +1 -0
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +14 -3
  37. package/dist/config.js.map +1 -1
  38. package/dist/conformance.d.ts +49 -0
  39. package/dist/conformance.d.ts.map +1 -0
  40. package/dist/conformance.js +121 -0
  41. package/dist/conformance.js.map +1 -0
  42. package/dist/cost-pricing.d.ts +46 -0
  43. package/dist/cost-pricing.d.ts.map +1 -1
  44. package/dist/cost-pricing.js +58 -1
  45. package/dist/cost-pricing.js.map +1 -1
  46. package/dist/credential-identity.d.ts +70 -0
  47. package/dist/credential-identity.d.ts.map +1 -0
  48. package/dist/credential-identity.js +128 -0
  49. package/dist/credential-identity.js.map +1 -0
  50. package/dist/doctor.d.ts +33 -1
  51. package/dist/doctor.d.ts.map +1 -1
  52. package/dist/doctor.js +284 -44
  53. package/dist/doctor.js.map +1 -1
  54. package/dist/engines/chat-stream.d.ts +71 -1
  55. package/dist/engines/chat-stream.d.ts.map +1 -1
  56. package/dist/engines/chat-stream.js +126 -8
  57. package/dist/engines/chat-stream.js.map +1 -1
  58. package/dist/engines/claude-code-chat.d.ts +11 -1
  59. package/dist/engines/claude-code-chat.d.ts.map +1 -1
  60. package/dist/engines/claude-code-chat.js +69 -5
  61. package/dist/engines/claude-code-chat.js.map +1 -1
  62. package/dist/engines/claude-code.d.ts.map +1 -1
  63. package/dist/engines/claude-code.js +30 -6
  64. package/dist/engines/claude-code.js.map +1 -1
  65. package/dist/engines/codex.d.ts.map +1 -1
  66. package/dist/engines/codex.js +18 -5
  67. package/dist/engines/codex.js.map +1 -1
  68. package/dist/engines/registry.d.ts +26 -0
  69. package/dist/engines/registry.d.ts.map +1 -1
  70. package/dist/engines/registry.js +37 -0
  71. package/dist/engines/registry.js.map +1 -1
  72. package/dist/engines/types.d.ts +5 -0
  73. package/dist/engines/types.d.ts.map +1 -1
  74. package/dist/failure-classifier.d.ts +1 -1
  75. package/dist/failure-classifier.d.ts.map +1 -1
  76. package/dist/failure-classifier.js +65 -12
  77. package/dist/failure-classifier.js.map +1 -1
  78. package/dist/gh.d.ts +15 -0
  79. package/dist/gh.d.ts.map +1 -1
  80. package/dist/gh.js +18 -0
  81. package/dist/gh.js.map +1 -1
  82. package/dist/git.d.ts +6 -0
  83. package/dist/git.d.ts.map +1 -1
  84. package/dist/git.js +27 -1
  85. package/dist/git.js.map +1 -1
  86. package/dist/keychain.d.ts +17 -0
  87. package/dist/keychain.d.ts.map +1 -1
  88. package/dist/keychain.js +30 -0
  89. package/dist/keychain.js.map +1 -1
  90. package/dist/loc-advisory.d.ts +70 -0
  91. package/dist/loc-advisory.d.ts.map +1 -0
  92. package/dist/loc-advisory.js +196 -0
  93. package/dist/loc-advisory.js.map +1 -0
  94. package/dist/locks/claim-lock-policy.d.ts +67 -0
  95. package/dist/locks/claim-lock-policy.d.ts.map +1 -0
  96. package/dist/locks/claim-lock-policy.js +132 -0
  97. package/dist/locks/claim-lock-policy.js.map +1 -0
  98. package/dist/login.d.ts +59 -2
  99. package/dist/login.d.ts.map +1 -1
  100. package/dist/login.js +87 -7
  101. package/dist/login.js.map +1 -1
  102. package/dist/mcp-spawn.d.ts +54 -0
  103. package/dist/mcp-spawn.d.ts.map +1 -1
  104. package/dist/mcp-spawn.js +88 -4
  105. package/dist/mcp-spawn.js.map +1 -1
  106. package/dist/migration-seq-guard.d.ts +28 -1
  107. package/dist/migration-seq-guard.d.ts.map +1 -1
  108. package/dist/migration-seq-guard.js +111 -6
  109. package/dist/migration-seq-guard.js.map +1 -1
  110. package/dist/output-diagnostics.d.ts +15 -0
  111. package/dist/output-diagnostics.d.ts.map +1 -0
  112. package/dist/output-diagnostics.js +95 -0
  113. package/dist/output-diagnostics.js.map +1 -0
  114. package/dist/pr-open-guard.d.ts +67 -0
  115. package/dist/pr-open-guard.d.ts.map +1 -0
  116. package/dist/pr-open-guard.js +79 -0
  117. package/dist/pr-open-guard.js.map +1 -0
  118. package/dist/prepr-gate.d.ts +229 -1
  119. package/dist/prepr-gate.d.ts.map +1 -1
  120. package/dist/prepr-gate.js +406 -3
  121. package/dist/prepr-gate.js.map +1 -1
  122. package/dist/program.d.ts.map +1 -1
  123. package/dist/program.js +30 -5
  124. package/dist/program.js.map +1 -1
  125. package/dist/provider-auth.d.ts +8 -0
  126. package/dist/provider-auth.d.ts.map +1 -1
  127. package/dist/provider-auth.js +10 -0
  128. package/dist/provider-auth.js.map +1 -1
  129. package/dist/runtime/quarantine.d.ts +11 -0
  130. package/dist/runtime/quarantine.d.ts.map +1 -1
  131. package/dist/runtime/quarantine.js +16 -1
  132. package/dist/runtime/quarantine.js.map +1 -1
  133. package/dist/runtime/reviewer.d.ts +90 -2
  134. package/dist/runtime/reviewer.d.ts.map +1 -1
  135. package/dist/runtime/reviewer.js +547 -46
  136. package/dist/runtime/reviewer.js.map +1 -1
  137. package/dist/runtime/running-build.d.ts +24 -0
  138. package/dist/runtime/running-build.d.ts.map +1 -0
  139. package/dist/runtime/running-build.js +62 -0
  140. package/dist/runtime/running-build.js.map +1 -0
  141. package/dist/runtime/worktree.d.ts +45 -0
  142. package/dist/runtime/worktree.d.ts.map +1 -1
  143. package/dist/runtime/worktree.js +174 -5
  144. package/dist/runtime/worktree.js.map +1 -1
  145. package/dist/task-runner.d.ts +126 -5
  146. package/dist/task-runner.d.ts.map +1 -1
  147. package/dist/task-runner.js +1219 -127
  148. package/dist/task-runner.js.map +1 -1
  149. package/dist/types.d.ts +8 -0
  150. package/dist/types.d.ts.map +1 -1
  151. package/dist/watch-chat/wire.d.ts +80 -0
  152. package/dist/watch-chat/wire.d.ts.map +1 -1
  153. package/dist/watch-chat/wire.js +203 -43
  154. package/dist/watch-chat/wire.js.map +1 -1
  155. package/dist/watch.d.ts +70 -16
  156. package/dist/watch.d.ts.map +1 -1
  157. package/dist/watch.js +501 -89
  158. package/dist/watch.js.map +1 -1
  159. package/package.json +2 -2
package/dist/doctor.js CHANGED
@@ -8,18 +8,19 @@ import os from "node:os";
8
8
  import path from "node:path";
9
9
  import { execa } from "execa";
10
10
  import chalk from "chalk";
11
- import { buildEnv } from "./bin-resolve.js";
11
+ import { buildEnv, executableSearchPaths, resolveExecutable, unresolvedExecutableDetail, } from "./bin-resolve.js";
12
12
  import { authPrecedenceHint, servingAuthKind, describeServingAuthKind, anthropicApiKey, forwardsAmbientKey, } from "./provider-auth.js";
13
- import { openaiApiKey, openaiApiKeyFromKeychain, describeCodexAuthMode, } from "./provider-auth.js";
13
+ import { openaiApiKey, openaiApiKeyFromKeychain, describeCodexAuthMode } from "./provider-auth.js";
14
14
  import { codexEngineEnabled, resolveCodexSpawnMode } from "./engines/codex.js";
15
+ import { engineRegistrationState, getConfiguredChatEngine } from "./engines/registry.js";
15
16
  import { claudeCodeEngine } from "./engines/claude-code.js";
16
- import { claudeCodeChatEngine } from "./engines/claude-code-chat.js";
17
17
  import { applyBootstrapConfig, CONCURRENCY_ENV, concurrencyEnvExplicit, configSources, loadConfigOrNull, resolveConcurrencyLimit, resolveEffectiveConcurrency, resolveReservedReviewSlots, RESERVED_REVIEW_SLOT_ENV, supabaseConfigSource, } from "./config.js";
18
18
  import { getTokenProvider, resolveTokenMode } from "./token-provider.js";
19
19
  import { cacheDir } from "./runtime/cache-dir.js";
20
20
  import { cloneCacheRoot, listCachedClones } from "./runtime/clone-cache.js";
21
21
  import { createRunnerClient } from "./supabase.js";
22
22
  import { machineIdentity } from "./login.js";
23
+ import { safeText } from "./machine-capabilities/redact.js";
23
24
  import { INSTANCE_ENV, INSTANCE_NAME_RE, instanceKeychainAccount, resolveInstance, } from "./instance.js";
24
25
  import { checkVersion } from "./version-check.js";
25
26
  import { PROTOCOL_VERSION } from "./types.js";
@@ -33,6 +34,10 @@ import { getClaudeVersion, npmLatestVersion, readTaskEnv, } from "./runtime/vers
33
34
  import { compareSemver as compareSemverVer } from "./version-check.js";
34
35
  import { latestSelfUpdateAttempt } from "./runtime/self-upgrade.js";
35
36
  import { PACKAGE_VERSION } from "./pkg-version.js";
37
+ import { resolveCredentialIdentity } from "./credential-identity.js";
38
+ import { readRunningBuild } from "./runtime/running-build.js";
39
+ import { runningBuildStatus } from "./build-fingerprint.js";
40
+ import { probeMcpServer } from "./mcp-spawn.js";
36
41
  export const LAUNCHD_LABEL = "com.tokenfactory.acc-runner";
37
42
  export function launchdPlistPath() {
38
43
  return path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
@@ -76,7 +81,7 @@ function maskKey(s) {
76
81
  function formatConfig(cfg, bootstrap) {
77
82
  const src = configSources(bootstrap);
78
83
  const lines = Object.keys(CONFIG_LABELS).map((k) => {
79
- // v0.41-B: worktreeRepoPath is optional at the config layer
84
+ // worktreeRepoPath is optional at the config layer
80
85
  // (undefined means "use repoPath"). Render "<not set>" rather than
81
86
  // the literal string "undefined" so the doctor output is clean.
82
87
  const raw = cfg[k];
@@ -131,9 +136,26 @@ function formatRuntime() {
131
136
  ];
132
137
  return [chalk.bold("Runtime:"), ...lines].join("\n");
133
138
  }
134
- async function checkBinary(bin, args) {
139
+ async function checkBinary(bin, args, resolveOptions = {}) {
140
+ const platform = resolveOptions.platform ?? process.platform;
141
+ const env = resolveOptions.env ?? buildEnv();
142
+ // AC4: no behaviour change on darwin/linux. Only Windows resolves a concrete
143
+ // shim up front (execa cannot find a PATHEXT `.cmd`/`.exe` by bare name); POSIX
144
+ // hands execa the bare name, keeping its own PATH resolution + ENOENT failure.
145
+ let command = bin;
146
+ if (platform === "win32") {
147
+ const options = { ...resolveOptions, env, platform };
148
+ const resolved = resolveExecutable(bin, options);
149
+ if (!resolved) {
150
+ return {
151
+ ok: false,
152
+ detail: unresolvedExecutableDetail(bin, executableSearchPaths(bin, options)),
153
+ };
154
+ }
155
+ command = resolved.command;
156
+ }
135
157
  try {
136
- const { stdout } = await execa(bin, args, { env: buildEnv() });
158
+ const { stdout } = await execa(command, args, { env });
137
159
  const first = stdout.split(/\r?\n/)[0]?.trim() ?? bin;
138
160
  return { ok: true, detail: first };
139
161
  }
@@ -150,8 +172,8 @@ async function checkNode() {
150
172
  fix: "Install Node 20.6+ (https://nodejs.org).",
151
173
  };
152
174
  }
153
- async function checkClaude() {
154
- const r = await checkBinary("claude", ["--version"]);
175
+ export async function checkClaude(resolveOptions = {}) {
176
+ const r = await checkBinary("claude", ["--version"], resolveOptions);
155
177
  return {
156
178
  name: "claude CLI in PATH",
157
179
  ok: r.ok,
@@ -178,9 +200,7 @@ async function checkAnthropicKey() {
178
200
  return {
179
201
  name,
180
202
  ok: true,
181
- detail: hint
182
- ? `${describeServingAuthKind(kind)} — ${hint}`
183
- : describeServingAuthKind(kind),
203
+ detail: hint ? `${describeServingAuthKind(kind)} — ${hint}` : describeServingAuthKind(kind),
184
204
  };
185
205
  }
186
206
  return {
@@ -219,16 +239,23 @@ async function checkAnthropicKey() {
219
239
  * Secrets are never printed (the probe detail redacts key material). The probe
220
240
  * is injectable so the doctor test exercises the rendering without live gh/auth.
221
241
  */
222
- export async function checkChatAuth(probe = () => claudeCodeChatEngine.credentialProbe()) {
242
+ export async function checkChatAuth(probe, engineId) {
243
+ const engine = !probe || !engineId ? getConfiguredChatEngine() : null;
244
+ const selectedEngineId = engineId ?? engine?.id ?? "claude-code";
223
245
  const name = "chat lane credentials";
224
- const result = await probe();
246
+ const result = await (probe ?? (() => engine.credentialProbe()))();
225
247
  if (!result.ok) {
248
+ const login = selectedEngineId === "codex"
249
+ ? "codex login"
250
+ : selectedEngineId === "gemini"
251
+ ? "gemini"
252
+ : "claude login";
226
253
  return {
227
254
  name,
228
255
  ok: false,
229
256
  warn: true,
230
- detail: `chat lane unavailable — ${result.detail}`,
231
- fix: "run an interactive `claude` login so the chat lane can serve turns on the operator's subscription (REVIEW-SUBS: ambient ANTHROPIC_API_KEY is no longer used).",
257
+ detail: `chat lane unavailable for ${selectedEngineId} — ${result.detail}`,
258
+ fix: `run \`${login}\` under the runner's OS user so ${selectedEngineId} can serve chat from that subscription; no API key is required.`,
232
259
  };
233
260
  }
234
261
  const draftOnly = result.tier === "draft-only";
@@ -238,10 +265,61 @@ export async function checkChatAuth(probe = () => claudeCodeChatEngine.credentia
238
265
  warn: draftOnly ? true : undefined,
239
266
  detail: `tier=${result.tier} (${result.channel}); ${result.detail}`,
240
267
  ...(draftOnly
241
- ? { fix: "authenticate `gh` (run `gh auth login`) to lift the chat lane from draft-only to full (repo-mutating) turns." }
268
+ ? {
269
+ fix: "authenticate `gh` (run `gh auth login`) to lift the chat lane from draft-only to full (repo-mutating) turns.",
270
+ }
242
271
  : {}),
243
272
  };
244
273
  }
274
+ /** Prove the installed/fallback MCP command can initialize and list its catalog. */
275
+ export async function checkMcpServer(cfg, deps = {}) {
276
+ const name = "ACC MCP server";
277
+ if (!cfg)
278
+ return { name, ok: true, detail: "config unavailable (skipped)" };
279
+ let session;
280
+ try {
281
+ session = await (deps.loadSession ?? (() => getTokenProvider().load()))();
282
+ }
283
+ catch (err) {
284
+ return {
285
+ name,
286
+ ok: false,
287
+ detail: `runner session unavailable: ${err.message.split("\n")[0]}`,
288
+ fix: "Re-run `acc login`, then restart the runner.",
289
+ };
290
+ }
291
+ if (!session) {
292
+ return {
293
+ name,
294
+ ok: false,
295
+ detail: "runner session unavailable",
296
+ fix: "Run `acc login`, then restart the runner.",
297
+ };
298
+ }
299
+ const probe = deps.probe ?? ((args) => probeMcpServer(args));
300
+ const result = await probe({
301
+ cwd: cfg.repoPath,
302
+ taskId: "",
303
+ runnerId: session.runner_id,
304
+ accessToken: session.access_token,
305
+ publicUrl: cfg.publicUrl,
306
+ supabaseUrl: cfg.supabaseUrl,
307
+ supabaseAnonKey: cfg.supabaseAnonKey,
308
+ });
309
+ if (!result.ok) {
310
+ return {
311
+ name,
312
+ ok: false,
313
+ detail: `${result.command} healthcheck failed: ${result.detail}`,
314
+ fix: "Install the current `@tokenfactory/acc-mcp-server`, then restart every `acc-runner@*.service` instance.",
315
+ };
316
+ }
317
+ return {
318
+ name,
319
+ ok: true,
320
+ detail: `${result.detail} (${result.command})`,
321
+ };
322
+ }
245
323
  /**
246
324
  * M4 (ENGINE-CODEX): per-engine validation for the codex engine — binary in
247
325
  * PATH + a usable auth channel. Skipped-green when the `engines.codex_enabled`
@@ -253,8 +331,28 @@ export async function checkChatAuth(probe = () => claudeCodeChatEngine.credentia
253
331
  */
254
332
  export async function checkCodexEngine() {
255
333
  const name = "codex engine";
256
- if (!codexEngineEnabled()) {
257
- return { name, ok: true, detail: "disabled (engines.codex_enabled=off)" };
334
+ // M5 (CODEX-DISPATCH-WIRE-1): gate on the ACTUAL dispatch registry, not just
335
+ // the raw flag. `engineRegistrationState` is the same resolution
336
+ // `getEngine("codex")` performs on dispatch, so doctor's verdict matches what
337
+ // a codex-pinned task actually gets — binary + login are necessary but not
338
+ // sufficient; the engine must be REGISTERED for the runner to serve it.
339
+ const regState = engineRegistrationState("codex");
340
+ if (regState === "flag_disabled") {
341
+ return {
342
+ name,
343
+ ok: true,
344
+ detail: "disabled (ACC_ENGINE_CODEX_ENABLED off — not registered for dispatch)",
345
+ };
346
+ }
347
+ if (regState === "unknown") {
348
+ // codex is a KNOWN batch engine, so this is only reachable if the registry
349
+ // build dropped it entirely — fail loudly rather than skip green.
350
+ return {
351
+ name,
352
+ ok: false,
353
+ detail: "codex is not present in this runner's engine registry build",
354
+ fix: "Rebuild/reinstall @tokenfactory/acc-runner; the codex engine is missing from the registry.",
355
+ };
258
356
  }
259
357
  const bin = await checkBinary("codex", ["--version"]);
260
358
  if (!bin.ok) {
@@ -277,25 +375,77 @@ export async function checkCodexEngine() {
277
375
  fix: "Set OPENAI_API_KEY (env or keychain) for the api-key channel, or use ACC_CODEX_AUTH_PREFERENCE=subscription_first with a `codex login` session.",
278
376
  };
279
377
  }
378
+ if (mode === "subscription") {
379
+ const auth = await checkBinary("codex", ["login", "status"]);
380
+ if (!auth.ok) {
381
+ return {
382
+ name,
383
+ ok: false,
384
+ detail: `${bin.detail}; subscription login unusable: ${auth.detail}`,
385
+ fix: "Run `codex login`, then re-run `acc doctor`.",
386
+ };
387
+ }
388
+ return {
389
+ name,
390
+ ok: true,
391
+ detail: `registered for dispatch; ${bin.detail}; ${describeCodexAuthMode(mode)}; ${auth.detail}`,
392
+ };
393
+ }
280
394
  return {
281
395
  name,
282
396
  ok: true,
283
- detail: `${bin.detail}; ${describeCodexAuthMode(mode)}; OPENAI_API_KEY ${keyPresent ? "present" : "absent (subscription)"}`,
397
+ detail: `registered for dispatch; ${bin.detail}; ${describeCodexAuthMode(mode)}; OPENAI_API_KEY ${keyPresent ? "present" : "absent (subscription)"}`,
284
398
  };
285
399
  }
286
400
  export async function detectCapableEngines(deps = {}) {
287
401
  const claudeProbe = deps.claudeProbe ?? (() => claudeCodeEngine.healthProbe());
288
402
  const codexEnabled = deps.codexEnabled ?? codexEngineEnabled;
289
403
  const codexProbe = deps.codexProbe ?? checkCodexEngine;
404
+ const logRejected = deps.logRejected ?? ((line) => process.stderr.write(`${line}\n`));
290
405
  const engines = [];
291
- if ((await claudeProbe()).ok)
406
+ const claude = await claudeProbe();
407
+ if (claude.ok) {
292
408
  engines.push(claudeCodeEngine.id);
409
+ }
410
+ else {
411
+ const detail = safeText(claude.detail, 300) ?? "probe failed";
412
+ logRejected(`[acc-runner] engine rejected: ${claudeCodeEngine.id}: ${detail}`);
413
+ }
293
414
  if (codexEnabled()) {
294
- if ((await codexProbe()).ok)
415
+ const codex = await codexProbe();
416
+ if (codex.ok) {
295
417
  engines.push("codex");
418
+ }
419
+ else {
420
+ const detail = safeText(codex.detail, 300) ?? "probe failed";
421
+ logRejected(`[acc-runner] engine rejected: codex: ${detail}`);
422
+ }
296
423
  }
297
424
  return engines;
298
425
  }
426
+ /**
427
+ * CAPS-ENGINES-STALE-KEYCHAIN-1 (AC5): surface WHY an enabled engine is or isn't
428
+ * advertised, on doctor's DEFAULT reporting path — so an operator sees the same
429
+ * rejection reason detectCapableEngines uses, without reading source. Runs that
430
+ * exact detection and folds every rejection line into the row's detail. `detect`
431
+ * is injectable so the reason is testable without the real binaries.
432
+ */
433
+ export async function checkEngineDetection(detect = detectCapableEngines) {
434
+ const name = "engine detection";
435
+ const rejected = [];
436
+ const advertised = await detect({ logRejected: (line) => rejected.push(line) });
437
+ const adv = advertised.length ? advertised.join(", ") : "(default engine only)";
438
+ if (rejected.length === 0) {
439
+ return { name, ok: true, detail: `advertised: ${adv}` };
440
+ }
441
+ return {
442
+ name,
443
+ ok: false,
444
+ warn: true,
445
+ detail: `advertised: ${adv}; rejected — ${rejected.join(" ; ")}`,
446
+ fix: "An enabled engine failed its probe (reason above); fix its auth/binary or disable it.",
447
+ };
448
+ }
299
449
  async function checkGh() {
300
450
  const ghOk = await checkBinary("gh", ["--version"]);
301
451
  if (!ghOk.ok) {
@@ -399,11 +549,22 @@ export async function checkInstance() {
399
549
  }
400
550
  catch (err) {
401
551
  const detail = err.message.split("\n")[0];
402
- return { name, ok: false, detail, fix: `Fix ${INSTANCE_ENV} / --instance to match ${INSTANCE_NAME_RE.source}.` };
552
+ return {
553
+ name,
554
+ ok: false,
555
+ detail,
556
+ fix: `Fix ${INSTANCE_ENV} / --instance to match ${INSTANCE_NAME_RE.source}.`,
557
+ };
403
558
  }
404
559
  const slot = resolveTokenMode() === "env" ? "n/a (token mode: env)" : `"${instanceKeychainAccount()}"`;
405
- const runner = await getTokenProvider().load().then((s) => s?.runner_id ?? "no session", () => "unreadable");
406
- return { name, ok: true, detail: `${instance ?? "default (unnamed)"}, keychain slot ${slot}, runner ${runner}` };
560
+ const runner = await getTokenProvider()
561
+ .load()
562
+ .then((s) => s?.runner_id ?? "no session", () => "unreadable");
563
+ return {
564
+ name,
565
+ ok: true,
566
+ detail: `${instance ?? "default (unnamed)"}, keychain slot ${slot}, runner ${runner}`,
567
+ };
407
568
  }
408
569
  async function checkKeychain() {
409
570
  // v0.12.0 (T-52-7): env-token runners (containers) have no OS keychain;
@@ -493,9 +654,7 @@ export async function checkEnvToken() {
493
654
  // confirm spare credentials are wired before relying on failover. A single-
494
655
  // credential runner reports "no fleet (single credential)".
495
656
  const fleetSize = provider.fleetSize();
496
- const fleet = fleetSize > 1
497
- ? `fleet of ${fleetSize} (failover armed)`
498
- : "no fleet (single credential)";
657
+ const fleet = fleetSize > 1 ? `fleet of ${fleetSize} (failover armed)` : "no fleet (single credential)";
499
658
  const expMs = Date.parse(session.access_expires_at);
500
659
  if (Number.isFinite(expMs) && expMs <= Date.now()) {
501
660
  return {
@@ -511,6 +670,19 @@ export async function checkEnvToken() {
511
670
  detail: `runner ${session.runner_id} (id: ${idSource}), ${refresh}, ${fleet}, expires ${session.access_expires_at}`,
512
671
  };
513
672
  }
673
+ export async function checkCredentialIdentity() {
674
+ // Resolve the SERVING model-credential account (BYOK key → null email;
675
+ // config-file or OS-keychain login → the account it carries), NOT the ACC
676
+ // runner session. Only the safe email/org/source labels are surfaced — never
677
+ // the underlying token material.
678
+ const id = await resolveCredentialIdentity();
679
+ return {
680
+ name: "model credential identity",
681
+ ok: true,
682
+ detail: `email=${id.account_email ?? "unknown"}, org=${id.account_org ?? "unknown"}, ` +
683
+ `source=${id.credential_source}`,
684
+ };
685
+ }
514
686
  /**
515
687
  * RD-1 (BOOTSTRAP-CONFIG): best-effort load of the persisted keychain / env
516
688
  * session so the doctor can show WHERE the Supabase config resolved from and
@@ -640,7 +812,10 @@ async function checkOrphanWorktrees(cfg) {
640
812
  if (orphans.length === 0) {
641
813
  return { name: "no orphan worktrees", ok: true, detail: "clean" };
642
814
  }
643
- const first = orphans.slice(0, 3).map((o) => o.taskId).join(", ");
815
+ const first = orphans
816
+ .slice(0, 3)
817
+ .map((o) => o.taskId)
818
+ .join(", ");
644
819
  const more = orphans.length > 3 ? ` (+${orphans.length - 3} more)` : "";
645
820
  return {
646
821
  name: "no orphan worktrees",
@@ -706,7 +881,7 @@ async function checkQuarantine() {
706
881
  name: "runner quarantine",
707
882
  ok: false,
708
883
  detail: `QUARANTINED (${q.cause}): ${q.detail} — task: ${q.taskId}, at: ${q.classifiedAt}${consec}`,
709
- fix: `Resolve the underlying ${q.cause} issue then delete ${quarantineFile()} or run \`acc-runner quarantine clear\`.`,
884
+ fix: `Resolve the underlying ${q.cause} issue, then run \`acc-runner quarantine clear\` for this named instance (sentinel: ${quarantineFile()}); repeat for every instance on the host. Its running watcher resumes on the next recovery tick without a restart.`,
710
885
  };
711
886
  }
712
887
  catch (err) {
@@ -752,7 +927,10 @@ async function checkStaleLocks() {
752
927
  if (stale.length === 0) {
753
928
  return { name: "no stale task locks", ok: true, detail: "clean" };
754
929
  }
755
- const first = stale.slice(0, 3).map((l) => l.taskId).join(", ");
930
+ const first = stale
931
+ .slice(0, 3)
932
+ .map((l) => l.taskId)
933
+ .join(", ");
756
934
  const more = stale.length > 3 ? ` (+${stale.length - 3} more)` : "";
757
935
  return {
758
936
  name: "no stale task locks",
@@ -863,7 +1041,9 @@ export async function checkSelfUpdateOn426(cfg) {
863
1041
  const enabled = cfg?.selfUpdateOn426 === true;
864
1042
  const attempt = await latestSelfUpdateAttempt().catch(() => null);
865
1043
  const last = attempt
866
- ? `; last attempt v${attempt.target} ${attempt.record.ok ? "ok" : `failed${attempt.record.error ? ` (${attempt.record.error})` : ""}`} at ${attempt.record.attemptedAt}`
1044
+ ? `; last attempt v${attempt.target} ${attempt.record.ok
1045
+ ? "ok"
1046
+ : `failed${attempt.record.error ? ` (${attempt.record.error})` : ""}`} at ${attempt.record.attemptedAt}`
867
1047
  : "";
868
1048
  return {
869
1049
  name: "self-update on 426",
@@ -936,9 +1116,7 @@ async function checkRegistration() {
936
1116
  name: "runner registered",
937
1117
  ok: false,
938
1118
  detail: provider.mode === "env" ? "no session in env" : "no session in keychain",
939
- fix: provider.mode === "env"
940
- ? "Set ACC_RUNNER_ACCESS_TOKEN."
941
- : "Run `acc-runner login`.",
1119
+ fix: provider.mode === "env" ? "Set ACC_RUNNER_ACCESS_TOKEN." : "Run `acc-runner login`.",
942
1120
  };
943
1121
  }
944
1122
  try {
@@ -974,7 +1152,11 @@ async function checkRegistration() {
974
1152
  fix: "Run `acc-runner logout` then `acc-runner login` again.",
975
1153
  };
976
1154
  }
977
- return { name: "runner registered", ok: true, detail: `${session.runner_id} (status=${data.status})` };
1155
+ return {
1156
+ name: "runner registered",
1157
+ ok: true,
1158
+ detail: `${session.runner_id} (status=${data.status})`,
1159
+ };
978
1160
  }
979
1161
  catch (err) {
980
1162
  return {
@@ -1011,7 +1193,11 @@ export async function checkConcurrency(cfg) {
1011
1193
  envExplicit,
1012
1194
  });
1013
1195
  if (!cfg) {
1014
- return { name, ok: true, detail: `effective ${localEff.concurrency} (source=${localEff.source}), reserve ${localEff.reservedReviewSlots} — config unavailable (skipped)` };
1196
+ return {
1197
+ name,
1198
+ ok: true,
1199
+ detail: `effective ${localEff.concurrency} (source=${localEff.source}), reserve ${localEff.reservedReviewSlots} — config unavailable (skipped)`,
1200
+ };
1015
1201
  }
1016
1202
  const provider = getTokenProvider();
1017
1203
  let session;
@@ -1022,7 +1208,11 @@ export async function checkConcurrency(cfg) {
1022
1208
  session = null;
1023
1209
  }
1024
1210
  if (!session) {
1025
- return { name, ok: true, detail: `effective ${localEff.concurrency} (source=${localEff.source}), reserve ${localEff.reservedReviewSlots} — not registered (skipped)` };
1211
+ return {
1212
+ name,
1213
+ ok: true,
1214
+ detail: `effective ${localEff.concurrency} (source=${localEff.source}), reserve ${localEff.reservedReviewSlots} — not registered (skipped)`,
1215
+ };
1026
1216
  }
1027
1217
  let row = null;
1028
1218
  try {
@@ -1034,12 +1224,20 @@ export async function checkConcurrency(cfg) {
1034
1224
  .eq("id", session.runner_id)
1035
1225
  .maybeSingle();
1036
1226
  if (error) {
1037
- return { name, ok: true, detail: `effective ${localEff.concurrency} (source=${localEff.source}) — server row unreadable (skipped): ${error.message.split("\n")[0]}` };
1227
+ return {
1228
+ name,
1229
+ ok: true,
1230
+ detail: `effective ${localEff.concurrency} (source=${localEff.source}) — server row unreadable (skipped): ${error.message.split("\n")[0]}`,
1231
+ };
1038
1232
  }
1039
1233
  row = (data ?? null);
1040
1234
  }
1041
1235
  catch (err) {
1042
- return { name, ok: true, detail: `effective ${localEff.concurrency} (source=${localEff.source}) — server read failed (skipped): ${err.message.split("\n")[0]}` };
1236
+ return {
1237
+ name,
1238
+ ok: true,
1239
+ detail: `effective ${localEff.concurrency} (source=${localEff.source}) — server read failed (skipped): ${err.message.split("\n")[0]}`,
1240
+ };
1043
1241
  }
1044
1242
  const eff = resolveEffectiveConcurrency({
1045
1243
  serverConcurrency: row?.desired_concurrency ?? null,
@@ -1117,12 +1315,20 @@ export async function checkRepoAssignments(cfg) {
1117
1315
  .eq("active", true)
1118
1316
  .eq("enforced", true);
1119
1317
  if (error) {
1120
- return { name, ok: true, detail: `no visible assignments (skipped): ${error.message.split("\n")[0]}` };
1318
+ return {
1319
+ name,
1320
+ ok: true,
1321
+ detail: `no visible assignments (skipped): ${error.message.split("\n")[0]}`,
1322
+ };
1121
1323
  }
1122
1324
  rows = (data ?? []);
1123
1325
  }
1124
1326
  catch (err) {
1125
- return { name, ok: true, detail: `assignment read failed (skipped): ${err.message.split("\n")[0]}` };
1327
+ return {
1328
+ name,
1329
+ ok: true,
1330
+ detail: `assignment read failed (skipped): ${err.message.split("\n")[0]}`,
1331
+ };
1126
1332
  }
1127
1333
  if (rows.length === 0) {
1128
1334
  return { name, ok: true, detail: "no enforced DB assignments visible" };
@@ -1242,6 +1448,38 @@ function format(outcome, strict = false) {
1242
1448
  const fix = !outcome.ok && outcome.fix ? chalk.gray(`\n → ${outcome.fix}`) : "";
1243
1449
  return `${tag} ${outcome.name.padEnd(28, " ")} ${chalk.gray(outcome.detail)}${fix}`;
1244
1450
  }
1451
+ /**
1452
+ * BUILD-FINGERPRINT-RUNNING-1 (AC4): compare the build the long-lived
1453
+ * `watch`/companion process recorded at boot against the build THIS `acc doctor`
1454
+ * process loaded (=== the installed on-disk build). Answers the operator's real
1455
+ * question — "did the launchd process pick up the fix I just installed?" —
1456
+ * WITHOUT needing any symbol name from the diff, the check the incident had to
1457
+ * invent by grepping dist. A stale process is advisory (WARN), not a hard
1458
+ * environment failure; `--strict` promotes it like every other WARN. No record
1459
+ * (nothing long-lived has booted since the feature shipped) is not a problem.
1460
+ */
1461
+ export async function checkRunningBuild(read = () => readRunningBuild()) {
1462
+ const name = "Running build";
1463
+ const status = runningBuildStatus(await read());
1464
+ if (status.noRunningRecord) {
1465
+ return {
1466
+ name,
1467
+ ok: true,
1468
+ detail: "no running record — start the runner (`watch`/companion) to record it",
1469
+ };
1470
+ }
1471
+ if (status.restart_required) {
1472
+ return {
1473
+ name,
1474
+ ok: false,
1475
+ warn: true,
1476
+ detail: `STALE — process is running ${status.running?.dist_sha256} (${status.running?.commit_sha}) ` +
1477
+ `but the installed build is ${status.installed.dist_sha256} (${status.installed.commit_sha})`,
1478
+ fix: "Restart the runner (`watch`/companion) process to load the installed build.",
1479
+ };
1480
+ }
1481
+ return { name, ok: true, detail: "up to date (running the installed build)" };
1482
+ }
1245
1483
  export async function doctorCommand(options = {}) {
1246
1484
  const rawCfg = loadConfigOrNull();
1247
1485
  // RD-1: overlay the persisted device-code bootstrap so the displayed config
@@ -1253,13 +1491,16 @@ export async function doctorCommand(options = {}) {
1253
1491
  checkClaude,
1254
1492
  checkAnthropicKey,
1255
1493
  checkChatAuth,
1494
+ () => checkMcpServer(cfg),
1256
1495
  checkCodexEngine,
1496
+ checkEngineDetection,
1257
1497
  checkGh,
1258
1498
  checkGit,
1259
1499
  () => checkGitAuth(cfg),
1260
1500
  checkKeychain,
1261
1501
  checkInstance,
1262
1502
  checkEnvToken,
1503
+ checkCredentialIdentity,
1263
1504
  () => checkBootstrapConfig(bootstrap),
1264
1505
  ...(cfg ? [() => checkServer(cfg.publicUrl)] : []),
1265
1506
  ...(cfg ? [() => checkProtocolVersion(cfg.publicUrl)] : []),
@@ -1278,6 +1519,7 @@ export async function doctorCommand(options = {}) {
1278
1519
  checkClaudeDrift,
1279
1520
  checkCaffeinate,
1280
1521
  checkLaunchd,
1522
+ checkRunningBuild,
1281
1523
  ];
1282
1524
  const strict = options.strict === true;
1283
1525
  if (cfg && !options.checks) {
@@ -1322,9 +1564,7 @@ export async function doctorCommand(options = {}) {
1322
1564
  console.log(chalk.green(`✓ All checks passed${suffix}.`));
1323
1565
  }
1324
1566
  else {
1325
- const warnNote = warned > 0 && !strict
1326
- ? `, ${warned} advisory warning${warned === 1 ? "" : "s"}`
1327
- : "";
1567
+ const warnNote = warned > 0 && !strict ? `, ${warned} advisory warning${warned === 1 ? "" : "s"}` : "";
1328
1568
  console.log(chalk.red(`✗ ${failed} check${failed === 1 ? "" : "s"} failed${warnNote}.`));
1329
1569
  }
1330
1570
  }