claude-spotter 1.9.3 → 1.9.5

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
@@ -3,6 +3,24 @@
3
3
  各節はそのversion公開時点の変更記録であり、後続versionにより置換された仕様を含む。
4
4
  現行runtime契約は[`docs/00_overview.md`](https://github.com/kitepon/Spotter/blob/main/docs/00_overview.md)から辿る。
5
5
 
6
+ ## 1.9.5 — 2026-10-10
7
+
8
+ - Claude / CodexのStop監査がfindingを出した時の固定`systemMessage`(「Spotter は直前の応答について利用可能ツールの
9
+ 確認候補を記録しました。」)を出さない。どのツールの話かを含まず、読んでも対応できない知らせだった。
10
+ findingは従来どおり構造hook eventへ記録する。hookはfindingについてstdout・stderrへ何も出さない。
11
+ 監査が失敗した時の固定の知らせと、UserPromptSubmitの提案は変えていない。
12
+
13
+ ## 1.9.4 — 2026-10-09
14
+
15
+ - Windowsで、監査の制限時間と同時にCodexが自分で終わった時、`taskkill`がrootを見つけられず(exit 128)、
16
+ Spotterが「process treeの終了を確認できない」(`E_CODEX_CLI_TERMINATION`)として扱っていた。この失敗は
17
+ 汎用の失敗として`SPOTTER.AUDITOR.UNAVAILABLE`へ即時登録される。child自身のclose(終了と全stdio pipeのEOF)を
18
+ 確認できた時だけ終了済みとし、監査はtimeoutのまま扱う。closeを確認できない時と、128以外の`taskkill`失敗は
19
+ 従来どおり`E_CODEX_CLI_TERMINATION`にする。
20
+ - CodexとGrokの失敗hook eventへ、失敗した段の内部code(`internalCode`)と、あればその原因のcode・終了status
21
+ (`causeCode`、`causeExitCode`)を残す。これまでは表示用にまとめた`code`だけが残り、1回の失敗の種類を
22
+ logから読み戻せなかった。記録するのは固定の識別子と終了statusだけで、hookの出力とstderrは変えていない。
23
+
6
24
  ## 1.9.3 — 2026-10-07
7
25
 
8
26
  - 監査backendへ届かない・応じてもらえない失敗(network、timeout、認証、利用上限、5xx)を、1回ごとに
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.9.3",
3
+ "version": "1.9.5",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,13 +21,13 @@ import {
21
21
  } from '../hooks/lib.mjs';
22
22
  import { discardLegacyPending } from '../hooks/pending-context.mjs';
23
23
  import {
24
- STOP_FINDING_SYSTEM_MESSAGE,
25
24
  projectBackendFailure,
26
25
  projectParentAdvice,
27
26
  projectToolIds,
28
27
  } from '../hooks/parent-output-projector.mjs';
29
28
  import {
30
29
  appendHookEvent,
30
+ failureEventFields,
31
31
  hookEventsPath,
32
32
  summarizeHookEvents,
33
33
  } from '../core/hook-event-log.mjs';
@@ -228,6 +228,7 @@ export async function runCodexUserPromptSubmitHook({
228
228
  status: 'error',
229
229
  backend: err?.backend ?? null,
230
230
  code: failure.code,
231
+ ...failureEventFields(err),
231
232
  ...compactCodexModelSelectionForEvent(err?.diagnostics?.modelSelection),
232
233
  legacyPendingDiagnostic: legacyPending.diagnostic,
233
234
  durationMs: Date.now() - startedAt,
@@ -339,6 +340,7 @@ export async function runCodexStopHook({
339
340
  hook: 'Stop',
340
341
  status: 'error',
341
342
  code: failure.code,
343
+ ...failureEventFields(err),
342
344
  reason: 'tool_usage_observation',
343
345
  usedToolCount: 0,
344
346
  durationMs: Date.now() - startedAt,
@@ -395,6 +397,7 @@ export async function runCodexStopHook({
395
397
  status: 'error',
396
398
  backend: err?.backend ?? null,
397
399
  code: failure.code,
400
+ ...failureEventFields(err),
398
401
  ...compactCodexModelSelectionForEvent(err?.diagnostics?.modelSelection),
399
402
  usedToolCount: usedTools.length,
400
403
  ...toolUsageEvent,
@@ -423,7 +426,7 @@ export async function runCodexStopHook({
423
426
  return;
424
427
  }
425
428
 
426
- writeCodexSystemMessage({ systemMessage: STOP_FINDING_SYSTEM_MESSAGE, writeOutput });
429
+ // A finding is recorded only. The hook prints nothing for it.
427
430
  await recordCodexHookEventSafe(recordHookEventFn, {
428
431
  projectRoot,
429
432
  event: {
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { createAuditorBackend } from '../core/auditor-backend.mjs';
8
8
  import { createEvaluationStore } from '../core/evaluation-store.mjs';
9
- import { appendHookEventSafe } from '../core/hook-event-log.mjs';
9
+ import { appendHookEventSafe, failureEventFields } from '../core/hook-event-log.mjs';
10
10
  import { projectBackendFailure, projectToolIds } from '../hooks/parent-output-projector.mjs';
11
11
  import { findSpotterMarker, isChildCall, readStdinJson, requireString } from '../hooks/lib.mjs';
12
12
  import { readLocal, refresh } from '../tool-db/refresh.mjs';
@@ -154,7 +154,7 @@ export async function runGrokHook({
154
154
  const failure = projectBackendFailure(error?.code);
155
155
  writeError(failure.stderr);
156
156
  await record(projectRoot, expected, {
157
- status: 'degraded', code: failure.code, durationMs: Date.now() - startedAt,
157
+ status: 'degraded', code: failure.code, ...failureEventFields(error), durationMs: Date.now() - startedAt,
158
158
  }, recordFn);
159
159
  if (prompt !== null) await recordGrokEvaluation({ createEvaluationStoreFn, projectRoot, input, prompt, status: 'error', writeError });
160
160
  return;
@@ -24,7 +24,10 @@
24
24
  // backendDurationMs?: number | null,
25
25
  // usedToolCount?: number,
26
26
  // legacyPendingDiagnostic?: string | null,
27
- // toolName?: string | null
27
+ // toolName?: string | null,
28
+ // internalCode?: string | null, // failing step's own code; `code` is the projected one
29
+ // causeCode?: string,
30
+ // causeExitCode?: number
28
31
  // }
29
32
 
30
33
  import { appendFile, mkdir, readFile } from 'node:fs/promises';
@@ -34,6 +37,23 @@ const HOOK_EVENTS_FILE = 'hook-events.jsonl';
34
37
  export const HOOK_EVENT_SCHEMA = 'spotter.hook_event.v1';
35
38
  export const HOOK_EVENTS_SUMMARY_SCHEMA = 'spotter.hook_events_summary.v1';
36
39
 
40
+ const EVENT_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
41
+
42
+ function eventCode(value) {
43
+ return typeof value === 'string' && EVENT_CODE_PATTERN.test(value) ? value : null;
44
+ }
45
+
46
+ // `code` holds the projected, user-facing code, which folds many failures into one generic value.
47
+ // These fields keep the failing step's own code so a single failure can be read back from the log.
48
+ // Only fixed identifiers and an exit status are taken; messages and provider output never are.
49
+ export function failureEventFields(error) {
50
+ const fields = { internalCode: eventCode(error?.code) };
51
+ const causeCode = eventCode(error?.cause?.code);
52
+ if (causeCode !== null) fields.causeCode = causeCode;
53
+ if (Number.isInteger(error?.cause?.exitCode)) fields.causeExitCode = error.cause.exitCode;
54
+ return fields;
55
+ }
56
+
37
57
  export function hookEventsPath(projectRoot) {
38
58
  if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
39
59
  throw new TypeError('hookEventsPath: projectRoot must be a non-empty string');
@@ -38,8 +38,6 @@ const FAILURE_OUTPUTS = Object.freeze({
38
38
  }),
39
39
  });
40
40
 
41
- export const STOP_FINDING_SYSTEM_MESSAGE = 'Spotter は直前の応答について利用可能ツールの確認候補を記録しました。';
42
-
43
41
  export function projectParentAdvice(toolIds) {
44
42
  const accepted = projectToolIds(toolIds);
45
43
  if (accepted.length === 0) return '';
@@ -13,7 +13,7 @@ import {
13
13
  } from './lib.mjs';
14
14
  import { getLastAssistantText } from './transcript-reader.mjs';
15
15
  import { sendRequest } from '../daemon/transport.mjs';
16
- import { STOP_FINDING_SYSTEM_MESSAGE, projectBackendFailure, projectToolIds } from './parent-output-projector.mjs';
16
+ import { projectBackendFailure, projectToolIds } from './parent-output-projector.mjs';
17
17
 
18
18
  const TIMEOUT_MS = 50_000;
19
19
 
@@ -114,7 +114,7 @@ export async function runStop({
114
114
  return; // nothing to defer
115
115
  }
116
116
 
117
- writeOutput(JSON.stringify({ systemMessage: STOP_FINDING_SYSTEM_MESSAGE }));
117
+ // A finding is recorded only. The hook prints nothing for it.
118
118
  await recordHookEventFn({
119
119
  projectRoot,
120
120
  writeError: reportError,
@@ -134,12 +134,41 @@ export function npmShimEntryPath(source, shimDir) {
134
134
  return win32.join(shimDir, ...segments);
135
135
  }
136
136
 
137
+ // taskkill は root の pid が存在しない時に 128 で終わる。この時 taskkill は何も終了していない。
138
+ const TASKKILL_PROCESS_NOT_FOUND = 128;
139
+
140
+ // 'close' は process の終了と全 stdio pipe の EOF を意味する。root より長生きした子孫が
141
+ // pipe を持っている間は stream が閉じないので、ここは false になる。
142
+ function childHasClosed(child) {
143
+ if (!Number.isInteger(child.exitCode) && typeof child.signalCode !== 'string') return false;
144
+ return [child.stdin, child.stdout, child.stderr].every((stream) => !stream || stream.destroyed === true);
145
+ }
146
+
147
+ function waitForChildClose(child, graceMs) {
148
+ if (childHasClosed(child)) return Promise.resolve(true);
149
+ if (typeof child.once !== 'function' || typeof child.off !== 'function') return Promise.resolve(false);
150
+ return new Promise((resolve) => {
151
+ const onClose = () => {
152
+ clearTimeout(timer);
153
+ resolve(true);
154
+ };
155
+ const timer = setTimeout(() => {
156
+ child.off('close', onClose);
157
+ resolve(childHasClosed(child));
158
+ }, graceMs);
159
+ child.once('close', onClose);
160
+ });
161
+ }
162
+
137
163
  // cmd.exe shim のみ kill すると孫の CLI が残り得るため、Windows は process tree を
138
164
  // taskkill で終了する。taskkill 自体の起動・終了に失敗した時だけ direct kill へ戻す。
165
+ // taskkill が root を見つけられなかった時は、child 自身の close を確認できた場合だけ終了済みとする。
166
+ // timeout と同時に自分で終わった process を「終了未確認」にしないためで、close が無ければ従来どおり失敗する。
139
167
  export function terminateProcessTree(child, {
140
168
  platform = process.platform,
141
169
  spawnFn = spawn,
142
170
  timeoutMs = 5_000,
171
+ closeGraceMs = 500,
143
172
  } = {}) {
144
173
  if (!child || typeof child.kill !== 'function') return Promise.resolve();
145
174
  if (platform !== 'win32' || !Number.isSafeInteger(child.pid) || child.pid <= 0) {
@@ -184,9 +213,20 @@ export function terminateProcessTree(child, {
184
213
  finish();
185
214
  return;
186
215
  }
187
- const error = new Error(`taskkill exited with code ${code}`);
188
- error.code = 'E_PROCESS_TREE_TERMINATION';
189
- finish(error);
216
+ const fail = () => {
217
+ const error = new Error(`taskkill exited with code ${code}`);
218
+ error.code = 'E_PROCESS_TREE_TERMINATION';
219
+ error.exitCode = code;
220
+ finish(error);
221
+ };
222
+ if (code !== TASKKILL_PROCESS_NOT_FOUND) {
223
+ fail();
224
+ return;
225
+ }
226
+ waitForChildClose(child, closeGraceMs).then((closed) => {
227
+ if (closed) finish();
228
+ else fail();
229
+ });
190
230
  });
191
231
  } catch (cause) {
192
232
  const error = new Error(`taskkill spawn failed: ${cause.message}`);