claude-spotter 1.4.8 → 1.4.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/CHANGELOG.md +80 -0
- package/README.ja.md +1 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/cli/auditor-cmd.mjs +1 -1
- package/src/cli/codex-hook-cmd.mjs +17 -7
- package/src/core/auditor-backend.mjs +47 -33
- package/src/core/codex-cli-availability.mjs +51 -0
- package/src/daemon/daemon.mjs +13 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.10
|
|
4
|
+
|
|
5
|
+
**Claude host primary auditor: Codex CLI 検出で自動採用、なければ Haiku の 2 段選択へ**。
|
|
6
|
+
v1.4.7 までは Phase 5 opt-in の `SPOTTER_AUDITOR_BACKEND_POLICY=next` を立てたセッションだけが
|
|
7
|
+
Codex CLI を primary auditor に使い、それ以外は無条件で Haiku を呼んでいた。実測 (Phase 4 matrix
|
|
8
|
+
2026-05-06: `claude.codex-cli=10041ms` vs Haiku `user_input ~14.3s / turn_end ~16.6s`) で Codex CLI
|
|
9
|
+
の latency 優位は十分に確定していたため、opt-in を撤廃して既定動作を「Codex CLI 検出時 CLI、
|
|
10
|
+
なければ Haiku」に変更する。検出は configuration-time (daemon 起動時に env.PATH を同期 walk、
|
|
11
|
+
spawn 無し)。一度選ばれた backend が runtime で落ちた場合は従来通り `AuditorBackendError` を
|
|
12
|
+
throw する (§0 fallback 禁止維持) — 検出は **selection-time のみ**で、runtime 失敗時に別 backend へ
|
|
13
|
+
silent retry することはない。codex-sidecar は `spotter codex *` の明示 second-pass workflow
|
|
14
|
+
専用に固定 (現セッションでも `[caveat:codex-sidecar] advisory unavailable: sidecar command failed`
|
|
15
|
+
を観測したため、primary chain には入れない)。Codex host の primary backend (`codex-cli` 固定) と
|
|
16
|
+
監査用子プロセスのモデル指定 (`gpt-5.4-mini` / `model_reasoning_effort="low"`) は変更なし。
|
|
17
|
+
|
|
18
|
+
### 変更点
|
|
19
|
+
|
|
20
|
+
- **新規 [src/core/codex-cli-availability.mjs](src/core/codex-cli-availability.mjs)**:
|
|
21
|
+
`isCodexCliAvailable({env, platform, fileExists})` を追加。env.PATH を同期 walk して codex
|
|
22
|
+
バイナリの実在を判定する。Windows は PATHEXT 相当 (`.cmd` / `.exe` / `.bat`) を試行、`Path` を
|
|
23
|
+
優先しつつ `PATH` を fallback として受理。subprocess は spawn しない。
|
|
24
|
+
- **編集 [src/core/auditor-backend.mjs](src/core/auditor-backend.mjs)**:
|
|
25
|
+
`selectByPolicy` を policy 区分 (`current` / `next`) ベースから availability ベースに置換。
|
|
26
|
+
Claude host = `isCodexCliAvailable` 結果で `codex-cli` / `haiku` を分岐、
|
|
27
|
+
Codex host = `codex-cli` 固定。`SPOTTER_AUDITOR_BACKEND_POLICY` 環境変数は legacy 値
|
|
28
|
+
(`current` / `next`) を引き続き受理するが selection には影響しない (back-compat)。
|
|
29
|
+
`selectAuditorBackend` / `createAuditorBackend` に `isCodexCliAvailable` DI パラメータを追加。
|
|
30
|
+
`createAuditorBackend` は選択結果と理由を logger に 1 行出力する。
|
|
31
|
+
- **編集 [src/daemon/daemon.mjs](src/daemon/daemon.mjs)**:
|
|
32
|
+
`auditorBackendName` のデフォルトを `'haiku'` から `'auto'` に変更。`haikuCaller` が明示注入
|
|
33
|
+
された呼び出し (テスト経路) では `'haiku'` を既定にし、test 用 fixture が `projectRoot` 不要で
|
|
34
|
+
動作するよう保つ。
|
|
35
|
+
- **編集 [src/cli/auditor-cmd.mjs](src/cli/auditor-cmd.mjs)**:
|
|
36
|
+
`parseJudgeArgs` の backend デフォルトを `'auto'` に変更 (旧: `SPOTTER_AUDITOR_BACKEND_POLICY`
|
|
37
|
+
が無いと `'haiku'` 固定だった)。
|
|
38
|
+
- **新規 [test/codex-cli-availability.test.mjs](test/codex-cli-availability.test.mjs)**:
|
|
39
|
+
POSIX / Windows / 空 PATH / malformed PATH / `path.posix` vs `path.win32` の 7 件回帰ガード。
|
|
40
|
+
- **編集 [test/auditor-backend.test.mjs](test/auditor-backend.test.mjs)**:
|
|
41
|
+
旧 `policy_current_*` / `policy_next_*` テストを availability-based テスト
|
|
42
|
+
(`claude_host_codex_cli_detected` / `claude_host_codex_cli_unavailable` / `codex_host`) に置換、
|
|
43
|
+
legacy policy 値の受理 + selection 無効化テスト、`createAuditorBackend` の logger 出力テストを追加。
|
|
44
|
+
|
|
45
|
+
### ユーザー側で必要な手順
|
|
46
|
+
|
|
47
|
+
1. `npm install -g claude-spotter@1.4.10`
|
|
48
|
+
2. 既存プロジェクトでの settings.json 再生成は不要 (hook command path / contract に変更なし)。
|
|
49
|
+
3. `SPOTTER_AUDITOR_BACKEND_POLICY=next` を export していたユーザーは設定を外して構わない
|
|
50
|
+
(受理はするが既定動作と同じになる)。`SPOTTER_AUDITOR_BACKEND=haiku` の明示固定は引き続き有効。
|
|
51
|
+
|
|
52
|
+
## 1.4.9
|
|
53
|
+
|
|
54
|
+
**Codex hooks feature 名の現行 CLI 追従**。現行 Codex CLI の `codex features list` は
|
|
55
|
+
hook 機能を `hooks stable true` と表示するが、Spotter の `codex-hook diagnostics` は旧名
|
|
56
|
+
`codex_hooks` だけを見ていたため、hooks 登録済みでも `availability:"unavailable"` と誤判定していた。
|
|
57
|
+
|
|
58
|
+
### 変更点
|
|
59
|
+
|
|
60
|
+
- **編集 [src/cli/codex-hook-cmd.mjs](src/cli/codex-hook-cmd.mjs)**:
|
|
61
|
+
`codexHookDiagnostics` が現行 `hooks` と旧 `codex_hooks` の両方を enabled evidence として扱う。
|
|
62
|
+
`installCodexHooks` は現行 CLI に合わせて `[features].hooks = true` を書く。既に旧
|
|
63
|
+
`codex_hooks = true` がある環境では削除せず、`hooks = true` を追加して現行 CLI で確実に有効化する。
|
|
64
|
+
- **編集 [test/codex-hook-cmd.test.mjs](test/codex-hook-cmd.test.mjs)**:
|
|
65
|
+
現行 `hooks stable true` と旧 `codex_hooks stable true` の diagnostics 回帰テスト、
|
|
66
|
+
旧 feature key が残る config への install 回帰テストを追加。
|
|
67
|
+
- **編集 docs**:
|
|
68
|
+
`open-issues.md` の Codex pending / hook event path 記述を v1.4.8 以降の
|
|
69
|
+
host-neutral `.spotter/pending/` / `.spotter/hook-events.jsonl` に追従。
|
|
70
|
+
|
|
71
|
+
### ユーザー側で必要な手順
|
|
72
|
+
|
|
73
|
+
1. `npm install -g claude-spotter@1.4.9`
|
|
74
|
+
2. Codex hooks を使う環境では、各 Spotter install 済み project で `spotter install` を再実行
|
|
75
|
+
(`~/.codex/config.toml` に `[features].hooks = true` を確実に反映するため)
|
|
76
|
+
|
|
77
|
+
### 検証
|
|
78
|
+
|
|
79
|
+
- `node --test` 322 tests / 321 pass / 1 skip 緑
|
|
80
|
+
- `spotter codex-hook diagnostics --project /home/kite/projects/Spotter` が
|
|
81
|
+
`availability:"available"` / evidence=`hooks stable true` を返すことを実機確認
|
|
82
|
+
|
|
3
83
|
## 1.4.8
|
|
4
84
|
|
|
5
85
|
**Hook 挙動 parity (Codex → Claude) 移植**。Codex 側で確定していた 3 つの hook 挙動 — Stop
|
package/README.ja.md
CHANGED
|
@@ -45,6 +45,7 @@ spotter install
|
|
|
45
45
|
|
|
46
46
|
`v0.3.0` 以降は**プロジェクト単位の明示的 install** を採用しています (v0.2 までの `postinstall` 自動登録はデーモン増殖の主因だったため撤回)。各プロジェクトの `.claude/settings.json` に hook を登録し、そのプロジェクトでの Claude Code セッションのみで有効になります。
|
|
47
47
|
Codex CLI が使える環境では、同じ `spotter install` が user-level の Codex native hooks も登録します。実際に動くプロジェクトは `spotter install` が作る `.spotter/marker.json` で制限されるため、無関係な Codex セッションでは Spotter は起動しません。
|
|
48
|
+
Codex 側では現行の `[features].hooks = true` を有効化し、互換のため旧 `codex_hooks` diagnostics output も認識します。
|
|
48
49
|
|
|
49
50
|
Spotter を upgrade した後、release note で hook 設定変更が案内されている場合は、各 install 済みプロジェクトで `spotter install` を再実行してください。global package update でコード経路は変わりますが、既存 `.claude/settings.json` の timeout 値は自動では書き換わりません。
|
|
50
51
|
|
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ spotter install
|
|
|
45
45
|
|
|
46
46
|
Since `v0.3.0`, Spotter requires **explicit per-project install** (the earlier `postinstall` auto-registration was the leading cause of orphan daemons). `spotter install` writes hooks into the project's `.claude/settings.json`; the audit is then active only in Claude Code sessions for that project.
|
|
47
47
|
When the Codex CLI is available, the same `spotter install` also registers user-level Codex native hooks. Project activation still depends on the same per-project `.spotter/marker.json`, so unrelated Codex sessions do not trigger Spotter.
|
|
48
|
+
For Codex, install enables the current `[features].hooks = true` flag and still recognizes older `codex_hooks` diagnostics output for compatibility.
|
|
48
49
|
|
|
49
50
|
After upgrading Spotter, re-run `spotter install` in each installed project when release notes mention hook setting changes. The global package update changes the code path, but existing `.claude/settings.json` timeout values are not rewritten automatically.
|
|
50
51
|
|
package/package.json
CHANGED
package/src/cli/auditor-cmd.mjs
CHANGED
|
@@ -292,7 +292,7 @@ function parseJudgeArgs(argv) {
|
|
|
292
292
|
inputPath: null,
|
|
293
293
|
projectRoot: process.cwd(),
|
|
294
294
|
hostAgent: null,
|
|
295
|
-
backend: process.env.SPOTTER_AUDITOR_BACKEND ||
|
|
295
|
+
backend: process.env.SPOTTER_AUDITOR_BACKEND || 'auto',
|
|
296
296
|
};
|
|
297
297
|
for (let index = 0; index < argv.length; index += 1) {
|
|
298
298
|
const arg = argv[index];
|
|
@@ -35,6 +35,7 @@ const CODEX_HOOK_TIMEOUT_SEC = 60;
|
|
|
35
35
|
const DEFAULT_CODEX_HOOK_AUDITOR_TIMEOUT_MS = 20_000;
|
|
36
36
|
const SHORT_PROMPT_MAX_CHARS = 10;
|
|
37
37
|
const DEFAULT_CODEX_STOP_SHORT_FINAL_MAX_CHARS = 120;
|
|
38
|
+
const CODEX_HOOK_FEATURE_NAMES = ['hooks', 'codex_hooks'];
|
|
38
39
|
|
|
39
40
|
const CODEX_HOOK_USAGE = `spotter codex-hook — Codex native hook adapter
|
|
40
41
|
|
|
@@ -364,7 +365,8 @@ export async function codexHookDiagnostics({ codexHome = defaultCodexHome(), pro
|
|
|
364
365
|
const features = spawnSyncFn('codex', ['features', 'list'], { encoding: 'utf8', maxBuffer: 1024 * 1024 });
|
|
365
366
|
const featureOutput = [features.stdout, features.stderr].filter(Boolean).join('\n');
|
|
366
367
|
const hooks = await loadJson(join(codexHome, 'hooks.json'));
|
|
367
|
-
const
|
|
368
|
+
const evidence = featureOutput.split('\n').find((line) => isEnabledCodexHookFeatureLine(line)) ?? null;
|
|
369
|
+
const codexHooksFeature = evidence ? 'enabled' : 'not-enabled';
|
|
368
370
|
const installed = {
|
|
369
371
|
sessionStart: hookState(hooks, 'SessionStart', 'codex-hook session-start'),
|
|
370
372
|
userPromptSubmit: hookState(hooks, 'UserPromptSubmit', 'codex-hook user-prompt-submit'),
|
|
@@ -382,7 +384,7 @@ export async function codexHookDiagnostics({ codexHome = defaultCodexHome(), pro
|
|
|
382
384
|
codexHome,
|
|
383
385
|
hooksPath: join(codexHome, 'hooks.json'),
|
|
384
386
|
installedHooks: installed,
|
|
385
|
-
evidence
|
|
387
|
+
evidence,
|
|
386
388
|
runtime: runtimeProjectRoot
|
|
387
389
|
? await summarizeCodexHookEvents({ projectRoot: runtimeProjectRoot })
|
|
388
390
|
: null,
|
|
@@ -653,7 +655,7 @@ function isSpotterCodexHook(hook) {
|
|
|
653
655
|
async function ensureCodexHooksFeature(configPath) {
|
|
654
656
|
let raw = '';
|
|
655
657
|
if (existsSync(configPath)) raw = await readFile(configPath, 'utf8');
|
|
656
|
-
if (/^\s*
|
|
658
|
+
if (/^\s*hooks\s*=\s*true\s*$/m.test(raw)) return 'already-enabled';
|
|
657
659
|
const next = enableCodexHooksFeature(raw);
|
|
658
660
|
await mkdir(dirname(configPath), { recursive: true });
|
|
659
661
|
await writeFile(configPath, next, 'utf8');
|
|
@@ -663,7 +665,7 @@ async function ensureCodexHooksFeature(configPath) {
|
|
|
663
665
|
function enableCodexHooksFeature(raw) {
|
|
664
666
|
const text = raw.trimEnd();
|
|
665
667
|
if (!/^\s*\[features\]\s*$/m.test(text)) {
|
|
666
|
-
return `${text}${text ? '\n\n' : ''}[features]\
|
|
668
|
+
return `${text}${text ? '\n\n' : ''}[features]\nhooks = true\n`;
|
|
667
669
|
}
|
|
668
670
|
const lines = text.split('\n');
|
|
669
671
|
let inFeatures = false;
|
|
@@ -678,15 +680,23 @@ function enableCodexHooksFeature(raw) {
|
|
|
678
680
|
inFeatures = /^\s*\[features\]\s*$/.test(line);
|
|
679
681
|
continue;
|
|
680
682
|
}
|
|
681
|
-
if (inFeatures && /^\s*
|
|
682
|
-
lines[index] = '
|
|
683
|
+
if (inFeatures && /^\s*hooks\s*=/.test(line)) {
|
|
684
|
+
lines[index] = 'hooks = true';
|
|
683
685
|
return `${lines.join('\n')}\n`;
|
|
684
686
|
}
|
|
685
687
|
}
|
|
686
|
-
lines.splice(insertAt, 0, '
|
|
688
|
+
lines.splice(insertAt, 0, 'hooks = true');
|
|
687
689
|
return `${lines.join('\n')}\n`;
|
|
688
690
|
}
|
|
689
691
|
|
|
692
|
+
function isEnabledCodexHookFeatureLine(line) {
|
|
693
|
+
const trimmed = String(line ?? '').trim();
|
|
694
|
+
return CODEX_HOOK_FEATURE_NAMES.some((name) => {
|
|
695
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
696
|
+
return new RegExp(`^${escaped}\\s+\\S+\\s+true\\b`).test(trimmed);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
690
700
|
function hookState(settings, event, commandFragment) {
|
|
691
701
|
const groups = settings?.hooks?.[event];
|
|
692
702
|
if (!Array.isArray(groups)) return 'not-installed';
|
|
@@ -12,6 +12,7 @@ import { AuditorBackendError } from './auditor-error.mjs';
|
|
|
12
12
|
import { detectHostAgent } from './host-agent.mjs';
|
|
13
13
|
import { createCodexCliAuditorBackend } from './codex-cli-backend.mjs';
|
|
14
14
|
import { createCodexSidecarAuditorBackend } from './codex-sidecar-auditor-backend.mjs';
|
|
15
|
+
import { isCodexCliAvailable as defaultIsCodexCliAvailable } from './codex-cli-availability.mjs';
|
|
15
16
|
|
|
16
17
|
export { AuditorBackendError } from './auditor-error.mjs';
|
|
17
18
|
export {
|
|
@@ -33,10 +34,17 @@ export function createAuditorBackend({
|
|
|
33
34
|
logger = () => {},
|
|
34
35
|
haikuCaller = null,
|
|
35
36
|
timeoutMs = DEFAULT_HAIKU_AUDITOR_TIMEOUT_MS,
|
|
37
|
+
isCodexCliAvailable = defaultIsCodexCliAvailable,
|
|
36
38
|
} = {}) {
|
|
37
39
|
const selected = backend === 'auto'
|
|
38
|
-
? selectAuditorBackend({
|
|
40
|
+
? selectAuditorBackend({
|
|
41
|
+
hostAgent,
|
|
42
|
+
env,
|
|
43
|
+
projectConfig: projectRoot ? { projectRoot } : null,
|
|
44
|
+
isCodexCliAvailable,
|
|
45
|
+
})
|
|
39
46
|
: { backend, mode: backend, compatibility: backend === 'haiku' ? 'current_haiku' : 'none', reason: 'explicit_backend' };
|
|
47
|
+
logger(`auditor backend selected: backend=${selected.backend} reason=${selected.reason}`);
|
|
40
48
|
if (selected.backend === 'haiku') {
|
|
41
49
|
return createHaikuAuditorBackend({ catalog, logger, haikuCaller, timeoutMs });
|
|
42
50
|
}
|
|
@@ -126,6 +134,7 @@ export function selectAuditorBackend({
|
|
|
126
134
|
env = process.env,
|
|
127
135
|
projectConfig = null,
|
|
128
136
|
stage = 'user_input',
|
|
137
|
+
isCodexCliAvailable = defaultIsCodexCliAvailable,
|
|
129
138
|
} = {}) {
|
|
130
139
|
const explicit = env?.SPOTTER_AUDITOR_BACKEND;
|
|
131
140
|
const policy = env?.SPOTTER_AUDITOR_BACKEND_POLICY;
|
|
@@ -135,7 +144,7 @@ export function selectAuditorBackend({
|
|
|
135
144
|
if (explicit !== undefined && explicit !== '') {
|
|
136
145
|
assertAuditorBackend(explicit);
|
|
137
146
|
if (explicit === 'auto') {
|
|
138
|
-
return selectByPolicy({ hostAgent: effectiveHost, policy, projectConfig });
|
|
147
|
+
return selectByPolicy({ hostAgent: effectiveHost, policy, projectConfig, env, isCodexCliAvailable });
|
|
139
148
|
}
|
|
140
149
|
return {
|
|
141
150
|
backend: explicit,
|
|
@@ -145,51 +154,56 @@ export function selectAuditorBackend({
|
|
|
145
154
|
};
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
return selectByPolicy({ hostAgent: effectiveHost, policy, projectConfig });
|
|
157
|
+
return selectByPolicy({ hostAgent: effectiveHost, policy, projectConfig, env, isCodexCliAvailable });
|
|
149
158
|
}
|
|
150
159
|
|
|
151
|
-
|
|
160
|
+
// v1.4.10: Claude host = Codex CLI when detected on PATH, else Haiku.
|
|
161
|
+
// Codex host = Codex CLI unconditionally (Codex native hooks already require codex
|
|
162
|
+
// to be installed). The `SPOTTER_AUDITOR_BACKEND_POLICY` env var (`current` / `next`)
|
|
163
|
+
// is accepted for back-compat but no longer changes behavior — selection is now
|
|
164
|
+
// availability-based on both hosts. `SPOTTER_AUDITOR_BACKEND=haiku` (or any explicit
|
|
165
|
+
// backend name) still wins above this function via the explicit branch.
|
|
166
|
+
//
|
|
167
|
+
// Detection is configuration-time (synchronous PATH walk via `isCodexCliAvailable`,
|
|
168
|
+
// no spawn, no network). Once a backend is chosen, runtime failures throw
|
|
169
|
+
// `AuditorBackendError` — selection-time availability is not a runtime fallback.
|
|
170
|
+
// Phase 4 matrix smoke (2026-05-06, GeForce 5000 fixture) measured
|
|
171
|
+
// `claude.codex-cli=10041ms` vs Haiku `user_input ~14.3s / turn_end ~16.6s`, so
|
|
172
|
+
// Codex CLI wins on latency when reachable; Haiku stays as the default safety net
|
|
173
|
+
// for environments without codex on PATH.
|
|
174
|
+
function selectByPolicy({ hostAgent, policy, projectConfig, env, isCodexCliAvailable }) {
|
|
152
175
|
const effectivePolicy = policy || 'current';
|
|
153
176
|
assertAuditorPolicy(effectivePolicy);
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
backend: 'haiku',
|
|
164
|
-
mode: 'compatibility_haiku',
|
|
165
|
-
compatibility: 'current_haiku',
|
|
166
|
-
reason: `policy_current_${hostAgent}`,
|
|
167
|
-
};
|
|
177
|
+
if (hostAgent === 'unknown' || hostAgent === 'automation') {
|
|
178
|
+
throw new AuditorBackendError(
|
|
179
|
+
'E_BACKEND_HOST_UNKNOWN',
|
|
180
|
+
`explicit auditor backend required for hostAgent=${hostAgent}`,
|
|
181
|
+
{ backend: 'auto', diagnostics: { hostAgent, policy: effectivePolicy, projectConfig } }
|
|
182
|
+
);
|
|
168
183
|
}
|
|
169
184
|
if (hostAgent === 'codex') {
|
|
170
185
|
return {
|
|
171
186
|
backend: 'codex-cli',
|
|
172
187
|
mode: 'codex-cli',
|
|
173
188
|
compatibility: 'none',
|
|
174
|
-
reason: '
|
|
189
|
+
reason: 'codex_host',
|
|
175
190
|
};
|
|
176
191
|
}
|
|
177
192
|
if (hostAgent === 'claude') {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// `SPOTTER_AUDITOR_BACKEND=haiku`.
|
|
193
|
+
const codexAvailable = isCodexCliAvailable({ env });
|
|
194
|
+
if (codexAvailable) {
|
|
195
|
+
return {
|
|
196
|
+
backend: 'codex-cli',
|
|
197
|
+
mode: 'codex-cli',
|
|
198
|
+
compatibility: 'none',
|
|
199
|
+
reason: 'claude_host_codex_cli_detected',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
188
202
|
return {
|
|
189
|
-
backend: '
|
|
190
|
-
mode: '
|
|
191
|
-
compatibility: '
|
|
192
|
-
reason: '
|
|
203
|
+
backend: 'haiku',
|
|
204
|
+
mode: 'compatibility_haiku',
|
|
205
|
+
compatibility: 'current_haiku',
|
|
206
|
+
reason: 'claude_host_codex_cli_unavailable',
|
|
193
207
|
};
|
|
194
208
|
}
|
|
195
209
|
throw new AuditorBackendError(
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { posix, win32 } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Configuration-time (synchronous) check for whether `codex` is on PATH.
|
|
5
|
+
// No subprocess is spawned. The function walks env.PATH and tests each candidate
|
|
6
|
+
// with `fs.statSync`; PATHEXT-equivalent extensions are added on Windows.
|
|
7
|
+
//
|
|
8
|
+
// This is consumed by `selectByPolicy` in auditor-backend.mjs to choose the
|
|
9
|
+
// Claude-host primary auditor: when codex is reachable, the daemon prefers
|
|
10
|
+
// Codex CLI over Haiku. Once chosen, the backend's runtime failures still
|
|
11
|
+
// throw `AuditorBackendError` — selection-time availability does not become a
|
|
12
|
+
// runtime fallback.
|
|
13
|
+
|
|
14
|
+
const WINDOWS_EXTS = ['.cmd', '.exe', '.bat'];
|
|
15
|
+
|
|
16
|
+
export function isCodexCliAvailable({
|
|
17
|
+
env = process.env,
|
|
18
|
+
platform = process.platform,
|
|
19
|
+
fileExists = defaultFileExists,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const pathVar = pickPathVar(env, platform);
|
|
22
|
+
if (typeof pathVar !== 'string' || pathVar.length === 0) return false;
|
|
23
|
+
const sep = platform === 'win32' ? ';' : ':';
|
|
24
|
+
const join = platform === 'win32' ? win32.join : posix.join;
|
|
25
|
+
const candidates = platform === 'win32'
|
|
26
|
+
? WINDOWS_EXTS.map((ext) => `codex${ext}`)
|
|
27
|
+
: ['codex'];
|
|
28
|
+
for (const rawDir of pathVar.split(sep)) {
|
|
29
|
+
const dir = rawDir.trim();
|
|
30
|
+
if (dir.length === 0) continue;
|
|
31
|
+
for (const candidate of candidates) {
|
|
32
|
+
if (fileExists(join(dir, candidate))) return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pickPathVar(env, platform) {
|
|
39
|
+
if (platform === 'win32') {
|
|
40
|
+
return env?.Path ?? env?.PATH ?? env?.path ?? '';
|
|
41
|
+
}
|
|
42
|
+
return env?.PATH ?? '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultFileExists(p) {
|
|
46
|
+
try {
|
|
47
|
+
return statSync(p).isFile();
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/daemon/daemon.mjs
CHANGED
|
@@ -84,7 +84,11 @@ export async function startDaemon({
|
|
|
84
84
|
codexRiskCheckEnabled = isCodexRiskDispatchEnabled(),
|
|
85
85
|
codexRiskCheckDryRun = isCodexRiskDispatchDryRun(),
|
|
86
86
|
dispatchCodexRiskCheckFn = dispatchCodexRiskCheck,
|
|
87
|
-
|
|
87
|
+
// If a haikuCaller is explicitly injected (test path), default to haiku — the
|
|
88
|
+
// injection itself signals caller intent. Production callers never inject one,
|
|
89
|
+
// so they hit the `auto` branch which runs availability detection. Explicit
|
|
90
|
+
// SPOTTER_AUDITOR_BACKEND env or auditorBackendName override still wins above.
|
|
91
|
+
auditorBackendName = process.env.SPOTTER_AUDITOR_BACKEND || (haikuCaller ? 'haiku' : 'auto'),
|
|
88
92
|
auditorEnv = process.env,
|
|
89
93
|
stopShortFinalMaxChars = resolveStopShortFinalMaxChars(process.env),
|
|
90
94
|
} = {}) {
|
|
@@ -118,13 +122,14 @@ export async function startDaemon({
|
|
|
118
122
|
}
|
|
119
123
|
logFn(`tool-db loaded: ${toolList.length} tools` + (projectRoot ? ` (project=${projectRoot})` : ''));
|
|
120
124
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// hostAgent is hard-coded to 'claude' because this
|
|
124
|
-
// (see readLocal({hostAgent:'claude'}) above and
|
|
125
|
-
//
|
|
126
|
-
// E_BACKEND_HOST_UNKNOWN on Claude Code launches that don't set
|
|
127
|
-
// (e.g. custom wrappers), even though the daemon clearly
|
|
125
|
+
// v1.4.10: default `auto` triggers availability-based selection — Codex CLI when
|
|
126
|
+
// detected on PATH, Haiku otherwise. `SPOTTER_AUDITOR_BACKEND=haiku` (or any other
|
|
127
|
+
// explicit backend) still wins. hostAgent is hard-coded to 'claude' because this
|
|
128
|
+
// daemon process is Claude-only (see readLocal({hostAgent:'claude'}) above and
|
|
129
|
+
// dispatchCodexRiskCheck hostAgent below); without this, `auto` selection would
|
|
130
|
+
// fail with E_BACKEND_HOST_UNKNOWN on Claude Code launches that don't set
|
|
131
|
+
// CLAUDECODE/CLAUDE_CODE (e.g. custom wrappers), even though the daemon clearly
|
|
132
|
+
// belongs to a Claude host.
|
|
128
133
|
const auditorBackend = createAuditorBackend({
|
|
129
134
|
backend: auditorBackendName,
|
|
130
135
|
catalog: toolList,
|