claude-spotter 0.13.2 → 0.13.3
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 +22 -0
- package/package.json +1 -1
- package/src/daemon/daemon.mjs +13 -1
- package/src/daemon/haiku-caller.mjs +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.3
|
|
4
|
+
|
|
5
|
+
**カタログ外ツール名の推奨を遮断 (prompt 明示 + 事後 filter の二重防御)**。v0.13.2 リリース直後の実セッション ([daemon-f047521c.log](../../.spotter/logs/daemon-f047521c-9cce-4822-9555-90b206b8341e.log) line 9) で `turn_end: pass=false, missing=Skill(tl)` を観測。**`Skill(tl)` はカタログ (tool-db.json 57 件) に存在しない**。Haiku が training 記憶 or few-shot の `current_time` / `Skill` 表記から cargo-cult してカタログ外名を提案していた。これが恒常化するとユーザーが無効な推奨に混乱する + /tl など description を直しても Haiku は参照していないため修正が届かない、という構造問題になる。
|
|
6
|
+
|
|
7
|
+
### 変更点
|
|
8
|
+
|
|
9
|
+
- **編集 [src/daemon/haiku-caller.mjs](src/daemon/haiku-caller.mjs)**: `SHARED_HEADER` に「name は**カタログに列挙されたツール名そのまま**のみ許可」ルールを明記。カタログ外 (Skill(xxx) / 任意スラッシュコマンド / 記憶した既知ツール) は禁止、該当なければ pass:true
|
|
10
|
+
- **編集 [src/daemon/haiku-caller.mjs](src/daemon/haiku-caller.mjs)**: `filterCatalogMisses(parsed, catalogNames)` を export。parse 後の post-filter として、`missing_tools[].name` がカタログ外のエントリを drop する。全削除なら `pass=true, reason='hallucination_filtered'` に flip、部分削除なら valid 分だけ残し `pass=false` 維持
|
|
11
|
+
- **編集 [src/daemon/daemon.mjs](src/daemon/daemon.mjs)**: `startDaemon` が tool-db ロード時に `catalogNames = new Set(toolList.map(t => t.name))` を構築、`runHaikuJudgment` で `parseHaikuResponse` 後に filter を適用。drop した name はログに残す (`dropped catalog-external names: ...`)
|
|
12
|
+
- **テスト追加**: `filterCatalogMisses` の 4 ケース (passthrough / 全 drop / 部分 drop / array 形式 catalog) + preamble 文言 smoke test + daemon 統合 2 ケース (全ハルシ → pass flip / 混在 → valid 残し)
|
|
13
|
+
|
|
14
|
+
### 設計判断
|
|
15
|
+
|
|
16
|
+
- **prompt + filter の二重化**: prompt だけだと Haiku が従わない場合に素通りする。filter だけだと今後 preamble をいじる人が rule を外しても気付けない。両方ある方が安全
|
|
17
|
+
- **pass flip のセマンティクス**: 全 drop 時に pass:false のまま空配列を返すと v0.5.x で導入した schema 整合性チェック (`pass:false かつ missing_tools 空は inconsistent`) に引っかかる。`pass:true, reason='hallucination_filtered'` が正解
|
|
18
|
+
- **§0 silent fallback 禁止との関係**: これは「想定外を黙って潰す」ではなく「想定内の誤検出 = 記録 + 正常リターン」。dropped name はログに必ず残る
|
|
19
|
+
|
|
20
|
+
### 残課題
|
|
21
|
+
|
|
22
|
+
- v0.13.0 新軸の**カタログ内過検出** (Read 乱発 / caveat 誤爆等) は別問題。[docs/open-issues.md](docs/open-issues.md) の P0 観測タスクとして継続
|
|
23
|
+
- few-shot 例の `current_time` は現 tool-db に無い名前。Haiku が cargo-cult するリスクを filter で潰したが、例そのものを実在ツールに差し替えるかは要検討 (ただし例の抽象性が失われる tradeoff あり)
|
|
24
|
+
|
|
3
25
|
## 0.13.2
|
|
4
26
|
|
|
5
27
|
**Daemon の死因を必ずログに残す診断インフラ + Haiku 子プロセス stdio の防御的 error listener**。v0.13.1 までは daemon が `uncaughtException` / `unhandledRejection` で死ぬと痕跡ゼロで消えていた ([daemon-80b5c0af.log](../../.spotter/logs/daemon-80b5c0af-700f-47af-a3ac-796144823a7d.log) line 15 → line 16 で shutdown ログなしに再起動)。次に同じことが起きた時に真因を必ず捕まえられるよう、診断 handler を導入。
|
package/package.json
CHANGED
package/src/daemon/daemon.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
buildFinalStagePrompt,
|
|
39
39
|
buildPreamble,
|
|
40
40
|
parseHaikuResponse,
|
|
41
|
+
filterCatalogMisses,
|
|
41
42
|
createHaikuCaller,
|
|
42
43
|
HaikuError,
|
|
43
44
|
} from './haiku-caller.mjs';
|
|
@@ -103,6 +104,11 @@ export async function startDaemon({
|
|
|
103
104
|
}
|
|
104
105
|
logFn(`tool-db loaded: ${toolList.length} tools` + (projectRoot ? ` (project=${projectRoot})` : ''));
|
|
105
106
|
|
|
107
|
+
// v0.13.3: Haiku occasionally hallucinates tool names outside the catalog (training-memory
|
|
108
|
+
// leakage / few-shot cargo-cult). We filter these post-parse; entries not in this set are
|
|
109
|
+
// dropped. See filterCatalogMisses for the pass-flip semantics.
|
|
110
|
+
const catalogNames = new Set(toolList.map((t) => t.name));
|
|
111
|
+
|
|
106
112
|
// v0.6.0: preamble (role + schema + catalog) is built once and threaded into the Haiku
|
|
107
113
|
// caller. The caller prepends it on the first call only; --resume keeps it in session
|
|
108
114
|
// history for all subsequent calls.
|
|
@@ -133,8 +139,9 @@ export async function startDaemon({
|
|
|
133
139
|
// Other Haiku errors (timeout, spawn failure) still propagate — §14 unexpected → throw.
|
|
134
140
|
const runHaikuJudgment = async (stage, prompt) => {
|
|
135
141
|
const { raw, meta } = await callHaikuTracked(prompt);
|
|
142
|
+
let parsed;
|
|
136
143
|
try {
|
|
137
|
-
|
|
144
|
+
parsed = parseHaikuResponse(raw);
|
|
138
145
|
} catch (err) {
|
|
139
146
|
if (err instanceof HaikuError && err.code === 'E_HAIKU_SCHEMA') {
|
|
140
147
|
logFn(`${stage}: role collapse detected, session reset: ${err.message}`);
|
|
@@ -145,6 +152,11 @@ export async function startDaemon({
|
|
|
145
152
|
}
|
|
146
153
|
throw err;
|
|
147
154
|
}
|
|
155
|
+
const { parsed: filtered, dropped } = filterCatalogMisses(parsed, catalogNames);
|
|
156
|
+
if (dropped.length > 0) {
|
|
157
|
+
logFn(`${stage}: dropped catalog-external names: ${dropped.join(',')}`);
|
|
158
|
+
}
|
|
159
|
+
return { parsed: filtered, meta };
|
|
148
160
|
};
|
|
149
161
|
|
|
150
162
|
// v0.12.0: heartbeat. Reset on every envelope; if no event arrives within
|
|
@@ -51,6 +51,9 @@ const SHARED_HEADER = [
|
|
|
51
51
|
'{"pass": <true|false>, "missing_tools": [{"name": "<カタログ名>", "reason": "<一文の日本語>"}]}',
|
|
52
52
|
'- pass:true なら missing_tools は空、pass:false なら 1 件以上',
|
|
53
53
|
'- JSON のみ。前置き・コードフェンス禁止',
|
|
54
|
+
'- **name は後述「## カタログ」に列挙されたツール名そのまま**のみ許可。',
|
|
55
|
+
' カタログ外の名前 (Skill(xxx) / 任意のスラッシュコマンド / 記憶した既知ツール等) は禁止。',
|
|
56
|
+
' 該当するツールがカタログに見当たらなければ、無理に挙げず pass:true を返す。',
|
|
54
57
|
'',
|
|
55
58
|
'## 判定対象',
|
|
56
59
|
'各ターン、以下いずれかの stage で判定リクエストを受けます:',
|
|
@@ -179,6 +182,33 @@ export function parseHaikuResponse(raw) {
|
|
|
179
182
|
return parsed;
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
// v0.13.3: post-parse defence against catalog-external hallucinations. Haiku occasionally
|
|
186
|
+
// proposes tool names that are not in the catalog — training-memory leakage or few-shot
|
|
187
|
+
// cargo-culting. The SHARED_HEADER now forbids this explicitly, but we also filter
|
|
188
|
+
// defensively: entries whose name is not in `catalogNames` are dropped. If all entries are
|
|
189
|
+
// dropped, pass is flipped to true with reason='hallucination_filtered'. Mixed cases keep
|
|
190
|
+
// the valid entries and stay pass=false.
|
|
191
|
+
//
|
|
192
|
+
// Returns { parsed, dropped } where `dropped` is the list of filtered-out names (for
|
|
193
|
+
// observability / logging).
|
|
194
|
+
export function filterCatalogMisses(parsed, catalogNames) {
|
|
195
|
+
const names = catalogNames instanceof Set ? catalogNames : new Set(catalogNames);
|
|
196
|
+
const kept = [];
|
|
197
|
+
const dropped = [];
|
|
198
|
+
for (const m of parsed.missing_tools) {
|
|
199
|
+
if (names.has(m.name)) kept.push(m);
|
|
200
|
+
else dropped.push(m.name);
|
|
201
|
+
}
|
|
202
|
+
if (dropped.length === 0) return { parsed, dropped };
|
|
203
|
+
if (kept.length === 0) {
|
|
204
|
+
return {
|
|
205
|
+
parsed: { pass: true, missing_tools: [], reason: 'hallucination_filtered' },
|
|
206
|
+
dropped,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return { parsed: { ...parsed, missing_tools: kept }, dropped };
|
|
210
|
+
}
|
|
211
|
+
|
|
182
212
|
function stripFence(text) {
|
|
183
213
|
const fenceMatch = text.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/);
|
|
184
214
|
return fenceMatch ? fenceMatch[1] : text;
|