claude-spotter 1.4.9 → 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 CHANGED
@@ -1,5 +1,54 @@
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
+
3
52
  ## 1.4.9
4
53
 
5
54
  **Codex hooks feature 名の現行 CLI 追従**。現行 Codex CLI の `codex features list` は
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.4.9",
3
+ "version": "1.4.10",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 || (process.env.SPOTTER_AUDITOR_BACKEND_POLICY ? 'auto' : 'haiku'),
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];
@@ -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({ hostAgent, env, projectConfig: projectRoot ? { projectRoot } : null })
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
- function selectByPolicy({ hostAgent, policy, projectConfig }) {
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 (effectivePolicy === 'current') {
155
- if (hostAgent === 'unknown' || hostAgent === 'automation') {
156
- throw new AuditorBackendError(
157
- 'E_BACKEND_HOST_UNKNOWN',
158
- `explicit auditor backend required for hostAgent=${hostAgent}`,
159
- { backend: 'auto', diagnostics: { hostAgent, policy: effectivePolicy, projectConfig } }
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: 'policy_next_codex_host',
189
+ reason: 'codex_host',
175
190
  };
176
191
  }
177
192
  if (hostAgent === 'claude') {
178
- // Phase 5: Claude host opt-in `next` policy promotes the primary auditor backend
179
- // from Haiku to Codex CLI. Phase 4 matrix smoke (2026-05-06, GeForce 5000 fixture)
180
- // measured `claude.codex-cli=10041ms` vs `claude.codex-sidecar=12863ms` and Haiku
181
- // diagnostics averaged `user_input ~14.3s / turn_end ~16.6s`, so Codex CLI wins on
182
- // latency without giving up schema-fixed JSON judgment. Hidden fallback is
183
- // forbidden — when codex-cli is unavailable / times out / exits non-zero,
184
- // `createCodexCliAuditorBackend` throws `AuditorBackendError` and the daemon
185
- // surfaces the structured error instead of dropping back to Haiku.
186
- // Haiku stays reachable only via `current` policy or
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: 'codex-cli',
190
- mode: 'codex-cli',
191
- compatibility: 'none',
192
- reason: 'policy_next_claude_codex_cli',
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
+ }
@@ -84,7 +84,11 @@ export async function startDaemon({
84
84
  codexRiskCheckEnabled = isCodexRiskDispatchEnabled(),
85
85
  codexRiskCheckDryRun = isCodexRiskDispatchDryRun(),
86
86
  dispatchCodexRiskCheckFn = dispatchCodexRiskCheck,
87
- auditorBackendName = process.env.SPOTTER_AUDITOR_BACKEND || (process.env.SPOTTER_AUDITOR_BACKEND_POLICY ? 'auto' : 'haiku'),
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
- // Phase 1 primary backend interface: default remains Haiku, but daemon now calls through
122
- // an auditor adapter so future Codex CLI / sidecar backends share one judgment surface.
123
- // hostAgent is hard-coded to 'claude' because this daemon process is Claude-only
124
- // (see readLocal({hostAgent:'claude'}) above and dispatchCodexRiskCheck hostAgent below).
125
- // Without this, SPOTTER_AUDITOR_BACKEND_POLICY=next opt-in would fail with
126
- // E_BACKEND_HOST_UNKNOWN on Claude Code launches that don't set CLAUDECODE/CLAUDE_CODE
127
- // (e.g. custom wrappers), even though the daemon clearly belongs to a Claude host.
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,