@tokenfactory/acc-runner 0.44.0 → 0.44.2

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 (197) hide show
  1. package/README.md +43 -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 +91 -0
  43. package/dist/cost-pricing.d.ts.map +1 -1
  44. package/dist/cost-pricing.js +115 -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 +37 -1
  51. package/dist/doctor.d.ts.map +1 -1
  52. package/dist/doctor.js +300 -41
  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/instance.d.ts +30 -0
  87. package/dist/instance.d.ts.map +1 -0
  88. package/dist/instance.js +51 -0
  89. package/dist/instance.js.map +1 -0
  90. package/dist/keychain.d.ts +20 -7
  91. package/dist/keychain.d.ts.map +1 -1
  92. package/dist/keychain.js +40 -4
  93. package/dist/keychain.js.map +1 -1
  94. package/dist/loc-advisory.d.ts +70 -0
  95. package/dist/loc-advisory.d.ts.map +1 -0
  96. package/dist/loc-advisory.js +196 -0
  97. package/dist/loc-advisory.js.map +1 -0
  98. package/dist/locks/claim-lock-policy.d.ts +67 -0
  99. package/dist/locks/claim-lock-policy.d.ts.map +1 -0
  100. package/dist/locks/claim-lock-policy.js +132 -0
  101. package/dist/locks/claim-lock-policy.js.map +1 -0
  102. package/dist/login.d.ts +63 -2
  103. package/dist/login.d.ts.map +1 -1
  104. package/dist/login.js +93 -8
  105. package/dist/login.js.map +1 -1
  106. package/dist/mcp-spawn.d.ts +54 -0
  107. package/dist/mcp-spawn.d.ts.map +1 -1
  108. package/dist/mcp-spawn.js +88 -4
  109. package/dist/mcp-spawn.js.map +1 -1
  110. package/dist/memory-context.d.ts +106 -0
  111. package/dist/memory-context.d.ts.map +1 -0
  112. package/dist/memory-context.js +220 -0
  113. package/dist/memory-context.js.map +1 -0
  114. package/dist/memory-curation.d.ts +172 -0
  115. package/dist/memory-curation.d.ts.map +1 -0
  116. package/dist/memory-curation.js +277 -0
  117. package/dist/memory-curation.js.map +1 -0
  118. package/dist/migration-seq-guard.d.ts +28 -1
  119. package/dist/migration-seq-guard.d.ts.map +1 -1
  120. package/dist/migration-seq-guard.js +111 -6
  121. package/dist/migration-seq-guard.js.map +1 -1
  122. package/dist/output-diagnostics.d.ts +15 -0
  123. package/dist/output-diagnostics.d.ts.map +1 -0
  124. package/dist/output-diagnostics.js +95 -0
  125. package/dist/output-diagnostics.js.map +1 -0
  126. package/dist/pr-open-guard.d.ts +67 -0
  127. package/dist/pr-open-guard.d.ts.map +1 -0
  128. package/dist/pr-open-guard.js +79 -0
  129. package/dist/pr-open-guard.js.map +1 -0
  130. package/dist/prepr-gate.d.ts +229 -1
  131. package/dist/prepr-gate.d.ts.map +1 -1
  132. package/dist/prepr-gate.js +406 -3
  133. package/dist/prepr-gate.js.map +1 -1
  134. package/dist/program.d.ts.map +1 -1
  135. package/dist/program.js +43 -5
  136. package/dist/program.js.map +1 -1
  137. package/dist/provider-auth.d.ts +27 -0
  138. package/dist/provider-auth.d.ts.map +1 -1
  139. package/dist/provider-auth.js +33 -0
  140. package/dist/provider-auth.js.map +1 -1
  141. package/dist/runtime/cache-dir.d.ts.map +1 -1
  142. package/dist/runtime/cache-dir.js +8 -3
  143. package/dist/runtime/cache-dir.js.map +1 -1
  144. package/dist/runtime/decision-provenance.d.ts +63 -0
  145. package/dist/runtime/decision-provenance.d.ts.map +1 -0
  146. package/dist/runtime/decision-provenance.js +107 -0
  147. package/dist/runtime/decision-provenance.js.map +1 -0
  148. package/dist/runtime/prompt-substitute.d.ts +100 -0
  149. package/dist/runtime/prompt-substitute.d.ts.map +1 -0
  150. package/dist/runtime/prompt-substitute.js +124 -0
  151. package/dist/runtime/prompt-substitute.js.map +1 -0
  152. package/dist/runtime/quarantine.d.ts +11 -0
  153. package/dist/runtime/quarantine.d.ts.map +1 -1
  154. package/dist/runtime/quarantine.js +16 -1
  155. package/dist/runtime/quarantine.js.map +1 -1
  156. package/dist/runtime/review-cost-cap.d.ts +167 -0
  157. package/dist/runtime/review-cost-cap.d.ts.map +1 -0
  158. package/dist/runtime/review-cost-cap.js +182 -0
  159. package/dist/runtime/review-cost-cap.js.map +1 -0
  160. package/dist/runtime/reviewer-abstain.d.ts +101 -0
  161. package/dist/runtime/reviewer-abstain.d.ts.map +1 -0
  162. package/dist/runtime/reviewer-abstain.js +138 -0
  163. package/dist/runtime/reviewer-abstain.js.map +1 -0
  164. package/dist/runtime/reviewer.d.ts +165 -2
  165. package/dist/runtime/reviewer.d.ts.map +1 -1
  166. package/dist/runtime/reviewer.js +752 -111
  167. package/dist/runtime/reviewer.js.map +1 -1
  168. package/dist/runtime/running-build.d.ts +24 -0
  169. package/dist/runtime/running-build.d.ts.map +1 -0
  170. package/dist/runtime/running-build.js +62 -0
  171. package/dist/runtime/running-build.js.map +1 -0
  172. package/dist/runtime/singleton.d.ts.map +1 -1
  173. package/dist/runtime/singleton.js +9 -1
  174. package/dist/runtime/singleton.js.map +1 -1
  175. package/dist/runtime/worktree.d.ts +45 -0
  176. package/dist/runtime/worktree.d.ts.map +1 -1
  177. package/dist/runtime/worktree.js +174 -5
  178. package/dist/runtime/worktree.js.map +1 -1
  179. package/dist/serving-auth/index.d.ts +79 -0
  180. package/dist/serving-auth/index.d.ts.map +1 -0
  181. package/dist/serving-auth/index.js +169 -0
  182. package/dist/serving-auth/index.js.map +1 -0
  183. package/dist/task-runner.d.ts +134 -5
  184. package/dist/task-runner.d.ts.map +1 -1
  185. package/dist/task-runner.js +1255 -129
  186. package/dist/task-runner.js.map +1 -1
  187. package/dist/types.d.ts +8 -0
  188. package/dist/types.d.ts.map +1 -1
  189. package/dist/watch-chat/wire.d.ts +80 -0
  190. package/dist/watch-chat/wire.d.ts.map +1 -1
  191. package/dist/watch-chat/wire.js +203 -43
  192. package/dist/watch-chat/wire.js.map +1 -1
  193. package/dist/watch.d.ts +70 -16
  194. package/dist/watch.d.ts.map +1 -1
  195. package/dist/watch.js +548 -108
  196. package/dist/watch.js.map +1 -1
  197. package/package.json +4 -3
package/dist/doctor.js CHANGED
@@ -8,18 +8,20 @@ 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";
24
+ import { INSTANCE_ENV, INSTANCE_NAME_RE, instanceKeychainAccount, resolveInstance, } from "./instance.js";
23
25
  import { checkVersion } from "./version-check.js";
24
26
  import { PROTOCOL_VERSION } from "./types.js";
25
27
  import { locksDir, listStaleLocks } from "./runtime/locks.js";
@@ -32,6 +34,10 @@ import { getClaudeVersion, npmLatestVersion, readTaskEnv, } from "./runtime/vers
32
34
  import { compareSemver as compareSemverVer } from "./version-check.js";
33
35
  import { latestSelfUpdateAttempt } from "./runtime/self-upgrade.js";
34
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";
35
41
  export const LAUNCHD_LABEL = "com.tokenfactory.acc-runner";
36
42
  export function launchdPlistPath() {
37
43
  return path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
@@ -75,7 +81,7 @@ function maskKey(s) {
75
81
  function formatConfig(cfg, bootstrap) {
76
82
  const src = configSources(bootstrap);
77
83
  const lines = Object.keys(CONFIG_LABELS).map((k) => {
78
- // v0.41-B: worktreeRepoPath is optional at the config layer
84
+ // worktreeRepoPath is optional at the config layer
79
85
  // (undefined means "use repoPath"). Render "<not set>" rather than
80
86
  // the literal string "undefined" so the doctor output is clean.
81
87
  const raw = cfg[k];
@@ -130,9 +136,26 @@ function formatRuntime() {
130
136
  ];
131
137
  return [chalk.bold("Runtime:"), ...lines].join("\n");
132
138
  }
133
- 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
+ }
134
157
  try {
135
- const { stdout } = await execa(bin, args, { env: buildEnv() });
158
+ const { stdout } = await execa(command, args, { env });
136
159
  const first = stdout.split(/\r?\n/)[0]?.trim() ?? bin;
137
160
  return { ok: true, detail: first };
138
161
  }
@@ -149,8 +172,8 @@ async function checkNode() {
149
172
  fix: "Install Node 20.6+ (https://nodejs.org).",
150
173
  };
151
174
  }
152
- async function checkClaude() {
153
- const r = await checkBinary("claude", ["--version"]);
175
+ export async function checkClaude(resolveOptions = {}) {
176
+ const r = await checkBinary("claude", ["--version"], resolveOptions);
154
177
  return {
155
178
  name: "claude CLI in PATH",
156
179
  ok: r.ok,
@@ -177,9 +200,7 @@ async function checkAnthropicKey() {
177
200
  return {
178
201
  name,
179
202
  ok: true,
180
- detail: hint
181
- ? `${describeServingAuthKind(kind)} — ${hint}`
182
- : describeServingAuthKind(kind),
203
+ detail: hint ? `${describeServingAuthKind(kind)} — ${hint}` : describeServingAuthKind(kind),
183
204
  };
184
205
  }
185
206
  return {
@@ -218,16 +239,23 @@ async function checkAnthropicKey() {
218
239
  * Secrets are never printed (the probe detail redacts key material). The probe
219
240
  * is injectable so the doctor test exercises the rendering without live gh/auth.
220
241
  */
221
- 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";
222
245
  const name = "chat lane credentials";
223
- const result = await probe();
246
+ const result = await (probe ?? (() => engine.credentialProbe()))();
224
247
  if (!result.ok) {
248
+ const login = selectedEngineId === "codex"
249
+ ? "codex login"
250
+ : selectedEngineId === "gemini"
251
+ ? "gemini"
252
+ : "claude login";
225
253
  return {
226
254
  name,
227
255
  ok: false,
228
256
  warn: true,
229
- detail: `chat lane unavailable — ${result.detail}`,
230
- 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.`,
231
259
  };
232
260
  }
233
261
  const draftOnly = result.tier === "draft-only";
@@ -237,10 +265,61 @@ export async function checkChatAuth(probe = () => claudeCodeChatEngine.credentia
237
265
  warn: draftOnly ? true : undefined,
238
266
  detail: `tier=${result.tier} (${result.channel}); ${result.detail}`,
239
267
  ...(draftOnly
240
- ? { 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
+ }
241
271
  : {}),
242
272
  };
243
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
+ }
244
323
  /**
245
324
  * M4 (ENGINE-CODEX): per-engine validation for the codex engine — binary in
246
325
  * PATH + a usable auth channel. Skipped-green when the `engines.codex_enabled`
@@ -252,8 +331,28 @@ export async function checkChatAuth(probe = () => claudeCodeChatEngine.credentia
252
331
  */
253
332
  export async function checkCodexEngine() {
254
333
  const name = "codex engine";
255
- if (!codexEngineEnabled()) {
256
- 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
+ };
257
356
  }
258
357
  const bin = await checkBinary("codex", ["--version"]);
259
358
  if (!bin.ok) {
@@ -276,25 +375,77 @@ export async function checkCodexEngine() {
276
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.",
277
376
  };
278
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
+ }
279
394
  return {
280
395
  name,
281
396
  ok: true,
282
- 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)"}`,
283
398
  };
284
399
  }
285
400
  export async function detectCapableEngines(deps = {}) {
286
401
  const claudeProbe = deps.claudeProbe ?? (() => claudeCodeEngine.healthProbe());
287
402
  const codexEnabled = deps.codexEnabled ?? codexEngineEnabled;
288
403
  const codexProbe = deps.codexProbe ?? checkCodexEngine;
404
+ const logRejected = deps.logRejected ?? ((line) => process.stderr.write(`${line}\n`));
289
405
  const engines = [];
290
- if ((await claudeProbe()).ok)
406
+ const claude = await claudeProbe();
407
+ if (claude.ok) {
291
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
+ }
292
414
  if (codexEnabled()) {
293
- if ((await codexProbe()).ok)
415
+ const codex = await codexProbe();
416
+ if (codex.ok) {
294
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
+ }
295
423
  }
296
424
  return engines;
297
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
+ }
298
449
  async function checkGh() {
299
450
  const ghOk = await checkBinary("gh", ["--version"]);
300
451
  if (!ghOk.ok) {
@@ -387,6 +538,34 @@ export async function checkGitAuth(cfg, probeFactory = (repoPath) => makeGitAuth
387
538
  fix: "Network/remote hiccup — re-run `doctor`; if it persists, check connectivity to GitHub.",
388
539
  };
389
540
  }
541
+ /** NAMED-INSTANCES-1: names what diverges per instance — the name, its
542
+ * keychain slot, and the runner id that slot resolved to. Always OK: unnamed
543
+ * is normal, and a missing session already fails `runner registered`. */
544
+ export async function checkInstance() {
545
+ const name = "runner instance";
546
+ let instance;
547
+ try {
548
+ instance = resolveInstance();
549
+ }
550
+ catch (err) {
551
+ const detail = err.message.split("\n")[0];
552
+ return {
553
+ name,
554
+ ok: false,
555
+ detail,
556
+ fix: `Fix ${INSTANCE_ENV} / --instance to match ${INSTANCE_NAME_RE.source}.`,
557
+ };
558
+ }
559
+ const slot = resolveTokenMode() === "env" ? "n/a (token mode: env)" : `"${instanceKeychainAccount()}"`;
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
+ };
568
+ }
390
569
  async function checkKeychain() {
391
570
  // v0.12.0 (T-52-7): env-token runners (containers) have no OS keychain;
392
571
  // the check is inapplicable rather than broken.
@@ -475,9 +654,7 @@ export async function checkEnvToken() {
475
654
  // confirm spare credentials are wired before relying on failover. A single-
476
655
  // credential runner reports "no fleet (single credential)".
477
656
  const fleetSize = provider.fleetSize();
478
- const fleet = fleetSize > 1
479
- ? `fleet of ${fleetSize} (failover armed)`
480
- : "no fleet (single credential)";
657
+ const fleet = fleetSize > 1 ? `fleet of ${fleetSize} (failover armed)` : "no fleet (single credential)";
481
658
  const expMs = Date.parse(session.access_expires_at);
482
659
  if (Number.isFinite(expMs) && expMs <= Date.now()) {
483
660
  return {
@@ -493,6 +670,19 @@ export async function checkEnvToken() {
493
670
  detail: `runner ${session.runner_id} (id: ${idSource}), ${refresh}, ${fleet}, expires ${session.access_expires_at}`,
494
671
  };
495
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
+ }
496
686
  /**
497
687
  * RD-1 (BOOTSTRAP-CONFIG): best-effort load of the persisted keychain / env
498
688
  * session so the doctor can show WHERE the Supabase config resolved from and
@@ -622,7 +812,10 @@ async function checkOrphanWorktrees(cfg) {
622
812
  if (orphans.length === 0) {
623
813
  return { name: "no orphan worktrees", ok: true, detail: "clean" };
624
814
  }
625
- 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(", ");
626
819
  const more = orphans.length > 3 ? ` (+${orphans.length - 3} more)` : "";
627
820
  return {
628
821
  name: "no orphan worktrees",
@@ -688,7 +881,7 @@ async function checkQuarantine() {
688
881
  name: "runner quarantine",
689
882
  ok: false,
690
883
  detail: `QUARANTINED (${q.cause}): ${q.detail} — task: ${q.taskId}, at: ${q.classifiedAt}${consec}`,
691
- 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.`,
692
885
  };
693
886
  }
694
887
  catch (err) {
@@ -734,7 +927,10 @@ async function checkStaleLocks() {
734
927
  if (stale.length === 0) {
735
928
  return { name: "no stale task locks", ok: true, detail: "clean" };
736
929
  }
737
- 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(", ");
738
934
  const more = stale.length > 3 ? ` (+${stale.length - 3} more)` : "";
739
935
  return {
740
936
  name: "no stale task locks",
@@ -845,7 +1041,9 @@ export async function checkSelfUpdateOn426(cfg) {
845
1041
  const enabled = cfg?.selfUpdateOn426 === true;
846
1042
  const attempt = await latestSelfUpdateAttempt().catch(() => null);
847
1043
  const last = attempt
848
- ? `; 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}`
849
1047
  : "";
850
1048
  return {
851
1049
  name: "self-update on 426",
@@ -918,9 +1116,7 @@ async function checkRegistration() {
918
1116
  name: "runner registered",
919
1117
  ok: false,
920
1118
  detail: provider.mode === "env" ? "no session in env" : "no session in keychain",
921
- fix: provider.mode === "env"
922
- ? "Set ACC_RUNNER_ACCESS_TOKEN."
923
- : "Run `acc-runner login`.",
1119
+ fix: provider.mode === "env" ? "Set ACC_RUNNER_ACCESS_TOKEN." : "Run `acc-runner login`.",
924
1120
  };
925
1121
  }
926
1122
  try {
@@ -956,7 +1152,11 @@ async function checkRegistration() {
956
1152
  fix: "Run `acc-runner logout` then `acc-runner login` again.",
957
1153
  };
958
1154
  }
959
- 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
+ };
960
1160
  }
961
1161
  catch (err) {
962
1162
  return {
@@ -993,7 +1193,11 @@ export async function checkConcurrency(cfg) {
993
1193
  envExplicit,
994
1194
  });
995
1195
  if (!cfg) {
996
- 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
+ };
997
1201
  }
998
1202
  const provider = getTokenProvider();
999
1203
  let session;
@@ -1004,7 +1208,11 @@ export async function checkConcurrency(cfg) {
1004
1208
  session = null;
1005
1209
  }
1006
1210
  if (!session) {
1007
- 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
+ };
1008
1216
  }
1009
1217
  let row = null;
1010
1218
  try {
@@ -1016,12 +1224,20 @@ export async function checkConcurrency(cfg) {
1016
1224
  .eq("id", session.runner_id)
1017
1225
  .maybeSingle();
1018
1226
  if (error) {
1019
- 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
+ };
1020
1232
  }
1021
1233
  row = (data ?? null);
1022
1234
  }
1023
1235
  catch (err) {
1024
- 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
+ };
1025
1241
  }
1026
1242
  const eff = resolveEffectiveConcurrency({
1027
1243
  serverConcurrency: row?.desired_concurrency ?? null,
@@ -1099,12 +1315,20 @@ export async function checkRepoAssignments(cfg) {
1099
1315
  .eq("active", true)
1100
1316
  .eq("enforced", true);
1101
1317
  if (error) {
1102
- 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
+ };
1103
1323
  }
1104
1324
  rows = (data ?? []);
1105
1325
  }
1106
1326
  catch (err) {
1107
- 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
+ };
1108
1332
  }
1109
1333
  if (rows.length === 0) {
1110
1334
  return { name, ok: true, detail: "no enforced DB assignments visible" };
@@ -1224,6 +1448,38 @@ function format(outcome, strict = false) {
1224
1448
  const fix = !outcome.ok && outcome.fix ? chalk.gray(`\n → ${outcome.fix}`) : "";
1225
1449
  return `${tag} ${outcome.name.padEnd(28, " ")} ${chalk.gray(outcome.detail)}${fix}`;
1226
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
+ }
1227
1483
  export async function doctorCommand(options = {}) {
1228
1484
  const rawCfg = loadConfigOrNull();
1229
1485
  // RD-1: overlay the persisted device-code bootstrap so the displayed config
@@ -1235,12 +1491,16 @@ export async function doctorCommand(options = {}) {
1235
1491
  checkClaude,
1236
1492
  checkAnthropicKey,
1237
1493
  checkChatAuth,
1494
+ () => checkMcpServer(cfg),
1238
1495
  checkCodexEngine,
1496
+ checkEngineDetection,
1239
1497
  checkGh,
1240
1498
  checkGit,
1241
1499
  () => checkGitAuth(cfg),
1242
1500
  checkKeychain,
1501
+ checkInstance,
1243
1502
  checkEnvToken,
1503
+ checkCredentialIdentity,
1244
1504
  () => checkBootstrapConfig(bootstrap),
1245
1505
  ...(cfg ? [() => checkServer(cfg.publicUrl)] : []),
1246
1506
  ...(cfg ? [() => checkProtocolVersion(cfg.publicUrl)] : []),
@@ -1259,6 +1519,7 @@ export async function doctorCommand(options = {}) {
1259
1519
  checkClaudeDrift,
1260
1520
  checkCaffeinate,
1261
1521
  checkLaunchd,
1522
+ checkRunningBuild,
1262
1523
  ];
1263
1524
  const strict = options.strict === true;
1264
1525
  if (cfg && !options.checks) {
@@ -1303,9 +1564,7 @@ export async function doctorCommand(options = {}) {
1303
1564
  console.log(chalk.green(`✓ All checks passed${suffix}.`));
1304
1565
  }
1305
1566
  else {
1306
- const warnNote = warned > 0 && !strict
1307
- ? `, ${warned} advisory warning${warned === 1 ? "" : "s"}`
1308
- : "";
1567
+ const warnNote = warned > 0 && !strict ? `, ${warned} advisory warning${warned === 1 ? "" : "s"}` : "";
1309
1568
  console.log(chalk.red(`✗ ${failed} check${failed === 1 ? "" : "s"} failed${warnNote}.`));
1310
1569
  }
1311
1570
  }