claude-spotter 0.5.0 → 0.5.2

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,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2
4
+
5
+ **Haiku 呼び出しのレイテンシ可視化 (観測性の改善のみ、機能変更なし)**。
6
+
7
+ ### 事の発端
8
+
9
+ v0.5.1 の hot-fix で session-scoped Haiku がようやく生きた状態で動き始めた。実セッション観測で `--resume` 経路の Haiku 判定はエラーなく走っているものの、**そこに「cold-start が実際に消えたか」を読み取れる情報が daemon ログに出ていない**。プラン §5.5 が前提としていた「`--resume` で毎ターン 20–50 秒の claude -p spawn コストを消せる」が実効的に効いているかは、呼び出し時間が見えないと判断できない。
10
+
11
+ ### 変更点
12
+
13
+ - **[src/daemon/haiku-caller.mjs](src/daemon/haiku-caller.mjs)**: `createHaikuCaller` の返り値に `isFirstCall` getter を追加。daemon 側が「次の呼び出しが初回かどうか」をスナップショットできる。
14
+ - **[src/daemon/daemon.mjs](src/daemon/daemon.mjs)**: `callHaikuTracked` が raw 文字列に加えて `{ durationMs, mode }` を返す構造に変更。`runHaikuJudgment` / `handleUserInput` / `handleTurnEnd` の戻り値構造も合わせて `{ parsed, meta }` に統一。ログ出力に `mode=first|resumed, duration_ms=<N>` を追加。role collapse 回復パスも meta を引き継ぐため silent-pass ログにも duration が残る。
15
+ - **[test/haiku-caller.test.mjs](test/haiku-caller.test.mjs)**: `isFirstCall` の初期値と `reset()` 後の挙動を検証するテストを追加。
16
+ - **[test/daemon.test.mjs](test/daemon.test.mjs)**: `user_input` / `turn_end` のログ行に `mode=first` / `mode=resumed` と `duration_ms=<N>` が含まれることを検証するテストを 2 件追加。
17
+
18
+ ### 観測できるようになったこと
19
+
20
+ これまでは `user_input: pass=true, missing=` だけだったログ行が `user_input: pass=true, missing=, mode=first, duration_ms=8432` のように出る。次のセッションから:
21
+
22
+ - **`mode=resumed` の duration_ms が mode=first より有意に短い**なら `--resume` の効果が実測で確認できる
23
+ - **role_collapse_reset が発生しても duration_ms が見える**ので、回復が高速なのか cold-start 待ちなのかが区別できる
24
+ - **timeout 30s に対する余裕**が数値で見える (ギリギリなら延長、余裕なら短縮の判断材料)
25
+
26
+ これらは v0.5.0 / v0.5.1 の「resume の実効 spawn 削減量未検証」「role collapse 実発生頻度の観測」という既知課題を **観測可能な状態に引き上げる**ための最小変更。判断材料が貯まるまで追加の構造変更は凍結。
27
+
28
+ ### 機能的には非変更
29
+
30
+ ログフォーマット以外の挙動は一切変わらない。envelope 契約・hook の終了コード・Haiku プロンプト・catalog 形式・回復ロジックのいずれも手付かず。
31
+
32
+ ## 0.5.1
33
+
34
+ **v0.5.0 の Haiku spawn が初呼び出し時点で落ちていたバグの hot-fix**。
35
+
36
+ ### 事の発端
37
+
38
+ v0.5.0 リリース直後の実セッションで、Stop hook の Haiku 判定が毎回失敗して daemon ログに `--session-id cannot be used with --resume unless --fork-session is also passed` が出ていた。claude CLI は `--session-id` と `--resume` の併用を `--fork-session` なしでは拒否する仕様で、v0.5.0 の `buildSpawnArgs` が resume 時に両方渡していたのが原因。v0.5.0 は起動はするが監査は 1 度も成立していなかった (role collapse 回復も永遠に triggering しない状態)。
39
+
40
+ ### 変更点
41
+
42
+ - **[src/daemon/haiku-caller.mjs](src/daemon/haiku-caller.mjs)**: resume 時は `--session-id` を外して `--resume <uuid>` のみを渡す形に修正。初回は従来どおり `--session-id <uuid>` で新規セッション確立。
43
+ - **[test/haiku-caller.test.mjs](test/haiku-caller.test.mjs)**: `buildSpawnArgs: subsequent call` のアサーションを「`--session-id` が含まれない / `--resume <uuid>` が正しく渡る」に書き換え。
44
+
45
+ ### 残る既知課題
46
+
47
+ v0.5.0 の既知課題 (resume の spawn 削減量未検証、カタログ毎ターン再送コスト、role collapse 実発生頻度の観測) は引き続き持ち越し。v0.5.1 でようやく session-scoped の効果測定が可能になる。
48
+
3
49
  ## 0.5.0
4
50
 
5
51
  **Session-scoped Haiku 復活 + role-collapse 回復機構 (UX 改善)**。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -88,23 +88,26 @@ export async function startDaemon({
88
88
 
89
89
  const callHaikuTracked = async (prompt) => {
90
90
  lastHaikuCallAt = Date.now();
91
- return callHaiku(prompt);
91
+ const mode = callHaiku.isFirstCall === false ? 'resumed' : 'first';
92
+ const start = Date.now();
93
+ const raw = await callHaiku(prompt);
94
+ return { raw, meta: { durationMs: Date.now() - start, mode } };
92
95
  };
93
96
 
94
97
  // v0.5.0: shared Haiku-invocation + parse helper. On E_HAIKU_SCHEMA (role collapse),
95
98
  // rotates the Haiku session-id and silent-passes the turn (reason: role_collapse_reset).
96
99
  // Other Haiku errors (timeout, spawn failure) still propagate — §14 unexpected → throw.
97
100
  const runHaikuJudgment = async (stage, prompt) => {
98
- const raw = await callHaikuTracked(prompt);
101
+ const { raw, meta } = await callHaikuTracked(prompt);
99
102
  try {
100
- return parseHaikuResponse(raw);
103
+ return { parsed: parseHaikuResponse(raw), meta };
101
104
  } catch (err) {
102
105
  if (err instanceof HaikuError && err.code === 'E_HAIKU_SCHEMA') {
103
106
  logFn(`${stage}: role collapse detected, session reset: ${err.message}`);
104
107
  if (typeof callHaiku.reset === 'function') {
105
108
  callHaiku.reset();
106
109
  }
107
- return { pass: true, missing_tools: [], reason: 'role_collapse_reset' };
110
+ return { parsed: { pass: true, missing_tools: [], reason: 'role_collapse_reset' }, meta };
108
111
  }
109
112
  throw err;
110
113
  }
@@ -166,9 +169,9 @@ export async function startDaemon({
166
169
  state.lastUserInput = userInput;
167
170
  state.usedTools = []; // reset tools for this turn
168
171
 
169
- const parsed = await runHaikuJudgment('user_input', buildFirstStagePrompt({ catalog, userInput }));
172
+ const { parsed, meta } = await runHaikuJudgment('user_input', buildFirstStagePrompt({ catalog, userInput }));
170
173
  logFn(
171
- `user_input: pass=${parsed.pass}, missing=${parsed.missing_tools.map((m) => m.name).join(',')}${
174
+ `user_input: pass=${parsed.pass}, missing=${parsed.missing_tools.map((m) => m.name).join(',')}, mode=${meta.mode}, duration_ms=${meta.durationMs}${
172
175
  parsed.reason ? `, reason=${parsed.reason}` : ''
173
176
  }`
174
177
  );
@@ -207,7 +210,7 @@ export async function startDaemon({
207
210
 
208
211
  const savedUserInput = state.lastUserInput;
209
212
  const savedUsedTools = state.usedTools.slice();
210
- const parsed = await runHaikuJudgment(
213
+ const { parsed, meta } = await runHaikuJudgment(
211
214
  'turn_end',
212
215
  buildFinalStagePrompt({
213
216
  catalog,
@@ -217,7 +220,7 @@ export async function startDaemon({
217
220
  })
218
221
  );
219
222
  logFn(
220
- `turn_end: pass=${parsed.pass}, missing=${parsed.missing_tools.map((m) => m.name).join(',')}${
223
+ `turn_end: pass=${parsed.pass}, missing=${parsed.missing_tools.map((m) => m.name).join(',')}, mode=${meta.mode}, duration_ms=${meta.durationMs}${
221
224
  parsed.reason ? `, reason=${parsed.reason}` : ''
222
225
  }`
223
226
  );
@@ -168,12 +168,14 @@ function truncate(s, n = 300) {
168
168
  // without going through the shell. We use cmd.exe /c explicitly rather than spawn({ shell:
169
169
  // true }) because the latter triggers DEP0190 on Node 24+.
170
170
  //
171
- // v0.5.0: session-scoped. First call of a given session id uses --session-id only; every
172
- // subsequent call adds --resume so claude -p re-attaches to that session, skipping cold
173
- // start. buildSpawnArgs is exported so tests can assert the flag wiring without spawning.
171
+ // v0.5.1: claude CLI rejects `--session-id` together with `--resume` unless `--fork-session`
172
+ // is present (fork would create a new id, defeating the point). So first call uses
173
+ // `--session-id <uuid>` to pin the id; every subsequent call uses `--resume <uuid>` alone to
174
+ // re-attach. buildSpawnArgs is exported so tests can assert flag wiring without spawning.
174
175
  export function buildSpawnArgs({ claudeBin, model, sessionId, resume }) {
175
- const args = ['-p', '--session-id', sessionId, '--model', model];
176
- if (resume) args.push('--resume', sessionId);
176
+ const args = resume
177
+ ? ['-p', '--resume', sessionId, '--model', model]
178
+ : ['-p', '--session-id', sessionId, '--model', model];
177
179
  if (process.platform === 'win32') {
178
180
  return { cmd: 'cmd.exe', cmdArgs: ['/c', claudeBin, ...args] };
179
181
  }
@@ -261,5 +263,10 @@ export function createHaikuCaller({ timeoutMs, claudeBin = 'claude', model = HAI
261
263
  enumerable: true,
262
264
  });
263
265
 
266
+ Object.defineProperty(callHaiku, 'isFirstCall', {
267
+ get: () => isFirstCall,
268
+ enumerable: true,
269
+ });
270
+
264
271
  return callHaiku;
265
272
  }
package/src/version.mjs CHANGED
@@ -1 +1 @@
1
- export const version = '0.5.0';
1
+ export const version = '0.5.2';