@on-par/factory-cli 0.9.4 → 0.9.5-next.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.
- package/README.md +3 -3
- package/dist/cli/doctor.d.ts +42 -3
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +221 -42
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/github-stub.d.ts +38 -0
- package/dist/cli/github-stub.d.ts.map +1 -0
- package/dist/cli/github-stub.js +206 -0
- package/dist/cli/github-stub.js.map +1 -0
- package/dist/cli/index.d.ts +35 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +336 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/stop-sentinel.js +3 -3
- package/dist/cli/stop-sentinel.js.map +1 -1
- package/dist/cli/sweep-heartbeat.js +5 -5
- package/dist/cli/sweep-heartbeat.js.map +1 -1
- package/dist/entry.d.ts +4 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +14 -3
- package/dist/entry.js.map +1 -1
- package/dist/node-version.d.ts +5 -1
- package/dist/node-version.d.ts.map +1 -1
- package/dist/node-version.js +11 -3
- package/dist/node-version.js.map +1 -1
- package/package.json +4 -4
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
|
-

|
|
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: [#
|
|
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
|
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/cli/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -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
|
|
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
|
|
22
|
+
return 'skipped: no sandbox runtime (sandbox-exec/firejail) on this host';
|
|
19
23
|
if (!opts.hostOk)
|
|
20
|
-
return 'skipped
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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)
|
|
485
|
-
fix: 'run the factory from a context with login-keychain access
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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}
|
|
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}
|
|
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})
|
|
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
|
|
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}`}
|
|
712
|
-
fix: `review and merge #${row.number} (or close it)
|
|
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
|
|
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
|
|
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
|