@on-par/factory-cli 0.9.4 → 0.9.5-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ On PAR Dev's Software Factory is a local CLI that takes a GitHub issue through p
6
6
 
7
7
  Built in TypeScript. Routes work across Claude and Codex, with experimental local models through Ollama, and logs cost per task.
8
8
 
9
- ![On PAR Dev's Software Factory running a Sound Buddy lane (sped up)](https://raw.githubusercontent.com/on-par/software-factory/main/docs/assets/factory-run.gif)
9
+ ![On PAR Dev's Software Factory running a lane on a real product repo (sped up)](https://raw.githubusercontent.com/on-par/software-factory/main/docs/assets/factory-run.gif)
10
10
 
11
11
  ## Install
12
12
 
@@ -16,7 +16,7 @@ factory --version
16
16
  factory doctor
17
17
  ```
18
18
 
19
- Also requires `git`, one supported coding agent CLI (see [docs/agents.md](https://github.com/on-par/software-factory/blob/main/docs/agents.md)), signed in, and the GitHub CLI signed in with `gh auth login`. The factory reads `GITHUB_TOKEN`, then `GH_TOKEN`, and falls back to `gh auth token`. `factory doctor` checks all of this and prints a fix for anything missing. It also checks your git identity and the sandbox runtime, and it runs outside a repo.
19
+ Also requires `git`, one supported coding agent CLI (see [docs/agents.md](https://github.com/on-par/software-factory/blob/main/docs/agents.md)), signed in, and the GitHub CLI signed in with `gh auth login`. The factory reads `GITHUB_TOKEN`, then `GH_TOKEN`, and falls back to `gh auth token`. `factory doctor` checks all of this and prints a fix for anything missing. It also checks your git identity and the sandbox runtime, and it runs outside a repo. Global or repo-local `git config` for `user.name` and `user.email` both work. On Linux, install `firejail` first: without it, Codex runs with `--sandbox danger-full-access`.
20
20
 
21
21
  `factory init` creates a local-only `.factory/` (git-excluded) with a `default-code` constitution. See the main README quick start.
22
22
 
@@ -30,7 +30,7 @@ Agents run with their permission prompts off (Claude Code with `--permission-mod
30
30
  npm install -g @on-par/factory-cli@latest
31
31
  ```
32
32
 
33
- Automatic updates are planned: [#2354](https://github.com/on-par/software-factory/issues/2354), [#2355](https://github.com/on-par/software-factory/issues/2355).
33
+ `factory self-update --check` reports whether a newer release is on npm. It works in 0.9.3 and later. Automatic updates are planned: [#2355](https://github.com/on-par/software-factory/issues/2355), [#1939](https://github.com/on-par/software-factory/issues/1939).
34
34
 
35
35
  ## Documentation
36
36
 
@@ -1,4 +1,5 @@
1
1
  import type { DeclaredHarnessProbe } from '@on-par/factory-core';
2
+ import type { ConfigLintInput } from '@on-par/factory-core/internal';
2
3
  export interface DoctorCheck {
3
4
  name: string;
4
5
  ok: boolean;
@@ -18,6 +19,14 @@ export interface DoctorExecOptions {
18
19
  /** Kill the command after this many ms; the probe then returns null. */
19
20
  timeoutMs?: number;
20
21
  }
22
+ /** A declared, agentic command harness the repo's models.registry uses (#2691). */
23
+ export interface ConfiguredCommandHarness {
24
+ harness: string;
25
+ /** The declared `command` template, verbatim. */
26
+ command: string;
27
+ /** First token of `command` to look up on PATH; null when it cannot be parsed safely. */
28
+ executable: string | null;
29
+ }
21
30
  export interface DoctorEnvProbes {
22
31
  commandAvailable: (cmd: string) => boolean;
23
32
  envPresent: (key: string) => boolean;
@@ -36,12 +45,16 @@ export interface DoctorEnvProbes {
36
45
  claudeLoggedIn?: () => boolean;
37
46
  /** Whether a stored opencode auth login exists (#2556); reads local state only, never runs inference. */
38
47
  opencodeLoggedIn?: () => boolean;
48
+ /** Repo-configured command harnesses (#2691); absent or [] means none. */
49
+ commandHarnesses?: () => ConfiguredCommandHarness[];
39
50
  }
51
+ /** Section for rows about repo-configured command harnesses (#2691). */
52
+ export declare const COMMAND_HARNESS_SECTION = "Command harness";
40
53
  /** Repo wired-up rows, sectioned only inside a git repo (#2557). */
41
54
  export declare const REPO_SECTION = "Repo";
42
55
  export declare const NOT_IN_REPO_HINT = "cd into your repo, then run factory doctor again";
43
56
  /** Detail for every repo wired-up row when doctor runs outside a git repository (#2554). */
44
- export declare const NOT_IN_REPO_SKIP_DETAIL = "skipped \u2014 not inside a git repository; cd into your repo, then run factory doctor again";
57
+ export declare const NOT_IN_REPO_SKIP_DETAIL = "skipped: not inside a git repository; cd into your repo, then run factory doctor again";
45
58
  export declare function hasSandboxRuntime(platform: NodeJS.Platform, commandAvailable: (cmd: string) => boolean): boolean;
46
59
  /** Reason the sandboxed claude auth probe is skipped before a sandbox policy is resolved,
47
60
  * or null when cmdDoctor should resolve the policy as before (#2383). */
@@ -71,6 +84,8 @@ export declare const GIT_AUTH_FIX = "run `gh auth setup-git` so git uses your gh
71
84
  export declare function gitAuthCheck(repoRoot: string | null, tryExec: (cmd: string, opts?: DoctorExecOptions) => string | null): DoctorCheck;
72
85
  export interface HarnessSetup {
73
86
  section: string;
87
+ /** HARNESS_CATALOG id whose CLI probe decides whether this harness is installed (#2631). */
88
+ harnessId: string;
74
89
  cli: string;
75
90
  install: string;
76
91
  /** Exact shell command that signs the harness in (#2556); also the row's `next` command. */
@@ -82,11 +97,24 @@ export interface HarnessSetup {
82
97
  export declare const HARNESS_SETUP: readonly HarnessSetup[];
83
98
  /** One optional `<cli> installed` row per harness, with the CLI version when found and the install command when not (#2555). */
84
99
  export declare function harnessInstallChecks(commandAvailable: (cmd: string) => boolean, tryExec: (cmd: string) => string | null): DoctorCheck[];
85
- /** One `<cli> signed in` row per harness (#2556), judged only from local evidence: an env key,
100
+ /** One `<cli> signed in` row per installed harness CLI (#2556, #2631), judged only from local evidence: an env key,
86
101
  * stored credentials, or `codex login status`. Never runs inference and never prints a secret. */
87
102
  export declare function harnessSignInChecks(probes: DoctorEnvProbes): DoctorCheck[];
103
+ /** The executable (first token) of a command template, or null when it cannot be parsed safely (#2691).
104
+ * Guards `isCommandAvailable`, which interpolates into `command -v` unescaped. */
105
+ export declare function commandExecutable(command: string): string | null;
106
+ /** Declared, agentic command harnesses that at least one repo `models.registry` entry uses (#2691). */
107
+ export declare function repoCommandHarnesses(harnesses: Readonly<Record<string, {
108
+ command: string;
109
+ agentic: boolean;
110
+ }>> | undefined, registry: Readonly<Record<string, {
111
+ harness?: string;
112
+ }>> | undefined): ConfiguredCommandHarness[];
113
+ /** One optional `command harness <id>` row per configured command harness (#2691). */
114
+ export declare function commandHarnessChecks(harnesses: readonly ConfiguredCommandHarness[], commandAvailable: (cmd: string) => boolean): DoctorCheck[];
88
115
  /** Coding agent prerequisites: at least one agent CLI is required; each CLI is then reported
89
- * on its own optional line, with sign-in reported separately from installation (#1269, #2422, #2555, #2556). */
116
+ * on its own optional line, with sign-in reported separately from installation (#1269, #2422, #2555, #2556).
117
+ * A repo-configured command harness whose executable is on PATH also counts as a coding agent (#2691). */
90
118
  export declare function agentCliChecks(probes: DoctorEnvProbes): DoctorCheck[];
91
119
  export declare function runDoctorChecks(probes: DoctorEnvProbes): DoctorCheck[];
92
120
  /** Row icon (#2550): ❌ only for a failure (counted in "N failed"), ➖ for a skipped row, ⚠️ for an optional warning. */
@@ -210,4 +238,7 @@ export declare function declaredHarnessChecks(probes: DeclaredHarnessProbe[]): D
210
238
  export declare function modelsRegistryChecks(configPath: string, registry: Readonly<Record<string, {
211
239
  harness?: string;
212
240
  }>> | undefined, validHarnesses: readonly string[]): DoctorCheck[];
241
+ /** `config` rows from the shared pre-PLAN lint (#2612): one failing row per finding, so doctor fails exactly when
242
+ * the run preflight would escalate or park. A single ok row when clean. Read-only. */
243
+ export declare function configLintChecks(input: ConfigLintInput, loadError?: string): DoctorCheck[];
213
244
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,17 @@
1
1
  // packages/cli/src/cli/doctor.ts — `factory doctor` environment preflight
2
+ import { existsSync } from 'node:fs';
2
3
  import { resolve } from 'node:path';
3
4
  import { detectSandboxRuntime } from '@on-par/factory-core';
5
+ import { findConfigSchemaErrors, isHarnessCliPresent, isTemplateToken, lintFactoryConfig, listBuiltInCheckers, } from '@on-par/factory-core/internal';
4
6
  import { MIN_NODE_MAJOR } from '../node-version.js';
5
7
  import { availableAgentClis, CLAUDE_CODE_URL, CODEX_CLI_URL, GITHUB_TOKENS_URL } from './first-run.js';
8
+ /** Section for rows about repo-configured command harnesses (#2691). */
9
+ export const COMMAND_HARNESS_SECTION = 'Command harness';
6
10
  /** Repo wired-up rows, sectioned only inside a git repo (#2557). */
7
11
  export const REPO_SECTION = 'Repo';
8
12
  export const NOT_IN_REPO_HINT = 'cd into your repo, then run factory doctor again';
9
13
  /** Detail for every repo wired-up row when doctor runs outside a git repository (#2554). */
10
- export const NOT_IN_REPO_SKIP_DETAIL = `skipped — not inside a git repository; ${NOT_IN_REPO_HINT}`;
14
+ export const NOT_IN_REPO_SKIP_DETAIL = `skipped: not inside a git repository; ${NOT_IN_REPO_HINT}`;
11
15
  export function hasSandboxRuntime(platform, commandAvailable) {
12
16
  return detectSandboxRuntime(platform, commandAvailable) !== 'none';
13
17
  }
@@ -15,9 +19,9 @@ export function hasSandboxRuntime(platform, commandAvailable) {
15
19
  * or null when cmdDoctor should resolve the policy as before (#2383). */
16
20
  export function sandboxAuthSkipDetail(opts) {
17
21
  if (!opts.runtimeAvailable)
18
- return 'skipped — no sandbox runtime (sandbox-exec/firejail) on this host';
22
+ return 'skipped: no sandbox runtime (sandbox-exec/firejail) on this host';
19
23
  if (!opts.hostOk)
20
- return 'skipped — host claude auth was not verified';
24
+ return 'skipped: host claude auth was not verified';
21
25
  if (!opts.inRepo)
22
26
  return NOT_IN_REPO_SKIP_DETAIL;
23
27
  return null;
@@ -70,7 +74,7 @@ export function gitAuthCheck(repoRoot, tryExec) {
70
74
  }
71
75
  const origin = tryExec('git remote get-url origin');
72
76
  if (!origin) {
73
- return { name: GIT_AUTH_CHECK, ok: true, optional: true, detail: 'skipped — no origin remote configured' };
77
+ return { name: GIT_AUTH_CHECK, ok: true, optional: true, detail: 'skipped: no origin remote configured' };
74
78
  }
75
79
  // An empty string is exit 0 with no HEAD ref (an empty repo): still authenticated.
76
80
  const probe = tryExec(GIT_AUTH_PROBE_CMD, { env: { ...GIT_AUTH_PROBE_ENV }, timeoutMs: GIT_AUTH_PROBE_TIMEOUT_MS });
@@ -89,6 +93,7 @@ export function gitAuthCheck(repoRoot, tryExec) {
89
93
  export const HARNESS_SETUP = [
90
94
  {
91
95
  section: 'Claude Code',
96
+ harnessId: 'claude-cli',
92
97
  cli: 'claude',
93
98
  install: 'npm install -g @anthropic-ai/claude-code',
94
99
  login: 'claude /login',
@@ -96,6 +101,7 @@ export const HARNESS_SETUP = [
96
101
  },
97
102
  {
98
103
  section: 'Codex',
104
+ harnessId: 'codex-cli',
99
105
  cli: 'codex',
100
106
  install: 'npm install -g @openai/codex',
101
107
  login: 'codex login',
@@ -103,6 +109,7 @@ export const HARNESS_SETUP = [
103
109
  },
104
110
  {
105
111
  section: 'OpenCode',
112
+ harnessId: 'opencode',
106
113
  cli: 'opencode',
107
114
  install: 'npm install -g opencode-ai',
108
115
  login: 'opencode auth login',
@@ -148,16 +155,13 @@ function safe(fn) {
148
155
  return false;
149
156
  }
150
157
  }
151
- /** One `<cli> signed in` row per harness (#2556), judged only from local evidence: an env key,
158
+ /** One `<cli> signed in` row per installed harness CLI (#2556, #2631), judged only from local evidence: an env key,
152
159
  * stored credentials, or `codex login status`. Never runs inference and never prints a secret. */
153
160
  export function harnessSignInChecks(probes) {
154
161
  const { commandAvailable, envPresent, tryExec } = probes;
155
162
  const found = availableAgentClis(commandAvailable);
156
- return HARNESS_SETUP.map(({ section, cli, login, envKeys }) => {
163
+ return HARNESS_SETUP.filter(({ harnessId }) => isHarnessCliPresent(harnessId, commandAvailable)).map(({ section, cli, login, envKeys }) => {
157
164
  const name = `${cli} signed in`;
158
- if (!commandAvailable(cli)) {
159
- return { name, section, ok: true, optional: true, detail: `skipped — ${cli} not installed` };
160
- }
161
165
  const fix = `sign in to ${section}: ${login}`;
162
166
  if (cli === 'codex') {
163
167
  const key = envKeys.find((k) => envPresent(k));
@@ -212,29 +216,98 @@ export function harnessSignInChecks(probes) {
212
216
  ok: false,
213
217
  // Required only when this is the sole agent: then nothing else can run.
214
218
  optional: found.length > 1,
215
- detail: `unverified — no sign-in evidence (${SIGN_IN_EVIDENCE[cli]})`,
219
+ detail: `unverified: no sign-in evidence (${SIGN_IN_EVIDENCE[cli]})`,
216
220
  fix,
217
221
  next: login,
218
222
  };
219
223
  });
220
224
  }
225
+ function safeList(fn) {
226
+ if (!fn)
227
+ return [];
228
+ try {
229
+ return fn();
230
+ }
231
+ catch {
232
+ return [];
233
+ }
234
+ }
235
+ /** The executable (first token) of a command template, or null when it cannot be parsed safely (#2691).
236
+ * Guards `isCommandAvailable`, which interpolates into `command -v` unescaped. */
237
+ export function commandExecutable(command) {
238
+ const tokens = command.trim().split(/\s+/);
239
+ while (tokens.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0]))
240
+ tokens.shift();
241
+ let token = tokens[0];
242
+ if (!token)
243
+ return null;
244
+ const quoted = /^(['"])(.*)\1$/.exec(token);
245
+ if (quoted)
246
+ token = quoted[2];
247
+ return /^[\w./@+~-]+$/.test(token) ? token : null;
248
+ }
249
+ /** Declared, agentic command harnesses that at least one repo `models.registry` entry uses (#2691). */
250
+ export function repoCommandHarnesses(harnesses, registry) {
251
+ const used = new Set(Object.values(registry ?? {}).map((def) => def.harness));
252
+ return Object.entries(harnesses ?? {})
253
+ .filter(([id, decl]) => decl.agentic === true && used.has(id))
254
+ .map(([id, decl]) => ({ harness: id, command: decl.command, executable: commandExecutable(decl.command) }));
255
+ }
256
+ /** One optional `command harness <id>` row per configured command harness (#2691). */
257
+ export function commandHarnessChecks(harnesses, commandAvailable) {
258
+ return harnesses.map((h) => {
259
+ const name = `command harness ${h.harness}`;
260
+ if (h.executable !== null && commandAvailable(h.executable)) {
261
+ return {
262
+ name,
263
+ section: COMMAND_HARNESS_SECTION,
264
+ optional: true,
265
+ ok: true,
266
+ detail: `${h.executable} found on PATH (command: ${h.command})`,
267
+ };
268
+ }
269
+ return {
270
+ name,
271
+ section: COMMAND_HARNESS_SECTION,
272
+ optional: true,
273
+ ok: false,
274
+ detail: `configured command \`${h.command}\`: ${h.executable === null ? 'cannot parse an executable from it' : `${h.executable} not found on PATH`}`,
275
+ fix: installFix(h),
276
+ };
277
+ });
278
+ }
279
+ function installFix(h) {
280
+ return `install ${h.executable ?? 'the agent CLI'} on PATH or fix "command" for harness ${h.harness} in the models config harnesses section`;
281
+ }
221
282
  /** Coding agent prerequisites: at least one agent CLI is required; each CLI is then reported
222
- * on its own optional line, with sign-in reported separately from installation (#1269, #2422, #2555, #2556). */
283
+ * on its own optional line, with sign-in reported separately from installation (#1269, #2422, #2555, #2556).
284
+ * A repo-configured command harness whose executable is on PATH also counts as a coding agent (#2691). */
223
285
  export function agentCliChecks(probes) {
224
286
  const { commandAvailable, tryExec } = probes;
225
287
  const found = availableAgentClis(commandAvailable);
288
+ const configured = safeList(probes.commandHarnesses);
289
+ const runnable = configured.filter((h) => h.executable !== null && commandAvailable(h.executable));
290
+ const agents = [...found, ...runnable.map((h) => `${h.harness} (command harness)`)];
291
+ const describe = (h) => `command harness ${h.harness} runs \`${h.command}\`, but ${h.executable ?? 'its executable'} is not on PATH`;
292
+ const missingRow = configured.length === 0
293
+ ? {
294
+ name: 'coding agent',
295
+ ok: false,
296
+ detail: 'no coding agent CLI on PATH (claude, codex or opencode)',
297
+ fix: `install Claude Code (${CLAUDE_CODE_URL}) or the Codex CLI (${CODEX_CLI_URL})`,
298
+ next: HARNESS_SETUP[0].install,
299
+ }
300
+ : {
301
+ name: 'coding agent',
302
+ ok: false,
303
+ detail: `no coding agent CLI on PATH (claude, codex or opencode); ${configured.map(describe).join('; ')}`,
304
+ fix: installFix(configured[0]),
305
+ };
226
306
  const checks = [
227
- found.length > 0
228
- ? { name: 'coding agent', ok: true, detail: `found ${found.join(', ')}` }
229
- : {
230
- name: 'coding agent',
231
- ok: false,
232
- detail: 'no coding agent CLI on PATH (claude, codex or opencode)',
233
- fix: `install Claude Code (${CLAUDE_CODE_URL}) or the Codex CLI (${CODEX_CLI_URL})`,
234
- next: HARNESS_SETUP[0].install,
235
- },
307
+ agents.length > 0 ? { name: 'coding agent', ok: true, detail: `found ${agents.join(', ')}` } : missingRow,
236
308
  ];
237
309
  checks.push(...harnessInstallChecks(commandAvailable, tryExec));
310
+ checks.push(...commandHarnessChecks(configured, commandAvailable));
238
311
  checks.push(...harnessSignInChecks(probes));
239
312
  return checks;
240
313
  }
@@ -248,7 +321,7 @@ export function runDoctorChecks(probes) {
248
321
  name: 'git repository',
249
322
  ok: false,
250
323
  optional: true,
251
- detail: 'not inside a git repository — repo checks skipped',
324
+ detail: 'not inside a git repository; repo checks skipped',
252
325
  fix: NOT_IN_REPO_HINT,
253
326
  });
254
327
  const repoSection = repoRoot === null ? {} : { section: REPO_SECTION };
@@ -353,14 +426,14 @@ export function runDoctorChecks(probes) {
353
426
  ok: true,
354
427
  optional: true,
355
428
  detail: sandboxRuntime === 'none'
356
- ? 'codex CLI found — no sandbox runtime, so codex runs with danger-full-access'
429
+ ? 'codex CLI found; no sandbox runtime, so codex runs with danger-full-access'
357
430
  : 'codex CLI found (sandboxed builds available)',
358
431
  }
359
432
  : {
360
433
  name: 'codex sandbox',
361
434
  ok: false,
362
435
  optional: true,
363
- detail: 'codex CLI not found — codex build routes unavailable',
436
+ detail: 'codex CLI not found; codex build routes unavailable',
364
437
  fix: 'install the codex CLI, or rely on claude routes',
365
438
  });
366
439
  checks.push(sandboxRuntime !== 'none'
@@ -369,7 +442,7 @@ export function runDoctorChecks(probes) {
369
442
  name: 'sandbox runtime',
370
443
  ok: false,
371
444
  optional: true,
372
- detail: 'no sandbox-exec (macOS) or firejail (Linux) on PATH — agents will run uncontained',
445
+ detail: 'no sandbox-exec (macOS) or firejail (Linux) on PATH; agents will run uncontained',
373
446
  fix: 'install firejail (Linux) or run on macOS; see README Safety',
374
447
  });
375
448
  if (repoRoot === null) {
@@ -441,7 +514,7 @@ export function formatDoctorChecks(checks, opts = {}) {
441
514
  }
442
515
  currentSection = check.section;
443
516
  const icon = doctorCheckIcon(check);
444
- lines.push(` ${icon} ${check.name} — ${check.detail}`);
517
+ lines.push(` ${icon} ${check.name} - ${check.detail}`);
445
518
  if (!check.ok && check.fix) {
446
519
  lines.push(` fix: ${check.fix}`);
447
520
  }
@@ -481,15 +554,15 @@ export function claudeKeychainCheck(probe) {
481
554
  return {
482
555
  name: 'claude keychain (macOS)',
483
556
  ok: false,
484
- detail: `security find-generic-password -s '${service}'${profile} failed inside tmux (SecKeychainSearchCopyNext) — Claude OAuth refresh will fail with local_auth`,
485
- fix: 'run the factory from a context with login-keychain access — a LaunchAgent in the gui domain (launchctl bootstrap gui/$(id -u) …) or a login Terminal, not a raw tmux pane — see docs/runbooks/macos-keychain-launchagent.md',
557
+ detail: `security find-generic-password -s '${service}'${profile} failed inside tmux (SecKeychainSearchCopyNext); Claude OAuth refresh will fail with local_auth`,
558
+ fix: 'run the factory from a context with login-keychain access, such as a LaunchAgent in the gui domain (launchctl bootstrap gui/$(id -u) …) or a login Terminal, not a raw tmux pane; see docs/runbooks/macos-keychain-launchagent.md',
486
559
  };
487
560
  }
488
561
  return {
489
562
  name: 'claude keychain (macOS)',
490
563
  ok: false,
491
564
  optional: true,
492
- detail: `security find-generic-password -s '${service}'${profile} failed — either never logged in, or this launch context cannot read the login keychain`,
565
+ detail: `security find-generic-password -s '${service}'${profile} failed; either never logged in, or this launch context cannot read the login keychain`,
493
566
  fix: `run ${loginFix(probe)} and complete /login (or export ANTHROPIC_API_KEY); if already logged in, see docs/runbooks/macos-keychain-launchagent.md`,
494
567
  };
495
568
  }
@@ -499,7 +572,7 @@ export function keychainPreflightError(probe) {
499
572
  if (probe.status !== 'unreadable' || !probe.inTmux)
500
573
  return null;
501
574
  const service = probe.service ?? DEFAULT_KEYCHAIN_SERVICE;
502
- return `Claude Code credentials are not readable from this tmux session (security find-generic-password -s '${service}' failed / SecKeychainSearchCopyNext). OAuth refresh will fail and every claimed issue would spuriously park as local_auth. Launch the factory from a context with login-keychain access — a LaunchAgent in the gui domain (launchctl bootstrap gui/$(id -u) …) or a login Terminal — or export ANTHROPIC_API_KEY. See docs/runbooks/macos-keychain-launchagent.md.`;
575
+ return `Claude Code credentials are not readable from this tmux session (security find-generic-password -s '${service}' failed / SecKeychainSearchCopyNext). OAuth refresh will fail and every claimed issue would spuriously park as local_auth. Launch the factory from a context with login-keychain access, such as a LaunchAgent in the gui domain (launchctl bootstrap gui/$(id -u) …) or a login Terminal, or export ANTHROPIC_API_KEY. See docs/runbooks/macos-keychain-launchagent.md.`;
503
576
  }
504
577
  const HOST_AUTH_CHECK = 'claude auth (host)';
505
578
  const SANDBOX_AUTH_CHECK = 'claude auth (sandboxed)';
@@ -528,7 +601,7 @@ export function sandboxClaudeAuthChecks(result) {
528
601
  sandboxed = {
529
602
  name: SANDBOX_AUTH_CHECK,
530
603
  ok: false,
531
- detail: `${result.sandboxDetail} — host claude auth is healthy, so the sandbox is blocking Claude credential refresh`,
604
+ detail: `${result.sandboxDetail}; host claude auth is healthy, so the sandbox is blocking Claude credential refresh`,
532
605
  fix: 'check the sandbox write allowlist (~/.claude.json*, $CLAUDE_CONFIG_DIR when set, ~/Library/Keychains); as a stopgap run with FACTORY_SANDBOX=0',
533
606
  };
534
607
  }
@@ -537,7 +610,7 @@ export function sandboxClaudeAuthChecks(result) {
537
610
  name: SANDBOX_AUTH_CHECK,
538
611
  ok: false,
539
612
  optional: true,
540
- detail: `${result.sandboxDetail} — host claude auth is not healthy either, so this is not attributable to the sandbox`,
613
+ detail: `${result.sandboxDetail}; host claude auth is not healthy either, so this is not attributable to the sandbox`,
541
614
  fix: 'fix host claude auth first, then re-run `factory doctor`',
542
615
  };
543
616
  }
@@ -568,7 +641,7 @@ export function leaseChecks(rows) {
568
641
  name: `port lease :${row.port}`,
569
642
  ok: true,
570
643
  optional: true,
571
- detail: `${row.worktreeId} — port ${row.port}, pid ${row.pid}, live`,
644
+ detail: `${row.worktreeId}: port ${row.port}, pid ${row.pid}, live`,
572
645
  };
573
646
  }
574
647
  const squatted = row.portSquatted ? '; port still in use by another process' : '';
@@ -576,7 +649,7 @@ export function leaseChecks(rows) {
576
649
  name: `port lease :${row.port}`,
577
650
  ok: false,
578
651
  optional: true,
579
- detail: `${row.worktreeId} — port ${row.port}, pid ${row.pid}, stale (${row.reason})${squatted}`,
652
+ detail: `${row.worktreeId}: port ${row.port}, pid ${row.pid}, stale (${row.reason})${squatted}`,
580
653
  fix: 'run `factory doctor --reconcile` to reclaim stale leases',
581
654
  };
582
655
  });
@@ -615,7 +688,7 @@ export function eventLogCheck(integrity) {
615
688
  ok: false,
616
689
  optional: true,
617
690
  detail: `${integrity.unparseable} of ${integrity.total} lines unparseable (${pct}%)`,
618
- fix: 'historical corruption from pre-lock parallel writes; appends are now serialized — metrics over this file carry this error bar',
691
+ fix: 'historical corruption from pre-lock parallel writes; appends are now serialized; metrics over this file carry this error bar',
619
692
  };
620
693
  }
621
694
  export function orphanContainerChecks(names) {
@@ -646,7 +719,7 @@ export function formatContainerReconcileReport(reaped) {
646
719
  return reaped
647
720
  .map((r) => r.removed
648
721
  ? `reconcile: removed container ${r.name}`
649
- : `reconcile: failed to remove container ${r.name} — ${r.detail}`)
722
+ : `reconcile: failed to remove container ${r.name}: ${r.detail}`)
650
723
  .join('\n');
651
724
  }
652
725
  export function formatMicroVmReconcileReport(reaped) {
@@ -655,7 +728,7 @@ export function formatMicroVmReconcileReport(reaped) {
655
728
  return reaped
656
729
  .map((r) => r.removed
657
730
  ? `reconcile: removed sbx VM ${r.name} (sbx rm --force)`
658
- : `reconcile: failed to remove sbx VM ${r.name} — ${r.detail}`)
731
+ : `reconcile: failed to remove sbx VM ${r.name}: ${r.detail}`)
659
732
  .join('\n');
660
733
  }
661
734
  export function formatReconcileReport(reaped) {
@@ -682,7 +755,7 @@ export function formatClaimReconcileReport(released) {
682
755
  return released
683
756
  .map((c) => c.released
684
757
  ? `reconcile: released issue #${c.issue} back to factory:queued (lease expired at ${c.expiresAt})`
685
- : `reconcile: failed to release issue #${c.issue} (lease expired at ${c.expiresAt}) — ${c.detail ?? 'unknown error'}`)
758
+ : `reconcile: failed to release issue #${c.issue} (lease expired at ${c.expiresAt}): ${c.detail ?? 'unknown error'}`)
686
759
  .join('\n');
687
760
  }
688
761
  const GREEN_PR_CHECK = 'green PRs awaiting merge';
@@ -696,7 +769,7 @@ export function unmergedGreenPrChecks(result) {
696
769
  name: GREEN_PR_CHECK,
697
770
  ok: false,
698
771
  optional: true,
699
- detail: `scan failed — ${result.detail}`,
772
+ detail: `scan failed: ${result.detail}`,
700
773
  fix: 'check `gh auth status` and network access, then re-run `factory doctor`',
701
774
  },
702
775
  ];
@@ -708,8 +781,8 @@ export function unmergedGreenPrChecks(result) {
708
781
  name: `green PR #${row.number}`,
709
782
  ok: false,
710
783
  optional: true,
711
- detail: `${row.issue === null ? 'owning issue unknown' : `issue #${row.issue}`} — green and ready on ${row.branch}, open ${row.ageHours}h, not merged`,
712
- fix: `review and merge #${row.number} (or close it) — the factory never auto-merges an orphaned PR`,
784
+ detail: `${row.issue === null ? 'owning issue unknown' : `issue #${row.issue}`}: green and ready on ${row.branch}, open ${row.ageHours}h, not merged`,
785
+ fix: `review and merge #${row.number} (or close it); the factory never auto-merges an orphaned PR`,
713
786
  }));
714
787
  }
715
788
  /** One doctor check per user-declared harness, from its `probe` result. */
@@ -722,7 +795,7 @@ export function declaredHarnessChecks(probes) {
722
795
  return {
723
796
  name,
724
797
  ok: false,
725
- detail: `unavailable — probe \`${p.probe}\` failed: ${p.detail}`,
798
+ detail: `unavailable: probe \`${p.probe}\` failed: ${p.detail}`,
726
799
  fix: 'install the agent CLI on PATH or fix the "probe" command in the models config harnesses section',
727
800
  };
728
801
  }
@@ -730,7 +803,7 @@ export function declaredHarnessChecks(probes) {
730
803
  name,
731
804
  ok: false,
732
805
  optional: true,
733
- detail: 'unverified — no probe configured',
806
+ detail: 'unverified: no probe configured',
734
807
  fix: 'add a "probe" command (exit 0 = reachable) to the harness in the models config',
735
808
  };
736
809
  });
@@ -768,4 +841,69 @@ export function modelsRegistryChecks(configPath, registry, validHarnesses) {
768
841
  const n = entries.length;
769
842
  return [{ name: REGISTRY_CHECK, ok: true, detail: `${n} entr${n === 1 ? 'y' : 'ies'} valid (${configPath})` }];
770
843
  }
844
+ /** `config` rows from the shared pre-PLAN lint (#2612): one failing row per finding, so doctor fails exactly when
845
+ * the run preflight would escalate or park. A single ok row when clean. Read-only. */
846
+ export function configLintChecks(input, loadError) {
847
+ const schemaRows = (input.configFile ? findConfigSchemaErrors(input.configFile) : []).map((f) => ({
848
+ name: 'config',
849
+ ok: false,
850
+ section: REPO_SECTION,
851
+ detail: `${f.file}${f.line ? `:${f.line}` : ''}: schema error at '${f.path}': ${f.message}`,
852
+ fix: f.fix,
853
+ }));
854
+ if (loadError !== undefined) {
855
+ return [
856
+ {
857
+ name: 'config',
858
+ ok: false,
859
+ section: REPO_SECTION,
860
+ detail: loadError,
861
+ fix: 'fix the constitution frontmatter YAML',
862
+ },
863
+ ...schemaRows,
864
+ ];
865
+ }
866
+ const { findings } = lintFactoryConfig(input);
867
+ if (findings.length > 0) {
868
+ const names = [...listBuiltInCheckers().map((c) => c.name), 'custom_*'].join(', ');
869
+ const lintRows = findings.map((f) => {
870
+ const where = `${f.file}${f.line ? `:${f.line}` : ''}`;
871
+ if (f.rule === 'unknown-checker') {
872
+ const kind = isTemplateToken(f.token) ? 'placeholder' : 'unknown';
873
+ return {
874
+ name: 'config',
875
+ ok: false,
876
+ section: REPO_SECTION,
877
+ detail: `${where}: ${kind} checker '${f.token}'`,
878
+ fix: `replace '${f.token}' with a real checker name (${names}) or delete the line`,
879
+ };
880
+ }
881
+ return {
882
+ name: 'config',
883
+ ok: false,
884
+ section: REPO_SECTION,
885
+ detail: `${where}: unfilled template token '${f.token}'`,
886
+ fix: `replace '${f.token}' with a real value`,
887
+ };
888
+ });
889
+ return [...lintRows, ...schemaRows];
890
+ }
891
+ if (schemaRows.length > 0)
892
+ return schemaRows;
893
+ const scanned = [];
894
+ if (input.constitution?.repoInstance)
895
+ scanned.push(input.constitution.repoInstance.relPath);
896
+ if (input.configFile && existsSync(input.configFile.absPath))
897
+ scanned.push(input.configFile.relPath);
898
+ return [
899
+ {
900
+ name: 'config',
901
+ ok: true,
902
+ section: REPO_SECTION,
903
+ detail: scanned.length > 0
904
+ ? `no unfilled placeholders, unknown checkers or schema errors in ${scanned.join(', ')}`
905
+ : 'no constitution or config file to scan',
906
+ },
907
+ ];
908
+ }
771
909
  //# sourceMappingURL=doctor.js.map