claude-spotter 0.12.0 → 0.13.1
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 +49 -0
- package/package.json +1 -1
- package/src/daemon/daemon.mjs +8 -6
- package/src/daemon/haiku-caller.mjs +33 -9
- package/src/hooks/stop.mjs +1 -1
- package/src/hooks/user-prompt.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.1
|
|
4
|
+
|
|
5
|
+
**Haiku timeout 30s → 45s 緩和 + hook 側 IPC timeout を整合**。v0.13.0 以前の実セッション ([daemon-80b5c0af.log](../../.spotter/logs/daemon-80b5c0af-700f-47af-a3ac-796144823a7d.log) line 15) で `E_HAIKU_TIMEOUT: haiku did not respond within 30000ms` を観測。同ログ line 20 でも `mode=first, duration_ms=20948` と 30s の 70% 域まで達しており、timeout が実測レイテンシに対して狭すぎた。合わせて [src/hooks/stop.mjs](src/hooks/stop.mjs) の IPC timeout が元々 15s で Haiku 側 30s と整合していなかった既存バグ (turn_end で Haiku が 16s 超かかると hook 側が先に諦めていた) も同時解消。
|
|
6
|
+
|
|
7
|
+
### 調査で判明した Haiku 4.5 の高速化ダイヤル不在
|
|
8
|
+
|
|
9
|
+
公式 docs 確認 (2026-04-20):
|
|
10
|
+
|
|
11
|
+
- `--effort` フラグは Opus 4.7 / Opus 4.6 / Sonnet 4.6 のみ対応、**Haiku 4.5 は effort 非対応** ([model-config docs](https://code.claude.com/docs/en/model-config#adjust-effort-level))
|
|
12
|
+
- Haiku 4.5 は **extended thinking 対応だが adaptive thinking 非対応** ([models/overview](https://platform.claude.com/docs/en/docs/about-claude/models/overview) 比較表)
|
|
13
|
+
- `claude -p` CLI に thinking 直接フラグ無し、API デフォルトで thinking は OFF
|
|
14
|
+
- つまり Haiku 側で「速くする手段」は存在せず、timeout 緩和しか打ち手が無い (Sonnet 4.6 切替は遅くなる + 3 倍コストで逆効果)
|
|
15
|
+
|
|
16
|
+
### 変更点
|
|
17
|
+
|
|
18
|
+
- **編集 [src/daemon/daemon.mjs](src/daemon/daemon.mjs)**: `DEFAULT_HAIKU_TIMEOUT_MS` を 30_000 → 45_000。first-call cold-start + 観測されたレイテンシスパイク (20.9s) + margin を包含
|
|
19
|
+
- **編集 [src/hooks/user-prompt.mjs](src/hooks/user-prompt.mjs)**: `TIMEOUT_MS` を 30_000 → 50_000 (Haiku 45s + IPC margin 5s)
|
|
20
|
+
- **編集 [src/hooks/stop.mjs](src/hooks/stop.mjs)**: `TIMEOUT_MS` を 15_000 → 50_000 (既存の整合性バグも解消)
|
|
21
|
+
|
|
22
|
+
### 残課題
|
|
23
|
+
|
|
24
|
+
Haiku 突然死 (shutdown ログなしで daemon 再起動する事象、v0.12.0 auto-resurrect で救われているが見えない欠落ターンが発生する) は未対処。[docs/open-issues.md](docs/open-issues.md) P0 に残置。
|
|
25
|
+
|
|
26
|
+
## 0.13.0
|
|
27
|
+
|
|
28
|
+
**Stop 判定軸を「要請充足チェック」から「ツール適用機会の監査」に転換**。v0.12.x 以前の `stage=turn_end` は `<user_input>` + `<used_tools>` + `<final_response>` を Haiku に渡し、「ユーザー要請されたツールが使われたか」を判定していた。この軸では Bell が Stop 到達後にすべき動作 — 事実断定の裏付け / 新知見の記録 / 既知情報の照会 — を拾えない。実セッションで Haiku が応答「判明: A モジュールは B に依存」に対し `caveat_record` を推奨する、といった本来期待される指摘が構造的に出ない状態だった (ユーザーが [この議論](https://github.com/kitepon-rgb/Spotter) で指摘)。
|
|
29
|
+
|
|
30
|
+
### 新軸: ツール適用機会の監査
|
|
31
|
+
|
|
32
|
+
- **入力**: `<used_tools>` + `<final_response>` のみ (user_input は削除)
|
|
33
|
+
- **問い**: 「この応答で、カタログ上のツールが役立つ箇所ないか?」
|
|
34
|
+
- **3 カテゴリ**: 検証 (Read/Grep/Bash/WebFetch 等) / 登録 (memory/caveat 等) / 照会 (search/list 等)
|
|
35
|
+
- **非対称**: 指摘ゼロは歓迎、`used_tools` 既含は再指摘しない、迷ったら pass:true
|
|
36
|
+
|
|
37
|
+
挨拶ターンの早期 pass は daemon 側の `state.lastUserInput === null` 分岐で従来通り機能する (user_input が来ていないターンは turn_end で `reason=no_user_input` で pass)。Stop hook の入力契約 (`final_response` のみを daemon に送信) は v0.4.4 時点で user_input を送っていないので hook 側の変更なし。
|
|
38
|
+
|
|
39
|
+
### 変更点
|
|
40
|
+
|
|
41
|
+
- **編集 [src/daemon/haiku-caller.mjs](src/daemon/haiku-caller.mjs)**: `SHARED_HEADER` の stage=turn_end 説明を書き換え、few-shot を 4 件 (検証/登録/照会/pass) に拡張。`buildFinalStagePrompt` から `userInput` 引数を削除、`<user_input>` タグも削除
|
|
42
|
+
- **編集 [src/daemon/daemon.mjs](src/daemon/daemon.mjs)**: `handleTurnEnd` の `buildFinalStagePrompt` 呼び出しから `userInput` 引数削除、`savedUserInput` 変数削除。`state.lastUserInput` は `no_user_input` pass 分岐用に保持継続 (コメントで意図明記)
|
|
43
|
+
- **編集 [test/haiku-caller.test.mjs](test/haiku-caller.test.mjs)**: `buildFinalStagePrompt` の 3 テストから `userInput:` 引数削除、`<user_input>` タグ包含アサートを非包含アサートに反転
|
|
44
|
+
- **編集 [test/daemon.test.mjs](test/daemon.test.mjs)**: turn_end の per-turn prompt に `<user_input>` タグも user 発言原文も含まれないことを確認するテストを 1 件追加
|
|
45
|
+
- **編集 [CLAUDE.md](CLAUDE.md)**: Product Concept に「判定軸 (v0.13.0 で 2 軸化)」セクションを追加、user_input=要請充足チェック / turn_end=ツール適用機会の監査 を明記
|
|
46
|
+
|
|
47
|
+
### 非互換
|
|
48
|
+
|
|
49
|
+
- **判定挙動の意味論変更**: v0.12.x までの「user_input 要請に対応するツール」しか指摘しなかった Stop hook が、v0.13.0 からは user_input 非依存で「応答に対する適用機会」を指摘する。false positive / false negative の方向性も変わるため、過検出率 / pass 率の再計測が必要 ([docs/open-issues.md](docs/open-issues.md) P0 に観測タスクを追加)
|
|
50
|
+
- **API 変更**: `buildFinalStagePrompt({ userInput, usedTools, finalResponse })` → `buildFinalStagePrompt({ usedTools, finalResponse })`。外部から直接呼ばれる API ではない (daemon 内部) ため影響範囲は Spotter 本体のみ
|
|
51
|
+
|
|
3
52
|
## 0.12.0
|
|
4
53
|
|
|
5
54
|
**親 PID watch を heartbeat 方式に置換 + UserPromptSubmit auto-resurrect**。v0.6.2 で導入した `--parent-pid` watch (Claude Code 本体 PID を `process.kill(pid, 0)` で 5 秒間隔 ping) が VSCode native extension 環境で誤爆する問題を解消。`process.ppid` は extension host から spawn される短命ラッパーを指していて、5 秒で ESRCH → daemon 自死していた (`~/.spotter/logs/ppid-probe.log` の env dump で実測: hook の ppid が毎回 (55692, 46020 等) 変わるのに対し `VSCODE_PID=39964` は固定、CLAUDE_* 系には PID 系 env なし)。
|
package/package.json
CHANGED
package/src/daemon/daemon.mjs
CHANGED
|
@@ -47,10 +47,11 @@ import { join } from 'node:path';
|
|
|
47
47
|
import { writeFile, unlink } from 'node:fs/promises';
|
|
48
48
|
const DEFAULT_HAIKU_CALL_WINDOW_MS = 10_000;
|
|
49
49
|
// v0.5.0: lowered 60s → 30s. Session-scoped (--resume) means the first call still pays
|
|
50
|
-
// cold-start but subsequent calls skip it.
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
50
|
+
// cold-start but subsequent calls skip it. 45s covers first-call cold path plus the
|
|
51
|
+
// observed Haiku CLI latency spikes (2026-04-20 log shows 20.9s resumed calls and 30s
|
|
52
|
+
// timeouts in the wild). Role-collapse recovery (reset → next call is effectively a
|
|
53
|
+
// cold start again) stays within budget.
|
|
54
|
+
const DEFAULT_HAIKU_TIMEOUT_MS = 45_000;
|
|
54
55
|
// v0.12.0: heartbeat-based orphan cleanup. Every envelope resets a setTimeout; if no
|
|
55
56
|
// hook event arrives within this window, the daemon self-shuts. 30 min is the longest
|
|
56
57
|
// silence we expect from a live Claude Code session. UserPromptSubmit auto-resurrects
|
|
@@ -258,12 +259,13 @@ export async function startDaemon({
|
|
|
258
259
|
return { pass: true, missing_tools: [], reason: 'no_user_input' };
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
|
|
262
|
+
// v0.13.0: state.lastUserInput は turn_end の Haiku 判定には渡さない (新軸は
|
|
263
|
+
// final_response + used_tools のみで判定)。ただし「挨拶ターン (user_input が来て
|
|
264
|
+
// いない) は早期 pass」の分岐は上で使うので保存は引き続き必要。
|
|
262
265
|
const savedUsedTools = state.usedTools.slice();
|
|
263
266
|
const { parsed, meta } = await runHaikuJudgment(
|
|
264
267
|
'turn_end',
|
|
265
268
|
buildFinalStagePrompt({
|
|
266
|
-
userInput: savedUserInput,
|
|
267
269
|
usedTools: savedUsedTools,
|
|
268
270
|
finalResponse,
|
|
269
271
|
})
|
|
@@ -38,6 +38,11 @@ export async function ensureWorkdir() {
|
|
|
38
38
|
|
|
39
39
|
// Shared header covers BOTH stages — the preamble documents stage=user_input and
|
|
40
40
|
// stage=turn_end so per-turn prompts only need to announce which stage they are.
|
|
41
|
+
//
|
|
42
|
+
// v0.13.0: stage=turn_end の判定軸を「要請充足チェック」から「ツール適用機会の監査」に転換。
|
|
43
|
+
// 旧軸は <user_input> に対し used_tools が足りているかをチェックしていた。新軸は
|
|
44
|
+
// <final_response> の内容 (事実断定 / 記録すべき新情報 / 既知情報の参照) に対し、カタログ
|
|
45
|
+
// 上のツール (検証 / 登録 / 照会) を差し込める余地を探す。非対称 (指摘ゼロ歓迎) 設計。
|
|
41
46
|
const SHARED_HEADER = [
|
|
42
47
|
'あなたは Spotter。Bell (主役の Claude) が呼び忘れるツールを検出する監査役です。',
|
|
43
48
|
'ユーザーへの会話文は生成せず、必ず下記 JSON のみを返します。',
|
|
@@ -49,13 +54,32 @@ const SHARED_HEADER = [
|
|
|
49
54
|
'',
|
|
50
55
|
'## 判定対象',
|
|
51
56
|
'各ターン、以下いずれかの stage で判定リクエストを受けます:',
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
57
|
+
'',
|
|
58
|
+
'### stage=user_input',
|
|
59
|
+
'<user_input> のみ届く。when_to_use に明確に該当するツールを列挙。',
|
|
60
|
+
'推測禁止。該当なしなら pass:true。',
|
|
61
|
+
'',
|
|
62
|
+
'### stage=turn_end (ツール適用機会の監査)',
|
|
63
|
+
'<final_response> + <used_tools> が届く。',
|
|
64
|
+
'Bell の応答に含まれる動作 — 事実の断定 / 記録すべき新情報 / 既知情報の参照 —',
|
|
65
|
+
'それぞれについて、カタログに役立つツールがあれば提示する。',
|
|
66
|
+
'検証 (Read/Grep/Bash/WebFetch 等) / 登録 (memory/caveat 等) / 照会 (search/list 等) のいずれも対象。',
|
|
67
|
+
'<used_tools> に既に含まれるツールは再指摘しない。',
|
|
68
|
+
'指摘ゼロは歓迎。迷ったら pass:true。',
|
|
55
69
|
'',
|
|
56
70
|
'## 例',
|
|
57
|
-
'- stage=user_input "今何時?"
|
|
58
|
-
'
|
|
71
|
+
'- stage=user_input "今何時?"',
|
|
72
|
+
' → {"pass":false,"missing_tools":[{"name":"current_time","reason":"時刻の直接質問"}]}',
|
|
73
|
+
'- stage=user_input "ありがとう"',
|
|
74
|
+
' → {"pass":true,"missing_tools":[]}',
|
|
75
|
+
'- stage=turn_end 応答「この関数は配列長を返します」(used:なし) ← 検証',
|
|
76
|
+
' → {"pass":false,"missing_tools":[{"name":"Read","reason":"関数実装の断定は実ファイル読取で裏付けるべき"}]}',
|
|
77
|
+
'- stage=turn_end 応答「判明: A モジュールは B に依存」(used:Grep) ← 登録',
|
|
78
|
+
' → {"pass":false,"missing_tools":[{"name":"mcp__caveat__caveat_record","reason":"新発見の依存関係は記録して次回参照可能にすべき"}]}',
|
|
79
|
+
'- stage=turn_end 応答「この話題は前にも議論したはず」(used:なし) ← 照会',
|
|
80
|
+
' → {"pass":false,"missing_tools":[{"name":"mcp__caveat__caveat_search","reason":"過去の議論参照は検索して裏付けるべき"}]}',
|
|
81
|
+
'- stage=turn_end 応答「作業完了しました」(used:Read,Edit,Bash) ← pass',
|
|
82
|
+
' → {"pass":true,"missing_tools":[]}',
|
|
59
83
|
].join('\n');
|
|
60
84
|
|
|
61
85
|
// Preamble — sent exactly once per Haiku session (first call). Contains the role,
|
|
@@ -92,13 +116,13 @@ export function buildFirstStagePrompt({ userInput }) {
|
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
// Per-turn prompt — Stop hook stage.
|
|
95
|
-
|
|
119
|
+
// v0.13.0: user_input は渡さない。判定軸が「ユーザー要請の充足」から「final_response に
|
|
120
|
+
// ツール適用機会があるか」に変わったため。挨拶ターンの早期 pass は daemon 側の
|
|
121
|
+
// state.lastUserInput === null 分岐 (handleTurnEnd) で処理する。
|
|
122
|
+
export function buildFinalStagePrompt({ usedTools, finalResponse }) {
|
|
96
123
|
const usedList = usedTools.length > 0 ? usedTools.map((t) => `- ${t}`).join('\n') : '(なし)';
|
|
97
124
|
return [
|
|
98
125
|
'stage=turn_end',
|
|
99
|
-
'<user_input>',
|
|
100
|
-
userInput,
|
|
101
|
-
'</user_input>',
|
|
102
126
|
'<used_tools>',
|
|
103
127
|
usedList,
|
|
104
128
|
'</used_tools>',
|
package/src/hooks/stop.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { getLastAssistantText } from './transcript-reader.mjs';
|
|
16
16
|
import { sendRequest } from '../daemon/transport.mjs';
|
|
17
17
|
|
|
18
|
-
const TIMEOUT_MS =
|
|
18
|
+
const TIMEOUT_MS = 50_000;
|
|
19
19
|
|
|
20
20
|
export async function runStop() {
|
|
21
21
|
if (isChildCall()) return;
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import { sendRequest, TransportError } from '../daemon/transport.mjs';
|
|
22
22
|
import { spawnDaemonAndWaitReady } from './spawn-daemon.mjs';
|
|
23
23
|
|
|
24
|
-
const TIMEOUT_MS =
|
|
24
|
+
const TIMEOUT_MS = 50_000;
|
|
25
25
|
const SHORT_PROMPT_MAX_CHARS = 10;
|
|
26
26
|
|
|
27
27
|
export async function runUserPrompt() {
|