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 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ごとに監査します。Throughlineは、提案が出た時に改善分析用の別文脈を
163
- `observer-read`で一度だけ記録する任意経路です。取得できなくても`context_unavailable`として記録するだけで、
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
- observer snapshotは端末内の評価SQLiteにだけ保存され、network送信、retry、background回収は行いません。
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、2種類の文脈、提案、利用、outcomeを確認
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
- `S/P/I/C/A/M`、提案率、採用率、project/tool内訳、非採用case、別々に取得した2種類の文脈を
240
- 確認できる。health確認は端末一覧request時だけなので、端末がofflineでもbackground監視や
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 may call
164
- Throughline `observer-read` once to save separate improvement evidence. A failed
165
- read is recorded as `context_unavailable` and never changes auditing or parent advice.
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, Spotter context, Throughline snapshot, proposal, usage, and outcome
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, and the two
248
- separately captured context sources. The hub checks health only
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
 
@@ -1,5 +1,8 @@
1
1
  # Spotter評価dashboard運用
2
2
 
3
+ 現行配布版: **v1.5.4**(2026-08-05)。Mac、main-server、FOX WSL2、FOX Windows nativeへ
4
+ global install済み。v1.5.4は監査gate修正でありdashboard routing構成はv1.5.3から変更していない。
5
+
3
6
  ## 固定構成
4
7
 
5
8
  各端末のdevice serverはloopbackだけで待ち受ける。main-server、Mac、FOX WSL2は
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 { loadEvaluationObserverContext } from '../core/evaluation-context.mjs';
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
- loadEvaluationObserverContextFn = loadEvaluationObserverContext,
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 observerContext = { status: 'not_requested', snapshot: null };
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
- observerContext = await loadEvaluationObserverContextFn({
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: observerContext.status,
178
- observerSnapshot: observerContext.snapshot,
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 THROUGHLINE_OBSERVER_READ_SCHEMA = 'throughline.observer_read.v1';
14
- export const EVALUATION_OBSERVER_CONTEXT_AVAILABLE = 'context_available';
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 one bounded, proposal-time Throughline observer snapshot.
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 loadEvaluationObserverContext({
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 = execFileAsync,
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 = JSON.parse(stdout);
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, 'observer_read_invalid');
83
+ return unavailableResult(recordedAtMs, 'auditor_context_failed');
106
84
  }
107
85
 
108
- if (UNAVAILABLE_STATUSES.has(snapshot?.status)) {
109
- return unavailableResult(recordedAtMs, `observer_${snapshot.status}`);
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: EVALUATION_OBSERVER_CONTEXT_AVAILABLE,
90
+ status: EVALUATION_CONTEXT_AVAILABLE,
122
91
  recordedAtMs,
123
- snapshot: freezeSnapshot(snapshot),
92
+ snapshot,
124
93
  });
125
94
  }
126
95
 
127
96
  function unavailableResult(recordedAtMs, reason) {
128
97
  return Object.freeze({
129
- status: EVALUATION_OBSERVER_CONTEXT_UNAVAILABLE,
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 matches = executable.matchAll(/\btools\.(mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_.:/-]+)\s*\(/gu);
171
- return [...new Set([...matches].map((match) => match[1]).filter(validCatalogId))];
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 { loadEvaluationObserverContext } from '../core/evaluation-context.mjs';
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
- loadEvaluationObserverContextFn = loadEvaluationObserverContext,
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 observer = proposedToolIds.length > 0
69
- ? await loadEvaluationObserverContextFn({
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: observer.status,
91
- observerSnapshot: observer.snapshot,
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,