claude-spotter 1.5.4 → 1.5.6
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/README.ja.md +12 -7
- package/README.md +8 -6
- package/docs/11_dashboard-operations.md +3 -0
- package/package.json +1 -1
- package/src/cli/codex-hook-cmd.mjs +7 -6
- package/src/core/evaluation-context.mjs +28 -105
- package/src/core/evaluation-tool-id.mjs +20 -2
- package/src/hooks/user-prompt.mjs +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.6 — 2026-08-05
|
|
4
|
+
|
|
5
|
+
- **Codexの動的nested MCP利用を採用として記録する。** `functions.exec`内で
|
|
6
|
+
`ALL_TOOLS.find(...name === "mcp__...")`から取得したtoolを`tools[tool.name](...)`で実行する
|
|
7
|
+
現行Codexの呼び方を認識する。単なるlookupや文字列・コメント内の記述は利用として数えない。
|
|
8
|
+
- **実際の誤判定を回帰化。** `mcp__aiterm__pty_open`を実行済みなのに`usedToolIds=[]`となった形を
|
|
9
|
+
focused testとStop hookの評価DB統合テストへ固定し、`adopted`になることを確認する。
|
|
10
|
+
|
|
11
|
+
## 1.5.5 — 2026-08-05
|
|
12
|
+
|
|
13
|
+
- **提案時文脈をexact sessionへ戻した。** 評価記録がproject全体の最新threadを読む
|
|
14
|
+
`observer-read`を誤って使っていた経路を撤去し、既存のThroughline `auditor-context`へ一本化した。
|
|
15
|
+
Claude / CodexのUserPromptSubmit hookから提案元の`session_id`、`host`、`transcript_path`を渡し、
|
|
16
|
+
提案直前の完了turnを非採用caseへ保存できる。
|
|
17
|
+
- **重複adapterを廃止。** 評価専用readerは、すでにfreshness・本文上限・exact-session照合を持つ
|
|
18
|
+
`loadAuditorContext`を再利用する。Throughline本文をauditor入力へ戻さず、監査と評価証拠の分離は維持する。
|
|
19
|
+
- **検証。** 既存adapterへの引数接続とClaude / Codex hookのfocused testを通過した。
|
|
20
|
+
|
|
3
21
|
## 1.5.4 — 2026-08-05
|
|
4
22
|
|
|
5
23
|
- **Throughlineを提案監査の実行条件から撤去。** Claude / CodexのUserPromptSubmitは、
|
package/README.ja.md
CHANGED
|
@@ -159,9 +159,9 @@ flowchart LR
|
|
|
159
159
|
### Throughlineの提案時評価文脈(任意)
|
|
160
160
|
|
|
161
161
|
Spotterの提案AIはThroughlineを使わず、Throughlineの導入・設定・freshness・取得成否に関係なく
|
|
162
|
-
UserPromptSubmit
|
|
163
|
-
`
|
|
164
|
-
|
|
162
|
+
UserPromptSubmitごとに監査します。提案が出た時だけ、改善分析用の別文脈としてThroughline
|
|
163
|
+
`auditor-context`を提案元のexact sessionとtranscriptで一度だけ呼びます。返されたfreshな直前完了turnは
|
|
164
|
+
監査入力と混ぜずに保存します。取得できなくても`context_unavailable`として記録するだけで、監査や親への助言には影響しません。
|
|
165
165
|
|
|
166
166
|
`spotter install`がPATH上のThroughlineを絶対パスへ解決できる場合、この評価証拠の取得経路を既定で設定します。
|
|
167
167
|
既存互換の`--auditor-context`名はmarker設定に残っていますが、監査のON/OFFは制御しません。
|
|
@@ -189,7 +189,7 @@ spotter install -y --auditor-context throughline `
|
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
`spotter doctor`はこの経路を`evaluation context`として表示し、command / argsや会話本文は表示しません。
|
|
192
|
-
|
|
192
|
+
評価文脈は端末内の評価SQLiteにだけ保存され、network送信、retry、background回収は行いません。
|
|
193
193
|
|
|
194
194
|
## よく使うコマンド
|
|
195
195
|
|
|
@@ -206,12 +206,16 @@ spotter db rebuild # Claude local + Claude global DB を両方消してか
|
|
|
206
206
|
spotter status # 稼働中の daemon 一覧
|
|
207
207
|
spotter doctor # 環境診断 (Node / claude CLI / Codex readiness / tool-db 整合性)
|
|
208
208
|
spotter diagnostics logs # daemon log から pass=false / backend latency / anomaly signal を集計
|
|
209
|
+
spotter diagnostics factory
|
|
210
|
+
# factory向けread-only診断snapshotをJSONで出力
|
|
211
|
+
spotter diagnostics runtime-errors
|
|
212
|
+
# opt-in端末内runtime error集計をread-only表示(network送信なし)
|
|
209
213
|
spotter evaluation report
|
|
210
214
|
# 端末内DBからproject横断の提案率・tool採用率を集計
|
|
211
215
|
spotter evaluation cases --outcome not-adopted
|
|
212
216
|
# 提案されたが同じturnで使われなかったtool itemを一覧
|
|
213
217
|
spotter evaluation case <observation-id>
|
|
214
|
-
# request
|
|
218
|
+
# request、任意のThroughline snapshot、提案、利用、outcomeを確認
|
|
215
219
|
spotter dashboard device --id mac --name Mac
|
|
216
220
|
# この端末の評価DBを127.0.0.1:53940で配信
|
|
217
221
|
spotter dashboard hub --config dashboard-hub.json --host 172.18.0.1
|
|
@@ -236,8 +240,9 @@ spotter uninstall # hook 登録を解除 (~/.spotter は残す)
|
|
|
236
240
|
|
|
237
241
|
dashboardはlocal-firstで動く。各端末が自身の`~/.spotter/evaluation.db`を読み、hubは固定の
|
|
238
242
|
端末・upstream対応だけを持つ。評価データをcloud DBへ複製しない。端末画面では
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
対象ターン、ツール提案あり、提案ツール数、利用判定済み、実際に使用、判定不能、提案率、採用率、
|
|
244
|
+
project/tool内訳、非採用case、監査対象request、任意の提案時Throughline証拠を確認できる。
|
|
245
|
+
health確認は端末一覧request時だけなので、端末がofflineでもbackground監視や
|
|
241
246
|
retry queueを作らず、その端末だけを切り離せる。
|
|
242
247
|
|
|
243
248
|
4端末のservice、reverse tunnel、Caddy/Cloudflare構成は
|
package/README.md
CHANGED
|
@@ -160,9 +160,11 @@ Both share the principle of **"don't rely on the primary agent to do it itself."
|
|
|
160
160
|
|
|
161
161
|
Spotter's proposal auditor does not use Throughline. Every UserPromptSubmit
|
|
162
162
|
audit runs independently of Throughline installation, configuration, freshness, or read
|
|
163
|
-
failures. When Spotter emits a proposal, the evaluation recorder
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
failures. When Spotter emits a proposal, the evaluation recorder calls Throughline
|
|
164
|
+
`auditor-context` once with the proposing host's exact session ID and transcript path.
|
|
165
|
+
The bounded, fresh completed turns are saved as separate improvement evidence; they are
|
|
166
|
+
never auditor input. A failed read is recorded as `context_unavailable` and never changes
|
|
167
|
+
auditing or parent advice.
|
|
166
168
|
|
|
167
169
|
When `spotter install` resolves Throughline on PATH to an absolute executable, it
|
|
168
170
|
configures this evaluation-evidence path by default. The legacy option name
|
|
@@ -218,7 +220,7 @@ spotter evaluation report
|
|
|
218
220
|
spotter evaluation cases --outcome not-adopted
|
|
219
221
|
# list proposed tools that were not used in the same turn
|
|
220
222
|
spotter evaluation case <observation-id>
|
|
221
|
-
# inspect request,
|
|
223
|
+
# inspect request, optional Throughline snapshot, proposal, usage, and outcome
|
|
222
224
|
spotter dashboard device --id mac --name Mac
|
|
223
225
|
# serve this terminal's local evaluation DB on 127.0.0.1:53940
|
|
224
226
|
spotter dashboard hub --config dashboard-hub.json --host 172.18.0.1
|
|
@@ -244,8 +246,8 @@ spotter uninstall # remove hooks from this project (leaves ~/.spotter int
|
|
|
244
246
|
The dashboard is local-first. Every terminal reads its own `~/.spotter/evaluation.db`; the hub
|
|
245
247
|
keeps only a static device-to-upstream map and does not copy evaluation data into a cloud database.
|
|
246
248
|
The device view shows Japanese labels for every evaluation metric, proposal and adoption rates
|
|
247
|
-
with their numerator and denominator, project/tool breakdowns, non-adopted cases,
|
|
248
|
-
|
|
249
|
+
with their numerator and denominator, project/tool breakdowns, non-adopted cases, the request
|
|
250
|
+
audited by Spotter, and optional proposal-time Throughline evidence. The hub checks health only
|
|
249
251
|
when the device list is requested, so an offline terminal is isolated without a background monitor
|
|
250
252
|
or retry queue.
|
|
251
253
|
|
package/package.json
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from '../core/hook-event-log.mjs';
|
|
33
33
|
import { observeRuntimeErrorIsolatedSafe } from '../core/runtime-error-store.mjs';
|
|
34
34
|
import { createEvaluationStore } from '../core/evaluation-store.mjs';
|
|
35
|
-
import {
|
|
35
|
+
import { loadEvaluationContext } from '../core/evaluation-context.mjs';
|
|
36
36
|
import {
|
|
37
37
|
canonicalizeCodexNestedMcpToolIds,
|
|
38
38
|
canonicalizeCodexSkillReadToolIds,
|
|
@@ -130,7 +130,7 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
130
130
|
writeError = (text) => process.stderr.write(text),
|
|
131
131
|
runtimeErrorObserver = async () => ({ collected: false, reason: 'observer_not_configured' }),
|
|
132
132
|
createEvaluationStoreFn = createEvaluationStore,
|
|
133
|
-
|
|
133
|
+
loadEvaluationContextFn = loadEvaluationContext,
|
|
134
134
|
randomUUIDFn = randomUUID,
|
|
135
135
|
now = () => Date.now(),
|
|
136
136
|
} = {}) {
|
|
@@ -150,15 +150,16 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
152
|
const proposals = canonicalizeProposedToolIds(proposedToolIds);
|
|
153
|
-
let
|
|
153
|
+
let evaluationContext = { status: 'not_requested', snapshot: null };
|
|
154
154
|
let proposedAtMs = recordedAtMs;
|
|
155
155
|
try {
|
|
156
156
|
if (auditStatus === 'success' && proposals.resolvedToolIds.length > 0) {
|
|
157
157
|
proposedAtMs = now();
|
|
158
|
-
|
|
158
|
+
evaluationContext = await loadEvaluationContextFn({
|
|
159
159
|
projectRoot,
|
|
160
160
|
host: 'codex',
|
|
161
161
|
sessionId,
|
|
162
|
+
transcriptPath: input.transcript_path,
|
|
162
163
|
recordedAtMs: proposedAtMs,
|
|
163
164
|
});
|
|
164
165
|
}
|
|
@@ -174,8 +175,8 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
174
175
|
auditStatus,
|
|
175
176
|
requestText: prompt,
|
|
176
177
|
auditorSeenContext: null,
|
|
177
|
-
observerContextStatus:
|
|
178
|
-
observerSnapshot:
|
|
178
|
+
observerContextStatus: evaluationContext.status,
|
|
179
|
+
observerSnapshot: evaluationContext.snapshot,
|
|
179
180
|
proposedToolIds: proposals.resolvedToolIds,
|
|
180
181
|
backend,
|
|
181
182
|
model,
|
|
@@ -1,45 +1,33 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
1
|
import { realpath } from 'node:fs/promises';
|
|
4
2
|
import { isAbsolute } from 'node:path';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
3
|
|
|
7
4
|
import {
|
|
8
5
|
DEFAULT_AUDITOR_CONTEXT_MAX_BUFFER,
|
|
9
6
|
DEFAULT_AUDITOR_CONTEXT_TIMEOUT_MS,
|
|
7
|
+
loadAuditorContext,
|
|
10
8
|
readProjectAuditorContextConfig,
|
|
11
9
|
} from './auditor-context.mjs';
|
|
12
10
|
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
export const EVALUATION_OBSERVER_CONTEXT_UNAVAILABLE = 'context_unavailable';
|
|
16
|
-
export const DEFAULT_EVALUATION_OBSERVER_LIMIT = 10;
|
|
17
|
-
|
|
18
|
-
const execFileAsync = promisify(execFile);
|
|
19
|
-
const UNAVAILABLE_STATUSES = new Set([
|
|
20
|
-
'projection_pending',
|
|
21
|
-
'ambiguous_parent',
|
|
22
|
-
'resync_required',
|
|
23
|
-
'error',
|
|
24
|
-
]);
|
|
11
|
+
export const EVALUATION_CONTEXT_AVAILABLE = 'context_available';
|
|
12
|
+
export const EVALUATION_CONTEXT_UNAVAILABLE = 'context_unavailable';
|
|
25
13
|
|
|
26
14
|
/**
|
|
27
|
-
* Reads
|
|
28
|
-
*
|
|
29
|
-
* This is deliberately separate from auditor-context.mjs: the returned snapshot
|
|
30
|
-
* is evaluation evidence and must never become auditor input.
|
|
15
|
+
* Reads the exact-session Throughline context used only as proposal-time
|
|
16
|
+
* evaluation evidence. It must never become auditor input.
|
|
31
17
|
*/
|
|
32
|
-
export async function
|
|
18
|
+
export async function loadEvaluationContext({
|
|
33
19
|
projectRoot,
|
|
34
20
|
host,
|
|
35
21
|
sessionId,
|
|
22
|
+
transcriptPath,
|
|
36
23
|
config,
|
|
37
24
|
recordedAtMs = Date.now(),
|
|
38
25
|
timeoutMs = DEFAULT_AUDITOR_CONTEXT_TIMEOUT_MS,
|
|
39
26
|
maxBuffer = DEFAULT_AUDITOR_CONTEXT_MAX_BUFFER,
|
|
40
|
-
execFileFn
|
|
27
|
+
execFileFn,
|
|
41
28
|
realpathFn = realpath,
|
|
42
29
|
readConfigFn = readProjectAuditorContextConfig,
|
|
30
|
+
loadAuditorContextFn = loadAuditorContext,
|
|
43
31
|
} = {}) {
|
|
44
32
|
if (!Number.isSafeInteger(recordedAtMs) || recordedAtMs < 0) {
|
|
45
33
|
throw new TypeError('recordedAtMs must be a non-negative safe integer');
|
|
@@ -56,6 +44,9 @@ export async function loadEvaluationObserverContext({
|
|
|
56
44
|
if (host === 'codex' && sessionId === 'codex:') {
|
|
57
45
|
throw new TypeError('sessionId must identify a thread');
|
|
58
46
|
}
|
|
47
|
+
if (typeof transcriptPath !== 'string' || transcriptPath.length === 0 || !isAbsoluteProjectPath(transcriptPath)) {
|
|
48
|
+
throw new TypeError('transcriptPath must be an absolute path');
|
|
49
|
+
}
|
|
59
50
|
|
|
60
51
|
let canonicalProjectRoot;
|
|
61
52
|
try {
|
|
@@ -75,110 +66,42 @@ export async function loadEvaluationObserverContext({
|
|
|
75
66
|
if (effectiveConfig?.mode !== 'throughline') {
|
|
76
67
|
return unavailableResult(recordedAtMs, 'provider_disabled');
|
|
77
68
|
}
|
|
78
|
-
if (!isDirectCommandConfig(effectiveConfig)) {
|
|
79
|
-
return unavailableResult(recordedAtMs, 'config_unavailable');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let stdout;
|
|
83
|
-
try {
|
|
84
|
-
({ stdout } = await execFileFn(effectiveConfig.command, [
|
|
85
|
-
...effectiveConfig.args,
|
|
86
|
-
'observer-read',
|
|
87
|
-
'--project', canonicalProjectRoot,
|
|
88
|
-
'--limit', String(DEFAULT_EVALUATION_OBSERVER_LIMIT),
|
|
89
|
-
'--json',
|
|
90
|
-
], {
|
|
91
|
-
encoding: 'utf8',
|
|
92
|
-
timeout: timeoutMs,
|
|
93
|
-
maxBuffer,
|
|
94
|
-
windowsHide: true,
|
|
95
|
-
shell: false,
|
|
96
|
-
}));
|
|
97
|
-
} catch {
|
|
98
|
-
return unavailableResult(recordedAtMs, 'observer_read_failed');
|
|
99
|
-
}
|
|
100
69
|
|
|
101
70
|
let snapshot;
|
|
102
71
|
try {
|
|
103
|
-
snapshot =
|
|
72
|
+
snapshot = await loadAuditorContextFn({
|
|
73
|
+
config: effectiveConfig,
|
|
74
|
+
host,
|
|
75
|
+
sessionId,
|
|
76
|
+
projectRoot: canonicalProjectRoot,
|
|
77
|
+
transcriptPath,
|
|
78
|
+
timeoutMs,
|
|
79
|
+
maxBuffer,
|
|
80
|
+
...(execFileFn === undefined ? {} : { execFileFn }),
|
|
81
|
+
});
|
|
104
82
|
} catch {
|
|
105
|
-
return unavailableResult(recordedAtMs, '
|
|
83
|
+
return unavailableResult(recordedAtMs, 'auditor_context_failed');
|
|
106
84
|
}
|
|
107
85
|
|
|
108
|
-
if (
|
|
109
|
-
return unavailableResult(recordedAtMs, `
|
|
110
|
-
}
|
|
111
|
-
if (!isObserverSnapshot(snapshot)) {
|
|
112
|
-
return unavailableResult(recordedAtMs, 'observer_read_invalid');
|
|
113
|
-
}
|
|
114
|
-
if (snapshot.host !== null && snapshot.host !== host) {
|
|
115
|
-
return unavailableResult(recordedAtMs, 'observer_host_mismatch');
|
|
116
|
-
}
|
|
117
|
-
if (snapshot.thread_sha256 !== null && snapshot.thread_sha256 !== expectedThreadHash(host, sessionId)) {
|
|
118
|
-
return unavailableResult(recordedAtMs, 'observer_session_mismatch');
|
|
86
|
+
if (snapshot.status !== 'fresh') {
|
|
87
|
+
return unavailableResult(recordedAtMs, `auditor_context_${snapshot.status}`);
|
|
119
88
|
}
|
|
120
89
|
return Object.freeze({
|
|
121
|
-
status:
|
|
90
|
+
status: EVALUATION_CONTEXT_AVAILABLE,
|
|
122
91
|
recordedAtMs,
|
|
123
|
-
snapshot
|
|
92
|
+
snapshot,
|
|
124
93
|
});
|
|
125
94
|
}
|
|
126
95
|
|
|
127
96
|
function unavailableResult(recordedAtMs, reason) {
|
|
128
97
|
return Object.freeze({
|
|
129
|
-
status:
|
|
98
|
+
status: EVALUATION_CONTEXT_UNAVAILABLE,
|
|
130
99
|
recordedAtMs,
|
|
131
100
|
reason,
|
|
132
101
|
snapshot: null,
|
|
133
102
|
});
|
|
134
103
|
}
|
|
135
104
|
|
|
136
|
-
function isDirectCommandConfig(config) {
|
|
137
|
-
return config && config.mode === 'throughline' &&
|
|
138
|
-
isAbsoluteProjectPath(config.command) &&
|
|
139
|
-
!/\.(?:cmd|bat)$/i.test(config.command) &&
|
|
140
|
-
Array.isArray(config.args) &&
|
|
141
|
-
config.args.every((arg) => typeof arg === 'string' && arg.length > 0);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
105
|
function isAbsoluteProjectPath(value) {
|
|
145
106
|
return isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value);
|
|
146
107
|
}
|
|
147
|
-
|
|
148
|
-
function isObserverSnapshot(value) {
|
|
149
|
-
if (!isRecord(value) || value.schema !== THROUGHLINE_OBSERVER_READ_SCHEMA || value.status !== 'snapshot') return false;
|
|
150
|
-
if (!Array.isArray(value.turns) || value.turns.length > DEFAULT_EVALUATION_OBSERVER_LIMIT) return false;
|
|
151
|
-
if (typeof value.historyTruncated !== 'boolean' || !isNullableString(value.afterCursor) || !isNullableString(value.throughCursor)) return false;
|
|
152
|
-
if (!isRecord(value.page) || typeof value.page.complete !== 'boolean' || !isNullableString(value.page.nextToken)) return false;
|
|
153
|
-
if (value.host === null) return value.thread_sha256 === null && value.turns.length === 0;
|
|
154
|
-
if ((value.host !== 'claude' && value.host !== 'codex') || !/^[a-f0-9]{64}$/.test(value.thread_sha256)) return false;
|
|
155
|
-
return value.turns.every((turn) => isRecord(turn) &&
|
|
156
|
-
turn.host === value.host && turn.thread_sha256 === value.thread_sha256);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Throughline observer-read hashes the Claude session ID directly. Codex hook
|
|
160
|
-
// session IDs may already carry Throughline's `codex:` namespace, while the
|
|
161
|
-
// observer feed hashes the underlying Codex thread ID.
|
|
162
|
-
function expectedThreadHash(host, sessionId) {
|
|
163
|
-
const threadId = host === 'codex' && sessionId.startsWith('codex:')
|
|
164
|
-
? sessionId.slice('codex:'.length)
|
|
165
|
-
: sessionId;
|
|
166
|
-
if (threadId.length === 0) throw new TypeError('sessionId must identify a thread');
|
|
167
|
-
return createHash('sha256').update(threadId, 'utf8').digest('hex');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function freezeSnapshot(snapshot) {
|
|
171
|
-
return Object.freeze({
|
|
172
|
-
...snapshot,
|
|
173
|
-
turns: Object.freeze(snapshot.turns.map((turn) => Object.freeze({ ...turn }))),
|
|
174
|
-
page: Object.freeze({ ...snapshot.page }),
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function isNullableString(value) {
|
|
179
|
-
return value === null || typeof value === 'string';
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function isRecord(value) {
|
|
183
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
184
|
-
}
|
|
@@ -167,8 +167,26 @@ function codexShellInputs(toolInput) {
|
|
|
167
167
|
function nestedMcpToolIds(toolInput) {
|
|
168
168
|
if (typeof toolInput !== 'string') return [];
|
|
169
169
|
const executable = maskJavaScriptLiteralsAndComments(toolInput);
|
|
170
|
-
const
|
|
171
|
-
|
|
170
|
+
const adopted = new Set(
|
|
171
|
+
[...executable.matchAll(/\btools\.(mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_.:/-]+)\s*\(/gu)]
|
|
172
|
+
.map((match) => match[1])
|
|
173
|
+
.filter(validCatalogId),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Codex may resolve a lazily exposed MCP tool by its exact name, then invoke it through the
|
|
177
|
+
// returned metadata object: `const tool = ALL_TOOLS.find(x => x.name === "mcp__...");
|
|
178
|
+
// await tools[tool.name](...)`. Count only bindings that are followed by that executable call.
|
|
179
|
+
const bindingPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*ALL_TOOLS\.find\s*\(\s*([A-Za-z_$][\w$]*)\s*=>\s*\2\.name\s*={2,3}/gu;
|
|
180
|
+
for (const binding of executable.matchAll(bindingPattern)) {
|
|
181
|
+
let valueStart = binding.index + binding[0].length;
|
|
182
|
+
while (/\s/u.test(toolInput[valueStart] ?? '')) valueStart += 1;
|
|
183
|
+
const parsedName = parseJavaScriptStringAt(toolInput, valueStart);
|
|
184
|
+
if (!parsedName || !MCP_ID_PATTERN.test(parsedName.value)) continue;
|
|
185
|
+
const variable = escapedRegExp(binding[1]);
|
|
186
|
+
const callPattern = new RegExp(String.raw`\btools\s*\[\s*${variable}\.name\s*\]\s*\(`, 'u');
|
|
187
|
+
if (callPattern.test(executable.slice(parsedName.end))) adopted.add(parsedName.value);
|
|
188
|
+
}
|
|
189
|
+
return [...adopted];
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
function escapedRegExp(value) {
|
|
@@ -27,7 +27,7 @@ import { discardLegacyPending } from './pending-context.mjs';
|
|
|
27
27
|
import { projectBackendFailure, projectParentAdvice, projectToolIds } from './parent-output-projector.mjs';
|
|
28
28
|
import { randomUUID } from 'node:crypto';
|
|
29
29
|
import { createEvaluationStore } from '../core/evaluation-store.mjs';
|
|
30
|
-
import {
|
|
30
|
+
import { loadEvaluationContext } from '../core/evaluation-context.mjs';
|
|
31
31
|
import { version } from '../version.mjs';
|
|
32
32
|
|
|
33
33
|
const TIMEOUT_MS = 50_000;
|
|
@@ -39,7 +39,7 @@ export async function runUserPrompt({
|
|
|
39
39
|
discardLegacyPendingFn = discardLegacyPending,
|
|
40
40
|
recordHookEventFn = recordClaudeHookEvent,
|
|
41
41
|
createEvaluationStoreFn = createEvaluationStore,
|
|
42
|
-
|
|
42
|
+
loadEvaluationContextFn = loadEvaluationContext,
|
|
43
43
|
randomUUIDFn = randomUUID,
|
|
44
44
|
spotterVersion = version,
|
|
45
45
|
now = Date.now,
|
|
@@ -65,11 +65,12 @@ export async function runUserPrompt({
|
|
|
65
65
|
? projectToolIds(Array.isArray(result?.missing_tools) ? result.missing_tools.map((entry) => entry?.name) : [])
|
|
66
66
|
: [];
|
|
67
67
|
const proposalRecordedAtMs = proposedToolIds.length > 0 ? now() : null;
|
|
68
|
-
const
|
|
69
|
-
? await
|
|
68
|
+
const evaluationContext = proposedToolIds.length > 0
|
|
69
|
+
? await loadEvaluationContextFn({
|
|
70
70
|
projectRoot,
|
|
71
71
|
host: 'claude',
|
|
72
72
|
sessionId,
|
|
73
|
+
transcriptPath: input.transcript_path,
|
|
73
74
|
recordedAtMs: proposalRecordedAtMs,
|
|
74
75
|
})
|
|
75
76
|
: { status: 'not_requested', snapshot: null };
|
|
@@ -87,8 +88,8 @@ export async function runUserPrompt({
|
|
|
87
88
|
auditStatus,
|
|
88
89
|
requestText: prompt,
|
|
89
90
|
auditorSeenContext: null,
|
|
90
|
-
observerContextStatus:
|
|
91
|
-
observerSnapshot:
|
|
91
|
+
observerContextStatus: evaluationContext.status,
|
|
92
|
+
observerSnapshot: evaluationContext.snapshot,
|
|
92
93
|
proposedToolIds,
|
|
93
94
|
backend: result?.evaluation_meta?.backend ?? null,
|
|
94
95
|
model: result?.evaluation_meta?.model ?? null,
|