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

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;
@@ -34,14 +43,26 @@ export interface DoctorEnvProbes {
34
43
  codexSessionState?: () => 'ok' | 'refresh-pending' | 'unusable' | 'unknown';
35
44
  /** Whether a stored Claude Code login exists (#2556); reads local state only, never runs inference. */
36
45
  claudeLoggedIn?: () => boolean;
46
+ /** Stored Claude Code OAuth session state (#2721), read locally; never refreshes. */
47
+ claudeSessionState?: () => 'ok' | 'refresh-pending' | 'unusable' | 'unknown';
48
+ /** The agent's last recorded run-time local_auth failure when no login has happened since
49
+ * (#2733); reads local state only, never refreshes. */
50
+ localAuthFailure?: (agent: 'claude' | 'codex') => {
51
+ agent: string;
52
+ at: string;
53
+ } | null;
37
54
  /** Whether a stored opencode auth login exists (#2556); reads local state only, never runs inference. */
38
55
  opencodeLoggedIn?: () => boolean;
56
+ /** Repo-configured command harnesses (#2691); absent or [] means none. */
57
+ commandHarnesses?: () => ConfiguredCommandHarness[];
39
58
  }
59
+ /** Section for rows about repo-configured command harnesses (#2691). */
60
+ export declare const COMMAND_HARNESS_SECTION = "Command harness";
40
61
  /** Repo wired-up rows, sectioned only inside a git repo (#2557). */
41
62
  export declare const REPO_SECTION = "Repo";
42
63
  export declare const NOT_IN_REPO_HINT = "cd into your repo, then run factory doctor again";
43
64
  /** 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";
65
+ export declare const NOT_IN_REPO_SKIP_DETAIL = "skipped: not inside a git repository; cd into your repo, then run factory doctor again";
45
66
  export declare function hasSandboxRuntime(platform: NodeJS.Platform, commandAvailable: (cmd: string) => boolean): boolean;
46
67
  /** Reason the sandboxed claude auth probe is skipped before a sandbox policy is resolved,
47
68
  * or null when cmdDoctor should resolve the policy as before (#2383). */
@@ -71,6 +92,8 @@ export declare const GIT_AUTH_FIX = "run `gh auth setup-git` so git uses your gh
71
92
  export declare function gitAuthCheck(repoRoot: string | null, tryExec: (cmd: string, opts?: DoctorExecOptions) => string | null): DoctorCheck;
72
93
  export interface HarnessSetup {
73
94
  section: string;
95
+ /** HARNESS_CATALOG id whose CLI probe decides whether this harness is installed (#2631). */
96
+ harnessId: string;
74
97
  cli: string;
75
98
  install: string;
76
99
  /** Exact shell command that signs the harness in (#2556); also the row's `next` command. */
@@ -82,11 +105,24 @@ export interface HarnessSetup {
82
105
  export declare const HARNESS_SETUP: readonly HarnessSetup[];
83
106
  /** One optional `<cli> installed` row per harness, with the CLI version when found and the install command when not (#2555). */
84
107
  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,
108
+ /** One `<cli> signed in` row per installed harness CLI (#2556, #2631), judged only from local evidence: an env key,
86
109
  * stored credentials, or `codex login status`. Never runs inference and never prints a secret. */
87
110
  export declare function harnessSignInChecks(probes: DoctorEnvProbes): DoctorCheck[];
111
+ /** The executable (first token) of a command template, or null when it cannot be parsed safely (#2691).
112
+ * Guards `isCommandAvailable`, which interpolates into `command -v` unescaped. */
113
+ export declare function commandExecutable(command: string): string | null;
114
+ /** Declared, agentic command harnesses that at least one repo `models.registry` entry uses (#2691). */
115
+ export declare function repoCommandHarnesses(harnesses: Readonly<Record<string, {
116
+ command: string;
117
+ agentic: boolean;
118
+ }>> | undefined, registry: Readonly<Record<string, {
119
+ harness?: string;
120
+ }>> | undefined): ConfiguredCommandHarness[];
121
+ /** One optional `command harness <id>` row per configured command harness (#2691). */
122
+ export declare function commandHarnessChecks(harnesses: readonly ConfiguredCommandHarness[], commandAvailable: (cmd: string) => boolean): DoctorCheck[];
88
123
  /** 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). */
124
+ * on its own optional line, with sign-in reported separately from installation (#1269, #2422, #2555, #2556).
125
+ * A repo-configured command harness whose executable is on PATH also counts as a coding agent (#2691). */
90
126
  export declare function agentCliChecks(probes: DoctorEnvProbes): DoctorCheck[];
91
127
  export declare function runDoctorChecks(probes: DoctorEnvProbes): DoctorCheck[];
92
128
  /** Row icon (#2550): ❌ only for a failure (counted in "N failed"), ➖ for a skipped row, ⚠️ for an optional warning. */
@@ -210,4 +246,7 @@ export declare function declaredHarnessChecks(probes: DeclaredHarnessProbe[]): D
210
246
  export declare function modelsRegistryChecks(configPath: string, registry: Readonly<Record<string, {
211
247
  harness?: string;
212
248
  }>> | undefined, validHarnesses: readonly string[]): DoctorCheck[];
249
+ /** `config` rows from the shared pre-PLAN lint (#2612): one failing row per finding, so doctor fails exactly when
250
+ * the run preflight would escalate or park. A single ok row when clean. Read-only. */
251
+ export declare function configLintChecks(input: ConfigLintInput, loadError?: string): DoctorCheck[];
213
252
  //# 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,17 +155,44 @@ 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
+ function safeState(fn) {
159
+ if (!fn)
160
+ return 'unknown';
161
+ try {
162
+ return fn();
163
+ }
164
+ catch {
165
+ return 'unknown';
166
+ }
167
+ }
168
+ /** One `<cli> signed in` row per installed harness CLI (#2556, #2631), judged only from local evidence: an env key,
152
169
  * stored credentials, or `codex login status`. Never runs inference and never prints a secret. */
153
170
  export function harnessSignInChecks(probes) {
154
171
  const { commandAvailable, envPresent, tryExec } = probes;
155
172
  const found = availableAgentClis(commandAvailable);
156
- return HARNESS_SETUP.map(({ section, cli, login, envKeys }) => {
173
+ return HARNESS_SETUP.filter(({ harnessId }) => isHarnessCliPresent(harnessId, commandAvailable)).map(({ section, cli, login, envKeys }) => {
157
174
  const name = `${cli} signed in`;
158
- if (!commandAvailable(cli)) {
159
- return { name, section, ok: true, optional: true, detail: `skipped — ${cli} not installed` };
160
- }
161
175
  const fix = `sign in to ${section}: ${login}`;
176
+ if (cli === 'claude' || cli === 'codex') {
177
+ let failure = null;
178
+ try {
179
+ failure = probes.localAuthFailure?.(cli) ?? null;
180
+ }
181
+ catch {
182
+ failure = null;
183
+ }
184
+ if (failure) {
185
+ return {
186
+ name,
187
+ section,
188
+ ok: false,
189
+ optional: found.length > 1,
190
+ detail: `the most recent run failed sign-in (local_auth) at ${failure.at} and there has been no login since; re-run ${cli} login`,
191
+ fix,
192
+ next: login,
193
+ };
194
+ }
195
+ }
162
196
  if (cli === 'codex') {
163
197
  const key = envKeys.find((k) => envPresent(k));
164
198
  if (key)
@@ -199,6 +233,17 @@ export function harnessSignInChecks(probes) {
199
233
  const key = envKeys.find((k) => envPresent(k));
200
234
  if (key)
201
235
  return { name, section, ok: true, detail: `signed in (${key} set)` };
236
+ if (cli === 'claude' && safeState(probes.claudeSessionState) === 'unusable') {
237
+ return {
238
+ name,
239
+ section,
240
+ ok: false,
241
+ optional: found.length > 1,
242
+ detail: 'stored Claude Code login has expired and cannot be refreshed (no live access token and no refresh token); re-run claude login',
243
+ fix: 'run `claude`, then /login, then re-run `factory doctor`',
244
+ next: login,
245
+ };
246
+ }
202
247
  if (cli === 'claude' && safe(probes.claudeLoggedIn)) {
203
248
  return { name, section, ok: true, detail: 'signed in (stored Claude Code login found)' };
204
249
  }
@@ -212,29 +257,98 @@ export function harnessSignInChecks(probes) {
212
257
  ok: false,
213
258
  // Required only when this is the sole agent: then nothing else can run.
214
259
  optional: found.length > 1,
215
- detail: `unverified — no sign-in evidence (${SIGN_IN_EVIDENCE[cli]})`,
260
+ detail: `unverified: no sign-in evidence (${SIGN_IN_EVIDENCE[cli]})`,
216
261
  fix,
217
262
  next: login,
218
263
  };
219
264
  });
220
265
  }
266
+ function safeList(fn) {
267
+ if (!fn)
268
+ return [];
269
+ try {
270
+ return fn();
271
+ }
272
+ catch {
273
+ return [];
274
+ }
275
+ }
276
+ /** The executable (first token) of a command template, or null when it cannot be parsed safely (#2691).
277
+ * Guards `isCommandAvailable`, which interpolates into `command -v` unescaped. */
278
+ export function commandExecutable(command) {
279
+ const tokens = command.trim().split(/\s+/);
280
+ while (tokens.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0]))
281
+ tokens.shift();
282
+ let token = tokens[0];
283
+ if (!token)
284
+ return null;
285
+ const quoted = /^(['"])(.*)\1$/.exec(token);
286
+ if (quoted)
287
+ token = quoted[2];
288
+ return /^[\w./@+~-]+$/.test(token) ? token : null;
289
+ }
290
+ /** Declared, agentic command harnesses that at least one repo `models.registry` entry uses (#2691). */
291
+ export function repoCommandHarnesses(harnesses, registry) {
292
+ const used = new Set(Object.values(registry ?? {}).map((def) => def.harness));
293
+ return Object.entries(harnesses ?? {})
294
+ .filter(([id, decl]) => decl.agentic === true && used.has(id))
295
+ .map(([id, decl]) => ({ harness: id, command: decl.command, executable: commandExecutable(decl.command) }));
296
+ }
297
+ /** One optional `command harness <id>` row per configured command harness (#2691). */
298
+ export function commandHarnessChecks(harnesses, commandAvailable) {
299
+ return harnesses.map((h) => {
300
+ const name = `command harness ${h.harness}`;
301
+ if (h.executable !== null && commandAvailable(h.executable)) {
302
+ return {
303
+ name,
304
+ section: COMMAND_HARNESS_SECTION,
305
+ optional: true,
306
+ ok: true,
307
+ detail: `${h.executable} found on PATH (command: ${h.command})`,
308
+ };
309
+ }
310
+ return {
311
+ name,
312
+ section: COMMAND_HARNESS_SECTION,
313
+ optional: true,
314
+ ok: false,
315
+ detail: `configured command \`${h.command}\`: ${h.executable === null ? 'cannot parse an executable from it' : `${h.executable} not found on PATH`}`,
316
+ fix: installFix(h),
317
+ };
318
+ });
319
+ }
320
+ function installFix(h) {
321
+ return `install ${h.executable ?? 'the agent CLI'} on PATH or fix "command" for harness ${h.harness} in the models config harnesses section`;
322
+ }
221
323
  /** 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). */
324
+ * on its own optional line, with sign-in reported separately from installation (#1269, #2422, #2555, #2556).
325
+ * A repo-configured command harness whose executable is on PATH also counts as a coding agent (#2691). */
223
326
  export function agentCliChecks(probes) {
224
327
  const { commandAvailable, tryExec } = probes;
225
328
  const found = availableAgentClis(commandAvailable);
329
+ const configured = safeList(probes.commandHarnesses);
330
+ const runnable = configured.filter((h) => h.executable !== null && commandAvailable(h.executable));
331
+ const agents = [...found, ...runnable.map((h) => `${h.harness} (command harness)`)];
332
+ const describe = (h) => `command harness ${h.harness} runs \`${h.command}\`, but ${h.executable ?? 'its executable'} is not on PATH`;
333
+ const missingRow = configured.length === 0
334
+ ? {
335
+ name: 'coding agent',
336
+ ok: false,
337
+ detail: 'no coding agent CLI on PATH (claude, codex or opencode)',
338
+ fix: `install Claude Code (${CLAUDE_CODE_URL}) or the Codex CLI (${CODEX_CLI_URL})`,
339
+ next: HARNESS_SETUP[0].install,
340
+ }
341
+ : {
342
+ name: 'coding agent',
343
+ ok: false,
344
+ detail: `no coding agent CLI on PATH (claude, codex or opencode); ${configured.map(describe).join('; ')}`,
345
+ fix: installFix(configured[0]),
346
+ };
226
347
  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
- },
348
+ agents.length > 0 ? { name: 'coding agent', ok: true, detail: `found ${agents.join(', ')}` } : missingRow,
236
349
  ];
237
350
  checks.push(...harnessInstallChecks(commandAvailable, tryExec));
351
+ checks.push(...commandHarnessChecks(configured, commandAvailable));
238
352
  checks.push(...harnessSignInChecks(probes));
239
353
  return checks;
240
354
  }
@@ -248,7 +362,7 @@ export function runDoctorChecks(probes) {
248
362
  name: 'git repository',
249
363
  ok: false,
250
364
  optional: true,
251
- detail: 'not inside a git repository — repo checks skipped',
365
+ detail: 'not inside a git repository; repo checks skipped',
252
366
  fix: NOT_IN_REPO_HINT,
253
367
  });
254
368
  const repoSection = repoRoot === null ? {} : { section: REPO_SECTION };
@@ -353,14 +467,14 @@ export function runDoctorChecks(probes) {
353
467
  ok: true,
354
468
  optional: true,
355
469
  detail: sandboxRuntime === 'none'
356
- ? 'codex CLI found — no sandbox runtime, so codex runs with danger-full-access'
470
+ ? 'codex CLI found; no sandbox runtime, so codex runs with danger-full-access'
357
471
  : 'codex CLI found (sandboxed builds available)',
358
472
  }
359
473
  : {
360
474
  name: 'codex sandbox',
361
475
  ok: false,
362
476
  optional: true,
363
- detail: 'codex CLI not found — codex build routes unavailable',
477
+ detail: 'codex CLI not found; codex build routes unavailable',
364
478
  fix: 'install the codex CLI, or rely on claude routes',
365
479
  });
366
480
  checks.push(sandboxRuntime !== 'none'
@@ -369,7 +483,7 @@ export function runDoctorChecks(probes) {
369
483
  name: 'sandbox runtime',
370
484
  ok: false,
371
485
  optional: true,
372
- detail: 'no sandbox-exec (macOS) or firejail (Linux) on PATH — agents will run uncontained',
486
+ detail: 'no sandbox-exec (macOS) or firejail (Linux) on PATH; agents will run uncontained',
373
487
  fix: 'install firejail (Linux) or run on macOS; see README Safety',
374
488
  });
375
489
  if (repoRoot === null) {
@@ -441,7 +555,7 @@ export function formatDoctorChecks(checks, opts = {}) {
441
555
  }
442
556
  currentSection = check.section;
443
557
  const icon = doctorCheckIcon(check);
444
- lines.push(` ${icon} ${check.name} — ${check.detail}`);
558
+ lines.push(` ${icon} ${check.name} - ${check.detail}`);
445
559
  if (!check.ok && check.fix) {
446
560
  lines.push(` fix: ${check.fix}`);
447
561
  }
@@ -481,15 +595,15 @@ export function claudeKeychainCheck(probe) {
481
595
  return {
482
596
  name: 'claude keychain (macOS)',
483
597
  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',
598
+ detail: `security find-generic-password -s '${service}'${profile} failed inside tmux (SecKeychainSearchCopyNext); Claude OAuth refresh will fail with local_auth`,
599
+ 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
600
  };
487
601
  }
488
602
  return {
489
603
  name: 'claude keychain (macOS)',
490
604
  ok: false,
491
605
  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`,
606
+ detail: `security find-generic-password -s '${service}'${profile} failed; either never logged in, or this launch context cannot read the login keychain`,
493
607
  fix: `run ${loginFix(probe)} and complete /login (or export ANTHROPIC_API_KEY); if already logged in, see docs/runbooks/macos-keychain-launchagent.md`,
494
608
  };
495
609
  }
@@ -499,7 +613,7 @@ export function keychainPreflightError(probe) {
499
613
  if (probe.status !== 'unreadable' || !probe.inTmux)
500
614
  return null;
501
615
  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.`;
616
+ 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
617
  }
504
618
  const HOST_AUTH_CHECK = 'claude auth (host)';
505
619
  const SANDBOX_AUTH_CHECK = 'claude auth (sandboxed)';
@@ -528,7 +642,7 @@ export function sandboxClaudeAuthChecks(result) {
528
642
  sandboxed = {
529
643
  name: SANDBOX_AUTH_CHECK,
530
644
  ok: false,
531
- detail: `${result.sandboxDetail} — host claude auth is healthy, so the sandbox is blocking Claude credential refresh`,
645
+ detail: `${result.sandboxDetail}; host claude auth is healthy, so the sandbox is blocking Claude credential refresh`,
532
646
  fix: 'check the sandbox write allowlist (~/.claude.json*, $CLAUDE_CONFIG_DIR when set, ~/Library/Keychains); as a stopgap run with FACTORY_SANDBOX=0',
533
647
  };
534
648
  }
@@ -537,7 +651,7 @@ export function sandboxClaudeAuthChecks(result) {
537
651
  name: SANDBOX_AUTH_CHECK,
538
652
  ok: false,
539
653
  optional: true,
540
- detail: `${result.sandboxDetail} — host claude auth is not healthy either, so this is not attributable to the sandbox`,
654
+ detail: `${result.sandboxDetail}; host claude auth is not healthy either, so this is not attributable to the sandbox`,
541
655
  fix: 'fix host claude auth first, then re-run `factory doctor`',
542
656
  };
543
657
  }
@@ -568,7 +682,7 @@ export function leaseChecks(rows) {
568
682
  name: `port lease :${row.port}`,
569
683
  ok: true,
570
684
  optional: true,
571
- detail: `${row.worktreeId} — port ${row.port}, pid ${row.pid}, live`,
685
+ detail: `${row.worktreeId}: port ${row.port}, pid ${row.pid}, live`,
572
686
  };
573
687
  }
574
688
  const squatted = row.portSquatted ? '; port still in use by another process' : '';
@@ -576,7 +690,7 @@ export function leaseChecks(rows) {
576
690
  name: `port lease :${row.port}`,
577
691
  ok: false,
578
692
  optional: true,
579
- detail: `${row.worktreeId} — port ${row.port}, pid ${row.pid}, stale (${row.reason})${squatted}`,
693
+ detail: `${row.worktreeId}: port ${row.port}, pid ${row.pid}, stale (${row.reason})${squatted}`,
580
694
  fix: 'run `factory doctor --reconcile` to reclaim stale leases',
581
695
  };
582
696
  });
@@ -615,7 +729,7 @@ export function eventLogCheck(integrity) {
615
729
  ok: false,
616
730
  optional: true,
617
731
  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',
732
+ fix: 'historical corruption from pre-lock parallel writes; appends are now serialized; metrics over this file carry this error bar',
619
733
  };
620
734
  }
621
735
  export function orphanContainerChecks(names) {
@@ -646,7 +760,7 @@ export function formatContainerReconcileReport(reaped) {
646
760
  return reaped
647
761
  .map((r) => r.removed
648
762
  ? `reconcile: removed container ${r.name}`
649
- : `reconcile: failed to remove container ${r.name} — ${r.detail}`)
763
+ : `reconcile: failed to remove container ${r.name}: ${r.detail}`)
650
764
  .join('\n');
651
765
  }
652
766
  export function formatMicroVmReconcileReport(reaped) {
@@ -655,7 +769,7 @@ export function formatMicroVmReconcileReport(reaped) {
655
769
  return reaped
656
770
  .map((r) => r.removed
657
771
  ? `reconcile: removed sbx VM ${r.name} (sbx rm --force)`
658
- : `reconcile: failed to remove sbx VM ${r.name} — ${r.detail}`)
772
+ : `reconcile: failed to remove sbx VM ${r.name}: ${r.detail}`)
659
773
  .join('\n');
660
774
  }
661
775
  export function formatReconcileReport(reaped) {
@@ -682,7 +796,7 @@ export function formatClaimReconcileReport(released) {
682
796
  return released
683
797
  .map((c) => c.released
684
798
  ? `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'}`)
799
+ : `reconcile: failed to release issue #${c.issue} (lease expired at ${c.expiresAt}): ${c.detail ?? 'unknown error'}`)
686
800
  .join('\n');
687
801
  }
688
802
  const GREEN_PR_CHECK = 'green PRs awaiting merge';
@@ -696,7 +810,7 @@ export function unmergedGreenPrChecks(result) {
696
810
  name: GREEN_PR_CHECK,
697
811
  ok: false,
698
812
  optional: true,
699
- detail: `scan failed — ${result.detail}`,
813
+ detail: `scan failed: ${result.detail}`,
700
814
  fix: 'check `gh auth status` and network access, then re-run `factory doctor`',
701
815
  },
702
816
  ];
@@ -708,8 +822,8 @@ export function unmergedGreenPrChecks(result) {
708
822
  name: `green PR #${row.number}`,
709
823
  ok: false,
710
824
  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`,
825
+ detail: `${row.issue === null ? 'owning issue unknown' : `issue #${row.issue}`}: green and ready on ${row.branch}, open ${row.ageHours}h, not merged`,
826
+ fix: `review and merge #${row.number} (or close it); the factory never auto-merges an orphaned PR`,
713
827
  }));
714
828
  }
715
829
  /** One doctor check per user-declared harness, from its `probe` result. */
@@ -722,7 +836,7 @@ export function declaredHarnessChecks(probes) {
722
836
  return {
723
837
  name,
724
838
  ok: false,
725
- detail: `unavailable — probe \`${p.probe}\` failed: ${p.detail}`,
839
+ detail: `unavailable: probe \`${p.probe}\` failed: ${p.detail}`,
726
840
  fix: 'install the agent CLI on PATH or fix the "probe" command in the models config harnesses section',
727
841
  };
728
842
  }
@@ -730,7 +844,7 @@ export function declaredHarnessChecks(probes) {
730
844
  name,
731
845
  ok: false,
732
846
  optional: true,
733
- detail: 'unverified — no probe configured',
847
+ detail: 'unverified: no probe configured',
734
848
  fix: 'add a "probe" command (exit 0 = reachable) to the harness in the models config',
735
849
  };
736
850
  });
@@ -768,4 +882,69 @@ export function modelsRegistryChecks(configPath, registry, validHarnesses) {
768
882
  const n = entries.length;
769
883
  return [{ name: REGISTRY_CHECK, ok: true, detail: `${n} entr${n === 1 ? 'y' : 'ies'} valid (${configPath})` }];
770
884
  }
885
+ /** `config` rows from the shared pre-PLAN lint (#2612): one failing row per finding, so doctor fails exactly when
886
+ * the run preflight would escalate or park. A single ok row when clean. Read-only. */
887
+ export function configLintChecks(input, loadError) {
888
+ const schemaRows = (input.configFile ? findConfigSchemaErrors(input.configFile) : []).map((f) => ({
889
+ name: 'config',
890
+ ok: false,
891
+ section: REPO_SECTION,
892
+ detail: `${f.file}${f.line ? `:${f.line}` : ''}: schema error at '${f.path}': ${f.message}`,
893
+ fix: f.fix,
894
+ }));
895
+ if (loadError !== undefined) {
896
+ return [
897
+ {
898
+ name: 'config',
899
+ ok: false,
900
+ section: REPO_SECTION,
901
+ detail: loadError,
902
+ fix: 'fix the constitution frontmatter YAML',
903
+ },
904
+ ...schemaRows,
905
+ ];
906
+ }
907
+ const { findings } = lintFactoryConfig(input);
908
+ if (findings.length > 0) {
909
+ const names = [...listBuiltInCheckers().map((c) => c.name), 'custom_*'].join(', ');
910
+ const lintRows = findings.map((f) => {
911
+ const where = `${f.file}${f.line ? `:${f.line}` : ''}`;
912
+ if (f.rule === 'unknown-checker') {
913
+ const kind = isTemplateToken(f.token) ? 'placeholder' : 'unknown';
914
+ return {
915
+ name: 'config',
916
+ ok: false,
917
+ section: REPO_SECTION,
918
+ detail: `${where}: ${kind} checker '${f.token}'`,
919
+ fix: `replace '${f.token}' with a real checker name (${names}) or delete the line`,
920
+ };
921
+ }
922
+ return {
923
+ name: 'config',
924
+ ok: false,
925
+ section: REPO_SECTION,
926
+ detail: `${where}: unfilled template token '${f.token}'`,
927
+ fix: `replace '${f.token}' with a real value`,
928
+ };
929
+ });
930
+ return [...lintRows, ...schemaRows];
931
+ }
932
+ if (schemaRows.length > 0)
933
+ return schemaRows;
934
+ const scanned = [];
935
+ if (input.constitution?.repoInstance)
936
+ scanned.push(input.constitution.repoInstance.relPath);
937
+ if (input.configFile && existsSync(input.configFile.absPath))
938
+ scanned.push(input.configFile.relPath);
939
+ return [
940
+ {
941
+ name: 'config',
942
+ ok: true,
943
+ section: REPO_SECTION,
944
+ detail: scanned.length > 0
945
+ ? `no unfilled placeholders, unknown checkers or schema errors in ${scanned.join(', ')}`
946
+ : 'no constitution or config file to scan',
947
+ },
948
+ ];
949
+ }
771
950
  //# sourceMappingURL=doctor.js.map