claude-spotter 0.4.3 → 0.4.4
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 +21 -0
- package/package.json +1 -1
- package/src/hooks/stop.mjs +7 -3
- package/src/hooks/transcript-reader.mjs +57 -0
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.4
|
|
4
|
+
|
|
5
|
+
**Stop hook が Bell の最終応答を Haiku に正しく渡すよう修正**。
|
|
6
|
+
|
|
7
|
+
### 事の発端
|
|
8
|
+
|
|
9
|
+
「Stop hook で Haiku に渡されるのは Bell の最終応答だけか、それとも thinking も含む膨大なテキストか」というユーザーからの疑問で調査したところ、**実は何も渡していなかった**ことが判明。
|
|
10
|
+
|
|
11
|
+
[src/hooks/stop.mjs](src/hooks/stop.mjs) は `input.final_response` フィールドを読んでいたが、Claude Code の Stop hook が stdin に渡す標準入力にそんなフィールドは存在しない。標準は `transcript_path` (セッションの JSONL ログファイルパス)。結果、`optionalString` が常に null を返し、daemon には **`'(no final response provided)'`** という sentinel 文字列だけが送られ続けていた。turn_end の Haiku 判定は Bell の応答を 1 文字も見ずに行われていた (したがって Stop 段階でのツール呼び忘れ検出は実質機能していなかった)。
|
|
12
|
+
|
|
13
|
+
### 変更点
|
|
14
|
+
|
|
15
|
+
- **[src/hooks/transcript-reader.mjs](src/hooks/transcript-reader.mjs) 新設**: transcript JSONL の末尾から assistant の text ブロックだけを抽出する `getLastAssistantText()`。Throughline (MIT, 同作者) から必要最小限を移植。**thinking ブロック / tool_use ブロックは除外**されるので、ユーザーに見えた最終応答テキストだけが Haiku に渡る。
|
|
16
|
+
- **[src/hooks/stop.mjs](src/hooks/stop.mjs) 書き換え**: `input.final_response` (存在しないフィールド) を廃止、`input.transcript_path` (§0 に従い `requireString` で必須化) から `getLastAssistantText()` 経由で最終応答を取得。
|
|
17
|
+
- **[test/transcript-reader.test.mjs](test/transcript-reader.test.mjs) 追加**: 8 ケース。thinking 除外、tool_use スキップ、複数 text ブロックの連結、欠損ファイル、partial JSONL 末尾 (書き込み途中) への耐性等。
|
|
18
|
+
|
|
19
|
+
### 効果
|
|
20
|
+
|
|
21
|
+
- Stop hook の Haiku 判定が Bell の最終応答を実際に読むようになる → ツール呼び忘れ検出 (Stop 段階) が設計通り動く。
|
|
22
|
+
- thinking は渡らないので内部推論の漏洩リスクなし。
|
|
23
|
+
|
|
3
24
|
## 0.4.3
|
|
4
25
|
|
|
5
26
|
**Haiku プロンプトの最小化**。
|
package/package.json
CHANGED
package/src/hooks/stop.mjs
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import {
|
|
6
6
|
readStdinJson,
|
|
7
7
|
requireString,
|
|
8
|
-
optionalString,
|
|
9
8
|
exitCodeFor,
|
|
10
9
|
die,
|
|
11
10
|
formatTransparentBlockReason,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
isSubagentCall,
|
|
14
13
|
isOutsideSpotterProject,
|
|
15
14
|
} from './lib.mjs';
|
|
15
|
+
import { getLastAssistantText } from './transcript-reader.mjs';
|
|
16
16
|
import { sendRequest } from '../daemon/transport.mjs';
|
|
17
17
|
|
|
18
18
|
const TIMEOUT_MS = 15_000;
|
|
@@ -24,9 +24,13 @@ export async function runStop() {
|
|
|
24
24
|
if (isOutsideSpotterProject(input)) return;
|
|
25
25
|
|
|
26
26
|
const sessionId = requireString(input, 'session_id');
|
|
27
|
+
const transcriptPath = requireString(input, 'transcript_path');
|
|
27
28
|
const stopHookActive = input.stop_hook_active === true;
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
// Extract only the visible assistant text (thinking and tool_use blocks excluded).
|
|
31
|
+
// null when the transcript has no assistant text yet — pass a sentinel so the
|
|
32
|
+
// daemon's schema (final_response: string) is still satisfied.
|
|
33
|
+
const finalResponse = getLastAssistantText(transcriptPath) ?? '(no final response available)';
|
|
30
34
|
|
|
31
35
|
let response;
|
|
32
36
|
try {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Transcript JSONL reader — extracts the final assistant text from Claude Code's
|
|
2
|
+
// transcript file. Used by Stop hook to pass *only* the visible response
|
|
3
|
+
// (not thinking, not tool_use) to the Haiku judge.
|
|
4
|
+
//
|
|
5
|
+
// JSONL format (verified against Claude Code transcripts):
|
|
6
|
+
// {type: "user", message: {role: "user", content: [{type:"text", text:"..."}]}, ...}
|
|
7
|
+
// {type: "assistant", message: {role: "assistant", content: [
|
|
8
|
+
// {type:"thinking", thinking:"..."}, // MUST be excluded
|
|
9
|
+
// {type:"tool_use", ...}, // MUST be excluded
|
|
10
|
+
// {type:"text", text:"..."}, // what we want
|
|
11
|
+
// ]}, ...}
|
|
12
|
+
//
|
|
13
|
+
// Ported from Throughline (MIT, same author) src/transcript-reader.mjs.
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
16
|
+
|
|
17
|
+
// Concatenate only text blocks — thinking / tool_use / image are dropped.
|
|
18
|
+
function extractText(content) {
|
|
19
|
+
if (typeof content === 'string') return content;
|
|
20
|
+
if (!Array.isArray(content)) return '';
|
|
21
|
+
return content
|
|
22
|
+
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
23
|
+
.map((b) => b.text)
|
|
24
|
+
.join('');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Walk the JSONL backwards and return the text of the last assistant entry
|
|
28
|
+
// that has at least one non-empty text block. Returns null if no such entry
|
|
29
|
+
// exists or the file is missing.
|
|
30
|
+
export function getLastAssistantText(transcriptPath) {
|
|
31
|
+
if (typeof transcriptPath !== 'string' || transcriptPath.length === 0) return null;
|
|
32
|
+
if (!existsSync(transcriptPath)) return null;
|
|
33
|
+
|
|
34
|
+
const raw = readFileSync(transcriptPath, 'utf8');
|
|
35
|
+
const lines = raw.split('\n');
|
|
36
|
+
|
|
37
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
38
|
+
const trimmed = lines[i].trim();
|
|
39
|
+
if (!trimmed) continue;
|
|
40
|
+
|
|
41
|
+
let entry;
|
|
42
|
+
try {
|
|
43
|
+
entry = JSON.parse(trimmed);
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (entry.type !== 'assistant') continue;
|
|
49
|
+
const msg = entry.message;
|
|
50
|
+
if (!msg || msg.role !== 'assistant') continue;
|
|
51
|
+
|
|
52
|
+
const text = extractText(msg.content);
|
|
53
|
+
if (text.length > 0) return text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
package/src/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.4.
|
|
1
|
+
export const version = '0.4.4';
|