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 +18 -0
- package/package.json +1 -1
- package/src/cli/codex-hook-cmd.mjs +5 -2
- package/src/cli/grok-hook-cmd.mjs +2 -2
- package/src/core/hook-event-log.mjs +21 -1
- package/src/hooks/parent-output-projector.mjs +0 -2
- package/src/hooks/stop.mjs +2 -2
- package/src/platform/spawn.mjs +43 -3
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
|
@@ -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
|
-
|
|
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 '';
|
package/src/hooks/stop.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
117
|
+
// A finding is recorded only. The hook prints nothing for it.
|
|
118
118
|
await recordHookEventFn({
|
|
119
119
|
projectRoot,
|
|
120
120
|
writeError: reportError,
|
package/src/platform/spawn.mjs
CHANGED
|
@@ -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
|
|
188
|
-
|
|
189
|
-
|
|
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}`);
|