claude-spotter 1.1.4 → 1.1.6

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,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.6
4
+
5
+ **Bell の isolated `CLAUDE_CONFIG_DIR` が Spotter haiku の auth を破壊する bug を修正**。bellbot 等で CLAUDE_CONFIG_DIR を分離する運用 (user scope MCP を流入させない隔離設計、credentials 非共有) のとき、hook → daemon → haiku の spawn 連鎖で Bell の isolated config が継承され、Spotter haiku が credentials 不在の config を読みに行き exit 1。その後同じ session-id が claude CLI 側で "already in use" と判定されて失敗が固定化し、user_input hook が非 0 exit し続けてベル本体のプロンプト処理が破綻していた。
6
+
7
+ ### 変更点
8
+
9
+ - **編集 [src/daemon/haiku-caller.mjs](src/daemon/haiku-caller.mjs)**: 純粋関数 `sanitizeHaikuEnv(baseEnv)` を named export として新設、`createHaikuCaller` の spawn env 構築時に `CLAUDE_CONFIG_DIR` を strip。haiku はデフォルト `~/.claude/` で起動する
10
+ - **編集 [src/daemon/daemon.mjs](src/daemon/daemon.mjs)**: `runHaikuJudgment` で `E_HAIKU_TIMEOUT` / `E_INTERNAL` (auth / spawn / exit != 0) 発生時も throw 前に `callHaiku.reset()` で session を rotate。haiku-caller は成功時のみ `isFirstCall=false` を倒す設計なので、失敗が続くと同じ UUID を `--session-id` で再送して claude CLI 側の "already in use" に化ける経路を塞ぐ
11
+ - **編集 [test/haiku-caller.test.mjs](test/haiku-caller.test.mjs)**: `sanitizeHaikuEnv` の回帰テスト 2 件 (strip 動作 / absent 時 no-op + 原本非破壊)
12
+ - **編集 [test/daemon.test.mjs](test/daemon.test.mjs)**: `runHaikuJudgment` が E_INTERNAL / E_HAIKU_TIMEOUT で reset() を呼ぶ回帰テスト 2 件
13
+
14
+ ### 背景
15
+
16
+ #### 指摘経路
17
+
18
+ 2026-04-20 外部指摘で、bellbot 相当の運用 (`CLAUDE_CONFIG_DIR=~/.bellbot-claude-config` + `--dangerously-skip-permissions`) で Discord → Bell ベース agent が無反応になる障害が報告された。daemon log に以下の連鎖:
19
+
20
+ ```
21
+ [13:28:57] handler error on user_input: E_INTERNAL: haiku exited with code 1:
22
+ [13:29:11] handler error on user_input: E_INTERNAL: haiku exited with code 1: Error: Session ID 4625feeb-... is already in use.
23
+ [13:30:25] handler error on user_input: E_INTERNAL: haiku exited with code 1: Error: Session ID 4625feeb-... is already in use.
24
+ ```
25
+
26
+ コード監査で (1) Bell の CLAUDE_CONFIG_DIR が spawn 継承で剥がされずに daemon / haiku まで到達していること、(2) [haiku-caller.mjs:311-313](src/daemon/haiku-caller.mjs#L311-L313) が `isFirstCall` を成功時のみ false に倒す設計のため失敗した uuid が固定化すること、の二点を実コードで確認。
27
+
28
+ #### strip 範囲をなぜ haiku-caller だけに限定したか
29
+
30
+ Spotter の catalog 調査 (`claude mcp list`, `claude mcp get`) は Bell が実際に見える MCP セットを反映する必要があるため、Bell の CLAUDE_CONFIG_DIR を尊重する。Haiku の推論エンジンだけが Spotter 側の credentials を必要とする。したがって strip は `claude -p` 呼出し (credentials-requiring call) のみで行う。`investigate-mcp.mjs` の `execClaude` や stdio MCP spawn は env 無加工継承を維持。
31
+
32
+ #### session rotate を追加した理由
33
+
34
+ CLAUDE_CONFIG_DIR の strip だけでは、将来の auth / network / quota / CLI crash 等の異なる失敗源でも同じ "session-id 固定化 → 失敗連鎖" 構造を再生産する。haiku-caller が isFirstCall を成功時のみ倒す設計である以上、daemon 側で HaikuError を掴んだ時点で必ず reset を呼ぶのが構造的な解。これは §0 silent fallback 禁止とは別軸の「unexpected でも内部 state は clean に保つ」防御。
35
+
36
+ ### 設計判断
37
+
38
+ - **`SPOTTER_CLAUDE_CONFIG_DIR` 等のユーザー向け override は未導入**: 指摘の proposed direction で副次案として挙がっていたが、現状 strip で十分解決。ユーザーが Spotter の config を別 dir にしたい具体的ユースケース (例: Spotter 専用 API key の隔離) が出たら再検討
39
+ - **他 env (`ANTHROPIC_API_KEY` 等) は strip しない**: ユーザーが明示的に設定した API key は両方に適用されるべき。strip 対象は「Bell セッション固有で、Haiku にとって誤動作源になる」 `CLAUDE_CONFIG_DIR` 一点のみ
40
+ - **reset 箇所は `runHaikuJudgment` の catch 一点に集約**: handler 毎 (handleUserInput / handleTurnEnd) に書かない。haiku 呼出しは必ず runHaikuJudgment を通る設計なのでそこで閉じる
41
+
42
+ ## 1.1.5
43
+
44
+ **Windows で refresh 毎に cmd.exe console window が flash + 入力フォーカスを奪う UX 回帰を修正**。`listMcpServers` / `getStdioConfig` が `execClaude` 経由で spawn する `cmd.exe /c claude mcp list/get` に `windowsHide: true` が付いておらず、SessionStart 毎の bg refresh と install 時 seed で毎回黒いウィンドウが一瞬表示されキーボード入力が奪われていた。
45
+
46
+ ### 変更点
47
+
48
+ - **編集 [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs)**: `execClaude` ヘルパ内で `opts` を spread した上で `windowsHide: true` を強制。呼び出し側 (`listMcpServers`, `getStdioConfig`) の `execOpts` に毎回書かせるのではなく、helper 層で固定することで将来の call site も自動で守られる。
49
+
50
+ ### 背景
51
+
52
+ Windows の `spawn` / `execFile` は `windowsHide` オプションが `false` のとき、child process の console window を visible で起動する。Spotter の spawn サイトは 6 箇所 (daemon spawn / refresh detached / haiku-caller / MCP stdio spawn / doctor / execClaude) あり、うち 5 箇所は個別に `windowsHide: true` を付けていたが、`execClaude` だけ opts 任せになっていて pass されていなかった。
53
+
54
+ SessionStart 毎の `spotter db refresh` で `listMcpServers` が 1 回、stdio MCP サーバーの数だけ `getStdioConfig` が呼ばれるため、MCP サーバー N 個の環境では SessionStart 毎に **1 + N 回** の flash が発生。加えて `spotter install` 時の seed でも同じ経路を通る。体感「結構な頻度で入力を奪われる」という UX 回帰の直接原因。
55
+
56
+ ### 設計判断
57
+
58
+ - **helper 層で windowsHide 強制**: call site 毎に書かせる方針は 2 箇所の execOpts を更新するだけで済むが、新 call site 追加時に忘れるリスクが残る。`execClaude` は外部コマンド (`claude` CLI) 専用で Windows では常に cmd.exe 経由のため、「このヘルパ経由なら silent」という不変条件を layer 内で閉じた方が防御堅牢。
59
+ - **他 5 spawn サイトの監査**: `spawn-daemon.mjs` (daemon + refresh detached), `haiku-caller.mjs` (claude -p), `investigate-mcp.mjs:spawnAndQuery` (MCP stdio), `doctor.mjs` (claude --version) はすべて `windowsHide: true` 済みを確認。この修正で残る穴はゼロ。
60
+ - **テスト追加なし**: Windows console window visibility は cross-platform ユニットテストで検証しづらい (Windows 環境でも Node の test runner 経由で spawn した child の visibility を assert する API がない)。監査対象は 6 spawn サイト全件の源コード上の `windowsHide: true` の存在のみ、これは grep で機械検証できる。
61
+
3
62
  ## 1.1.4
4
63
 
5
64
  **MCP 投資ロジックの 2 件の穴を修正**。どちらも「名乗っているスコープ」と「実際に参照されるスコープ」が一致していない silent mismatch。前者は projectRoot 引数が効かない経路、後者は baseline が現実を無視して常に 25 件投入される経路。
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Spotter
2
2
 
3
- > **v1.1.2 released 2026-04-20**. **install が tool-db を自動構築 + SessionStart で drift 自動追従**。`spotter install` 実行時に MCP / スキル / サブエージェントを discover して tool-db.json を seed、以降 Claude Code セッション起動ごとに SessionStart hook が detached で `spotter db refresh` を bg 発火 (反映は次セッション以降)。手動の `spotter db refresh` は不要に。監査対象は v1.0.0 でユーザー追加分 (MCP / スキル / サブエージェント) に絞り込み済み、本プロジェクトでの実測で 268 件 resolved (MCP 40 + skills 181 + agents/bare 47)。設計思想は [docs/catalog-design.md](docs/catalog-design.md)、変更詳細は [CHANGELOG](CHANGELOG.md)。
3
+ > **v1.1.4 released 2026-04-20**. **MCP 投資経路の 2 件の silent mismatch を修正**。(1) `claude mcp list / get` spawn 時の `cwd: projectRoot` 未指定、(2) claude.ai baseline (Gmail/Calendar/Drive 25 件) の無条件注入 — `claude mcp list` の実在確認を入れ、隔離 `CLAUDE_CONFIG_DIR` / 未連携環境で最大 25 件の幻ツールが catalog に残っていた状態を解消 (Bell 側実環境で 25 件消失を実測確認済み)。v1.1.0 からの柱 (install 時 tool-db 自動構築 + SessionStart での drift 自動追従) は継続、手動 `spotter db refresh` は通常不要。監査対象は v1.0.0 でユーザー追加分 (MCP / スキル / サブエージェント) に絞り込み済み、本プロジェクトでの実測で 268 件 resolved (MCP 40 + skills 181 + agents/bare 47)。設計思想は [docs/catalog-design.md](docs/catalog-design.md)、変更詳細は [CHANGELOG](CHANGELOG.md)。
4
4
 
5
5
  **気づく役と実行する役を分離する。** Spotter は Claude Code の横で静かに並走し、Bell (主役の Claude) が**ツールを呼び忘れたとき**に指摘する監査役です。
6
6
 
@@ -44,7 +44,7 @@ Stop hook → Spotter が応答と使用済みツールを見て最終チェッ
44
44
  見落としあれば差し戻し (max 1 回、Claude Code の stop_hook_active で自動担保)
45
45
  ```
46
46
 
47
- 監査対象のツール (name + description) は `~/.spotter/tool-db.json` (グローバル) と `<project>/.spotter/tool-db.json` (ローカル) に格納されます。**v1.1.0 以降、`spotter install` が初回 seed を自動実行し、Claude Code セッション起動ごとに SessionStart hook が bg で `spotter db refresh` を走らせる**ため、通常の運用で手動コマンドを叩く必要はありません。収集経路は (1) MCP サーバー: user/project scope の `.mcp.json` + `claude mcp list` で列挙、各サーバーの `tools/list` を JSON-RPC で取得、HTTP/SSE transport にも対応、(2) スキル: user/project/プラグインの SKILL.md frontmatter から `{name, description}` を抽出、(3) サブエージェント: user/project/プラグインの agent .md frontmatter から抽出、(4) claude.ai baseline: OAuth proxy 経由の Gmail/Calendar/Drive 25 件は手書き baseline で補完。**手書きでツールリストを管理する必要はありません**。
47
+ 監査対象のツール (name + description) は `~/.spotter/tool-db.json` (グローバル) と `<project>/.spotter/tool-db.json` (ローカル) に格納されます。**v1.1.0 以降、`spotter install` が初回 seed を自動実行し、Claude Code セッション起動ごとに SessionStart hook が bg で `spotter db refresh` を走らせる**ため、通常の運用で手動コマンドを叩く必要はありません。収集経路は (1) MCP サーバー: user/project scope の `.mcp.json` + `claude mcp list` で列挙、各サーバーの `tools/list` を JSON-RPC で取得、HTTP/SSE transport にも対応、(2) スキル: user/project/プラグインの SKILL.md frontmatter から `{name, description}` を抽出、(3) サブエージェント: user/project/プラグインの agent .md frontmatter から抽出、(4) claude.ai baseline: OAuth proxy 経由の Gmail/Calendar/Drive 25 件は手書き baseline で補完 (v1.1.4 以降、`claude mcp list` に該当サーバーが存在する環境でのみ注入)。**手書きでツールリストを管理する必要はありません**。
48
48
 
49
49
  ## Throughline との関係
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -136,9 +136,27 @@ export async function startDaemon({
136
136
 
137
137
  // v0.5.0: shared Haiku-invocation + parse helper. On E_HAIKU_SCHEMA (role collapse),
138
138
  // rotates the Haiku session-id and silent-passes the turn (reason: role_collapse_reset).
139
- // Other Haiku errors (timeout, spawn failure) still propagate — §14 unexpected → throw.
139
+ //
140
+ // v1.1.6: E_HAIKU_TIMEOUT / E_INTERNAL (spawn failure, auth failure, exit != 0) は
141
+ // §14 unexpected として throw するが、throw の前に必ず callHaiku.reset() で session を
142
+ // 回転する。haiku-caller は成功時のみ isFirstCall=false を倒す設計なので、失敗が続くと
143
+ // 同じ UUID を `--session-id` 再送 → claude CLI 側で "Session ID ... is already in use"
144
+ // に化けて失敗連鎖が固定化していた。reset で次 turn が fresh id + preamble 再送から
145
+ // やり直せるようにする (真因 — 例えば CLAUDE_CONFIG_DIR 誤継承による auth 失敗 — が
146
+ // 解消されたら即回復可能な状態を保つ)。silent fallback 新規導入ではない、§0 「想定済み
147
+ // 異常 = 記録 + 正常リターン」とは別軸の「unexpected でも内部 state は clean に保つ」
148
+ // 防御。
140
149
  const runHaikuJudgment = async (stage, prompt) => {
141
- const { raw, meta } = await callHaikuTracked(prompt);
150
+ let raw, meta;
151
+ try {
152
+ ({ raw, meta } = await callHaikuTracked(prompt));
153
+ } catch (err) {
154
+ if (err instanceof HaikuError && typeof callHaiku.reset === 'function') {
155
+ logFn(`${stage}: haiku invocation failed (${err.code}), rotating session before rethrow: ${err.message}`);
156
+ callHaiku.reset();
157
+ }
158
+ throw err;
159
+ }
142
160
  let parsed;
143
161
  try {
144
162
  parsed = parseHaikuResponse(raw);
@@ -219,6 +219,18 @@ function truncate(s, n = 300) {
219
219
  return s.slice(0, n) + '...';
220
220
  }
221
221
 
222
+ // Bell の isolated CLAUDE_CONFIG_DIR (例: bellbot プロファイル) が hook → daemon → haiku の
223
+ // spawn 連鎖で継承されると、Spotter haiku が credentials 不在の config を読みに行き exit 1。
224
+ // その後 session-id が「already in use」で stuck して user_input hook が非 0 exit し続ける。
225
+ // v1.1.6: spawn env 構築時に CLAUDE_CONFIG_DIR を剥がし、デフォルト ~/.claude/ で Haiku を
226
+ // 起動する。監査対象の Claude CLI 側 (Bell の env で走る `claude mcp list` 等) は意図通り
227
+ // Bell の config を参照するため、strip はこの一点 (credentials が必要な claude -p 呼出し)
228
+ // のみで行う。
229
+ export function sanitizeHaikuEnv(baseEnv) {
230
+ const { CLAUDE_CONFIG_DIR: _strip, ...rest } = baseEnv;
231
+ return rest;
232
+ }
233
+
222
234
  // On Windows, the `claude` entry is typically a .cmd shim which Node's spawn cannot locate
223
235
  // without going through the shell. We use cmd.exe /c explicitly rather than spawn({ shell:
224
236
  // true }) because the latter triggers DEP0190 on Node 24+.
@@ -264,7 +276,7 @@ export function createHaikuCaller({ preamble, timeoutMs, claudeBin = 'claude', m
264
276
  });
265
277
  const child = spawn(cmd, cmdArgs, {
266
278
  cwd: WORKDIR,
267
- env: { ...env, SPOTTER_PARENT_PID: String(process.pid) },
279
+ env: { ...sanitizeHaikuEnv(env), SPOTTER_PARENT_PID: String(process.pid) },
268
280
  stdio: ['pipe', 'pipe', 'pipe'],
269
281
  windowsHide: true,
270
282
  });
@@ -23,11 +23,15 @@ const HANDSHAKE_TIMEOUT_MS = 10_000;
23
23
  // On Windows, `claude` is a .cmd shim; Node's execFile cannot locate it directly without
24
24
  // going through cmd.exe. Matches the pattern in src/daemon/haiku-caller.mjs buildSpawnArgs.
25
25
  // We use cmd.exe /c rather than shell:true to avoid DEP0190 on Node 24+.
26
+ // `windowsHide: true` is forced at this layer so every caller (listMcpServers,
27
+ // getStdioConfig, etc.) is silent — without it a cmd.exe console window flashes on every
28
+ // refresh, and those flashes steal keyboard focus on Windows.
26
29
  async function execClaude(claudeBin, args, opts) {
30
+ const execOpts = { ...opts, windowsHide: true };
27
31
  if (process.platform === 'win32') {
28
- return execFileP('cmd.exe', ['/c', claudeBin, ...args], opts);
32
+ return execFileP('cmd.exe', ['/c', claudeBin, ...args], execOpts);
29
33
  }
30
- return execFileP(claudeBin, args, opts);
34
+ return execFileP(claudeBin, args, execOpts);
31
35
  }
32
36
 
33
37
  export class McpInvestigationError extends Error {