claude-spotter 1.5.3 → 1.5.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 +25 -0
- package/README.ja.md +19 -25
- package/README.md +19 -30
- package/bin/spotter.mjs +2 -2
- package/docs/11_dashboard-operations.md +3 -0
- package/package.json +1 -1
- package/src/cli/codex-hook-cmd.mjs +9 -124
- package/src/cli/doctor.mjs +1 -1
- package/src/cli/factory-diagnostics.mjs +1 -1
- package/src/cli/install.mjs +3 -3
- package/src/core/evaluation-context.mjs +28 -105
- package/src/daemon/daemon.mjs +1 -24
- package/src/hooks/user-prompt.mjs +9 -118
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.5 — 2026-08-05
|
|
4
|
+
|
|
5
|
+
- **提案時文脈をexact sessionへ戻した。** 評価記録がproject全体の最新threadを読む
|
|
6
|
+
`observer-read`を誤って使っていた経路を撤去し、既存のThroughline `auditor-context`へ一本化した。
|
|
7
|
+
Claude / CodexのUserPromptSubmit hookから提案元の`session_id`、`host`、`transcript_path`を渡し、
|
|
8
|
+
提案直前の完了turnを非採用caseへ保存できる。
|
|
9
|
+
- **重複adapterを廃止。** 評価専用readerは、すでにfreshness・本文上限・exact-session照合を持つ
|
|
10
|
+
`loadAuditorContext`を再利用する。Throughline本文をauditor入力へ戻さず、監査と評価証拠の分離は維持する。
|
|
11
|
+
- **検証。** 既存adapterへの引数接続とClaude / Codex hookのfocused testを通過した。
|
|
12
|
+
|
|
13
|
+
## 1.5.4 — 2026-08-05
|
|
14
|
+
|
|
15
|
+
- **Throughlineを提案監査の実行条件から撤去。** Claude / CodexのUserPromptSubmitは、
|
|
16
|
+
`auditorContext`のmode、Throughlineのfreshness、取得成否に関係なく、現在のuser promptと
|
|
17
|
+
host-local tool catalogで監査する。daemonも旧`audit:false`やcontext payloadで監査を止めず、
|
|
18
|
+
Throughline本文を監査AIへ渡さない。
|
|
19
|
+
- **評価文脈だけを独立取得。** 提案が出た時のThroughline `observer-read`は改善用証拠として
|
|
20
|
+
一度だけ取得し、失敗時は評価文脈だけをunavailableにする。監査結果、親へのtool提案、
|
|
21
|
+
成功Hook eventには影響させず、retryやbackground回収も追加しない。
|
|
22
|
+
- **停止を正常passへ偽装しない。** 旧版で`context_disabled` / `context_not_fresh` /
|
|
23
|
+
`auditor_context`となっていた経路を削除し、以後の監査成功turnは提案なしの場合も評価母数へ入る。
|
|
24
|
+
install / doctor / READMEもThroughlineを`evaluation context`として表示する。
|
|
25
|
+
- **検証。** disabled / stale / provider error / legacy payload / observer-read failureを含む
|
|
26
|
+
Claude・Codex回帰テストとfull suite 588件(586 pass / 2 platform skip)を通過した。
|
|
27
|
+
|
|
3
28
|
## 1.5.3 — 2026-08-04
|
|
4
29
|
|
|
5
30
|
- **dashboardの難解な集計略号を廃止。** 概要cardとproject/tool別内訳の
|
package/README.ja.md
CHANGED
|
@@ -156,11 +156,16 @@ flowchart LR
|
|
|
156
156
|
|
|
157
157
|
両者に共通するのは **「主体に頼らない仕組み」**。併用できます。
|
|
158
158
|
|
|
159
|
-
### Throughline
|
|
159
|
+
### Throughlineの提案時評価文脈(任意)
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
`
|
|
161
|
+
Spotterの提案AIはThroughlineを使わず、Throughlineの導入・設定・freshness・取得成否に関係なく
|
|
162
|
+
UserPromptSubmitごとに監査します。提案が出た時だけ、改善分析用の別文脈としてThroughline
|
|
163
|
+
`auditor-context`を提案元のexact sessionとtranscriptで一度だけ呼びます。返されたfreshな直前完了turnは
|
|
164
|
+
監査入力と混ぜずに保存します。取得できなくても`context_unavailable`として記録するだけで、監査や親への助言には影響しません。
|
|
165
|
+
|
|
166
|
+
`spotter install`がPATH上のThroughlineを絶対パスへ解決できる場合、この評価証拠の取得経路を既定で設定します。
|
|
167
|
+
既存互換の`--auditor-context`名はmarker設定に残っていますが、監査のON/OFFは制御しません。
|
|
168
|
+
評価文脈の取得だけを無効化するには次を実行します。
|
|
164
169
|
|
|
165
170
|
```bash
|
|
166
171
|
spotter install -y --auditor-context disabled
|
|
@@ -183,24 +188,8 @@ spotter install -y --auditor-context throughline `
|
|
|
183
188
|
--throughline-arg 'C:\absolute\path\to\throughline\bin\throughline.mjs'
|
|
184
189
|
```
|
|
185
190
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
AI 呼出し候補になり、それ以外の status では AI を呼びません。enabled connector の障害は hidden fallback
|
|
189
|
-
ではなく固定 warning として出します。
|
|
190
|
-
|
|
191
|
-
Throughline から渡すのは fresh な完了済み L2 user/assistant pair だけです。直近 2 pair (N=2)、各 body は
|
|
192
|
-
600 文字、合計は 4,000 文字に制限します。Spotter は Throughline の L2、`reason`、`raw` を親へ反射せず、
|
|
193
|
-
親には安全な catalog tool ID から作る固定・非命令形の助言だけを渡します。`spotter doctor` は command / args
|
|
194
|
-
を表示せず、auditor-context mode と固定の availability detail だけを表示します。
|
|
195
|
-
|
|
196
|
-
v2 model-matrix では context choice を明示できます。
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.v2.json \
|
|
200
|
-
--recent-turns 2 --body-cap 600
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
評価結論はN=2 / 600です。既定ONは確定済みで、7日・30 fresh resultの実運用測定は精度改善に使います。
|
|
191
|
+
`spotter doctor`はこの経路を`evaluation context`として表示し、command / argsや会話本文は表示しません。
|
|
192
|
+
評価文脈は端末内の評価SQLiteにだけ保存され、network送信、retry、background回収は行いません。
|
|
204
193
|
|
|
205
194
|
## よく使うコマンド
|
|
206
195
|
|
|
@@ -217,12 +206,16 @@ spotter db rebuild # Claude local + Claude global DB を両方消してか
|
|
|
217
206
|
spotter status # 稼働中の daemon 一覧
|
|
218
207
|
spotter doctor # 環境診断 (Node / claude CLI / Codex readiness / tool-db 整合性)
|
|
219
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送信なし)
|
|
220
213
|
spotter evaluation report
|
|
221
214
|
# 端末内DBからproject横断の提案率・tool採用率を集計
|
|
222
215
|
spotter evaluation cases --outcome not-adopted
|
|
223
216
|
# 提案されたが同じturnで使われなかったtool itemを一覧
|
|
224
217
|
spotter evaluation case <observation-id>
|
|
225
|
-
# request
|
|
218
|
+
# request、任意のThroughline snapshot、提案、利用、outcomeを確認
|
|
226
219
|
spotter dashboard device --id mac --name Mac
|
|
227
220
|
# この端末の評価DBを127.0.0.1:53940で配信
|
|
228
221
|
spotter dashboard hub --config dashboard-hub.json --host 172.18.0.1
|
|
@@ -247,8 +240,9 @@ spotter uninstall # hook 登録を解除 (~/.spotter は残す)
|
|
|
247
240
|
|
|
248
241
|
dashboardはlocal-firstで動く。各端末が自身の`~/.spotter/evaluation.db`を読み、hubは固定の
|
|
249
242
|
端末・upstream対応だけを持つ。評価データをcloud DBへ複製しない。端末画面では
|
|
250
|
-
|
|
251
|
-
|
|
243
|
+
対象ターン、ツール提案あり、提案ツール数、利用判定済み、実際に使用、判定不能、提案率、採用率、
|
|
244
|
+
project/tool内訳、非採用case、監査対象request、任意の提案時Throughline証拠を確認できる。
|
|
245
|
+
health確認は端末一覧request時だけなので、端末がofflineでもbackground監視や
|
|
252
246
|
retry queueを作らず、その端末だけを切り離せる。
|
|
253
247
|
|
|
254
248
|
4端末のservice、reverse tunnel、Caddy/Cloudflare構成は
|
package/README.md
CHANGED
|
@@ -156,12 +156,20 @@ The audited catalog is host-local: Claude uses `<project>/.spotter/tool-db.json`
|
|
|
156
156
|
|
|
157
157
|
Both share the principle of **"don't rely on the primary agent to do it itself."** They compose well — you can run them together.
|
|
158
158
|
|
|
159
|
-
### Throughline
|
|
159
|
+
### Optional Throughline evidence for proposal evaluation
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
Spotter's proposal auditor does not use Throughline. Every UserPromptSubmit
|
|
162
|
+
audit runs independently of Throughline installation, configuration, freshness, or read
|
|
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.
|
|
168
|
+
|
|
169
|
+
When `spotter install` resolves Throughline on PATH to an absolute executable, it
|
|
170
|
+
configures this evaluation-evidence path by default. The legacy option name
|
|
171
|
+
`--auditor-context` remains for marker compatibility but no longer controls whether
|
|
172
|
+
auditing runs. Disable only the evidence capture with:
|
|
165
173
|
|
|
166
174
|
```bash
|
|
167
175
|
spotter install -y --auditor-context disabled
|
|
@@ -185,28 +193,9 @@ spotter install -y --auditor-context throughline `
|
|
|
185
193
|
--throughline-arg 'C:\absolute\path\to\throughline\bin\throughline.mjs'
|
|
186
194
|
```
|
|
187
195
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
eligible for an AI call; every other status skips AI. An enabled connector
|
|
192
|
-
failure becomes a fixed warning, not a hidden fallback.
|
|
193
|
-
|
|
194
|
-
Throughline contributes only fresh, completed L2 user/assistant pairs: two
|
|
195
|
-
recent pairs (N=2), each body capped at 600 characters and 4,000 characters in
|
|
196
|
-
total. Spotter never reflects Throughline L2, `reason`, or `raw` to the parent.
|
|
197
|
-
The parent receives only fixed non-imperative advice built from safe catalog tool
|
|
198
|
-
IDs. `spotter doctor` displays the auditor-context mode and a fixed availability
|
|
199
|
-
detail without printing its command or arguments.
|
|
200
|
-
|
|
201
|
-
The v2 model-matrix can make the context choice explicit:
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.v2.json \
|
|
205
|
-
--recent-turns 2 --body-cap 600
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
The evaluated setting is N=2 / 600. Default-on is final; the 7-day,
|
|
209
|
-
30-fresh-result sample is used only to identify precision improvements.
|
|
196
|
+
`spotter doctor` reports this as `evaluation context` without printing commands,
|
|
197
|
+
arguments, or conversation text. Observer snapshots stay in the terminal-local
|
|
198
|
+
evaluation SQLite. Spotter adds no network upload, retry, or background recovery.
|
|
210
199
|
|
|
211
200
|
## Common commands
|
|
212
201
|
|
|
@@ -231,7 +220,7 @@ spotter evaluation report
|
|
|
231
220
|
spotter evaluation cases --outcome not-adopted
|
|
232
221
|
# list proposed tools that were not used in the same turn
|
|
233
222
|
spotter evaluation case <observation-id>
|
|
234
|
-
# inspect request,
|
|
223
|
+
# inspect request, optional Throughline snapshot, proposal, usage, and outcome
|
|
235
224
|
spotter dashboard device --id mac --name Mac
|
|
236
225
|
# serve this terminal's local evaluation DB on 127.0.0.1:53940
|
|
237
226
|
spotter dashboard hub --config dashboard-hub.json --host 172.18.0.1
|
|
@@ -257,8 +246,8 @@ spotter uninstall # remove hooks from this project (leaves ~/.spotter int
|
|
|
257
246
|
The dashboard is local-first. Every terminal reads its own `~/.spotter/evaluation.db`; the hub
|
|
258
247
|
keeps only a static device-to-upstream map and does not copy evaluation data into a cloud database.
|
|
259
248
|
The device view shows Japanese labels for every evaluation metric, proposal and adoption rates
|
|
260
|
-
with their numerator and denominator, project/tool breakdowns, non-adopted cases,
|
|
261
|
-
|
|
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
|
|
262
251
|
when the device list is requested, so an offline terminal is isolated without a background monitor
|
|
263
252
|
or retry queue.
|
|
264
253
|
|
package/bin/spotter.mjs
CHANGED
|
@@ -28,8 +28,8 @@ Usage:
|
|
|
28
28
|
[--throughline-command ABS] [--throughline-arg VALUE]
|
|
29
29
|
register hooks in <cwd>/.claude/settings.json
|
|
30
30
|
and create <cwd>/.spotter/marker.json
|
|
31
|
-
(Throughline on PATH enables
|
|
32
|
-
|
|
31
|
+
(Throughline on PATH enables proposal-time
|
|
32
|
+
evaluation evidence by default; it never gates auditing)
|
|
33
33
|
(run inside each project you want audited)
|
|
34
34
|
spotter install --user [-y] legacy: register globally in ~/.claude/settings.json
|
|
35
35
|
(NOT RECOMMENDED — fires for every Claude Code session
|
package/package.json
CHANGED
|
@@ -30,13 +30,9 @@ import {
|
|
|
30
30
|
hookEventsPath,
|
|
31
31
|
summarizeHookEvents,
|
|
32
32
|
} from '../core/hook-event-log.mjs';
|
|
33
|
-
import {
|
|
34
|
-
loadAuditorContext,
|
|
35
|
-
readProjectAuditorContextConfig,
|
|
36
|
-
} from '../core/auditor-context.mjs';
|
|
37
33
|
import { observeRuntimeErrorIsolatedSafe } from '../core/runtime-error-store.mjs';
|
|
38
34
|
import { createEvaluationStore } from '../core/evaluation-store.mjs';
|
|
39
|
-
import {
|
|
35
|
+
import { loadEvaluationContext } from '../core/evaluation-context.mjs';
|
|
40
36
|
import {
|
|
41
37
|
canonicalizeCodexNestedMcpToolIds,
|
|
42
38
|
canonicalizeCodexSkillReadToolIds,
|
|
@@ -130,13 +126,11 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
130
126
|
createAuditorBackendFn = createAuditorBackend,
|
|
131
127
|
recordHookEventFn = appendCodexHookEvent,
|
|
132
128
|
discardLegacyPendingFn = discardLegacyPending,
|
|
133
|
-
readAuditorContextConfigFn = readProjectAuditorContextConfig,
|
|
134
|
-
loadAuditorContextFn = loadAuditorContext,
|
|
135
129
|
writeOutput = (text) => process.stdout.write(text),
|
|
136
130
|
writeError = (text) => process.stderr.write(text),
|
|
137
131
|
runtimeErrorObserver = async () => ({ collected: false, reason: 'observer_not_configured' }),
|
|
138
132
|
createEvaluationStoreFn = createEvaluationStore,
|
|
139
|
-
|
|
133
|
+
loadEvaluationContextFn = loadEvaluationContext,
|
|
140
134
|
randomUUIDFn = randomUUID,
|
|
141
135
|
now = () => Date.now(),
|
|
142
136
|
} = {}) {
|
|
@@ -147,27 +141,25 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
147
141
|
const projectRoot = findSpotterMarker(input.cwd);
|
|
148
142
|
if (!projectRoot) return;
|
|
149
143
|
const startedAt = Date.now();
|
|
150
|
-
let contextDurationMs = null;
|
|
151
|
-
let auditorContextConfig;
|
|
152
144
|
|
|
153
145
|
const prompt = requireString(input, 'prompt');
|
|
154
146
|
const sessionId = codexSessionId(input);
|
|
155
|
-
const recordEvaluation = async ({ auditStatus, proposedToolIds = [], backend = null, model = null
|
|
147
|
+
const recordEvaluation = async ({ auditStatus, proposedToolIds = [], backend = null, model = null }) => {
|
|
156
148
|
if (!sessionId) {
|
|
157
149
|
reportEvaluationFailure(writeError);
|
|
158
150
|
return;
|
|
159
151
|
}
|
|
160
152
|
const proposals = canonicalizeProposedToolIds(proposedToolIds);
|
|
161
|
-
let
|
|
153
|
+
let evaluationContext = { status: 'not_requested', snapshot: null };
|
|
162
154
|
let proposedAtMs = recordedAtMs;
|
|
163
155
|
try {
|
|
164
156
|
if (auditStatus === 'success' && proposals.resolvedToolIds.length > 0) {
|
|
165
157
|
proposedAtMs = now();
|
|
166
|
-
|
|
158
|
+
evaluationContext = await loadEvaluationContextFn({
|
|
167
159
|
projectRoot,
|
|
168
160
|
host: 'codex',
|
|
169
161
|
sessionId,
|
|
170
|
-
|
|
162
|
+
transcriptPath: input.transcript_path,
|
|
171
163
|
recordedAtMs: proposedAtMs,
|
|
172
164
|
});
|
|
173
165
|
}
|
|
@@ -182,9 +174,9 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
182
174
|
sessionId,
|
|
183
175
|
auditStatus,
|
|
184
176
|
requestText: prompt,
|
|
185
|
-
auditorSeenContext,
|
|
186
|
-
observerContextStatus:
|
|
187
|
-
observerSnapshot:
|
|
177
|
+
auditorSeenContext: null,
|
|
178
|
+
observerContextStatus: evaluationContext.status,
|
|
179
|
+
observerSnapshot: evaluationContext.snapshot,
|
|
188
180
|
proposedToolIds: proposals.resolvedToolIds,
|
|
189
181
|
backend,
|
|
190
182
|
model,
|
|
@@ -198,97 +190,6 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
198
190
|
}
|
|
199
191
|
};
|
|
200
192
|
const legacyPending = await discardLegacyPendingFn({ projectRoot, sessionId: codexSessionId(input) });
|
|
201
|
-
let context;
|
|
202
|
-
const contextStartedAt = Date.now();
|
|
203
|
-
try {
|
|
204
|
-
const config = await readAuditorContextConfigFn(projectRoot);
|
|
205
|
-
auditorContextConfig = config;
|
|
206
|
-
contextDurationMs = Date.now() - contextStartedAt;
|
|
207
|
-
if (config.mode === 'disabled') {
|
|
208
|
-
await recordEvaluation({ auditStatus: 'skipped' });
|
|
209
|
-
await recordCodexHookEventSafe(recordHookEventFn, {
|
|
210
|
-
projectRoot,
|
|
211
|
-
event: {
|
|
212
|
-
hook: 'UserPromptSubmit',
|
|
213
|
-
status: 'skipped',
|
|
214
|
-
reason: 'context_disabled',
|
|
215
|
-
contextStatus: 'disabled',
|
|
216
|
-
contextDurationMs,
|
|
217
|
-
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
218
|
-
durationMs: Date.now() - startedAt,
|
|
219
|
-
},
|
|
220
|
-
}, writeError);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
context = await loadAuditorContextFn({
|
|
224
|
-
config,
|
|
225
|
-
host: 'codex',
|
|
226
|
-
sessionId: requireCodexSessionId(input),
|
|
227
|
-
projectRoot,
|
|
228
|
-
transcriptPath: requireString(input, 'transcript_path'),
|
|
229
|
-
});
|
|
230
|
-
contextDurationMs = Date.now() - contextStartedAt;
|
|
231
|
-
} catch (err) {
|
|
232
|
-
await recordEvaluation({ auditStatus: 'error' });
|
|
233
|
-
await observeRuntimeFailure(runtimeErrorObserver, 'auditor_unavailable');
|
|
234
|
-
contextDurationMs = Date.now() - contextStartedAt;
|
|
235
|
-
const failure = projectBackendFailure(err?.code);
|
|
236
|
-
safeWriteError(writeError, failure.stderr);
|
|
237
|
-
await recordCodexHookEventSafe(recordHookEventFn, {
|
|
238
|
-
projectRoot,
|
|
239
|
-
event: {
|
|
240
|
-
hook: 'UserPromptSubmit',
|
|
241
|
-
status: 'error',
|
|
242
|
-
code: failure.code,
|
|
243
|
-
reason: 'auditor_context',
|
|
244
|
-
contextDurationMs,
|
|
245
|
-
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
246
|
-
durationMs: Date.now() - startedAt,
|
|
247
|
-
},
|
|
248
|
-
}, writeError);
|
|
249
|
-
writeCodexSystemMessage({ systemMessage: failure.systemMessage, writeOutput });
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
if (context.status === 'unavailable' || context.status === 'schema_mismatch') {
|
|
253
|
-
await recordEvaluation({ auditStatus: 'error' });
|
|
254
|
-
await observeRuntimeFailure(runtimeErrorObserver, 'auditor_unavailable');
|
|
255
|
-
const failure = projectBackendFailure(context.status === 'unavailable'
|
|
256
|
-
? 'E_AUDITOR_CONTEXT_UNAVAILABLE'
|
|
257
|
-
: 'E_AUDITOR_CONTEXT_SCHEMA');
|
|
258
|
-
safeWriteError(writeError, failure.stderr);
|
|
259
|
-
await recordCodexHookEventSafe(recordHookEventFn, {
|
|
260
|
-
projectRoot,
|
|
261
|
-
event: {
|
|
262
|
-
hook: 'UserPromptSubmit',
|
|
263
|
-
status: 'error',
|
|
264
|
-
code: failure.code,
|
|
265
|
-
reason: 'auditor_context_status',
|
|
266
|
-
contextDurationMs,
|
|
267
|
-
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
268
|
-
durationMs: Date.now() - startedAt,
|
|
269
|
-
},
|
|
270
|
-
}, writeError);
|
|
271
|
-
writeCodexSystemMessage({ systemMessage: failure.systemMessage, writeOutput });
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (context.status !== 'fresh') {
|
|
275
|
-
await recordEvaluation({ auditStatus: 'skipped' });
|
|
276
|
-
await recordCodexHookEventSafe(recordHookEventFn, {
|
|
277
|
-
projectRoot,
|
|
278
|
-
event: {
|
|
279
|
-
hook: 'UserPromptSubmit',
|
|
280
|
-
status: 'skipped',
|
|
281
|
-
reason: 'context_not_fresh',
|
|
282
|
-
contextStatus: context.status,
|
|
283
|
-
contextTurns: 0,
|
|
284
|
-
contextChars: 0,
|
|
285
|
-
contextDurationMs,
|
|
286
|
-
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
287
|
-
durationMs: Date.now() - startedAt,
|
|
288
|
-
},
|
|
289
|
-
}, writeError);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
193
|
|
|
293
194
|
let catalog;
|
|
294
195
|
let backend;
|
|
@@ -301,8 +202,6 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
301
202
|
judgment = await backend.judge({
|
|
302
203
|
stage: 'user_input',
|
|
303
204
|
userInput: prompt,
|
|
304
|
-
recentContext: context.turns,
|
|
305
|
-
contextStatus: 'fresh',
|
|
306
205
|
});
|
|
307
206
|
} catch (err) {
|
|
308
207
|
await recordEvaluation({ auditStatus: 'error', backend: err?.backend ?? null, model: err?.diagnostics?.modelSelection?.effectiveModel ?? null });
|
|
@@ -332,10 +231,6 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
332
231
|
backend: judgment.meta?.backend ?? backend.name ?? 'unknown',
|
|
333
232
|
pass: judgment.pass,
|
|
334
233
|
missingTools: projectToolIds(judgment.findings.map((finding) => finding.toolName)),
|
|
335
|
-
contextStatus: 'fresh',
|
|
336
|
-
contextTurns: context.stats.returnedTurns,
|
|
337
|
-
contextChars: context.stats.chars,
|
|
338
|
-
contextDurationMs,
|
|
339
234
|
...compactCodexModelSelectionForEvent(judgment.meta?.modelSelection),
|
|
340
235
|
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
341
236
|
backendDurationMs: judgment.meta?.durationMs ?? null,
|
|
@@ -357,8 +252,6 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
357
252
|
proposedToolIds: toolIds,
|
|
358
253
|
backend: judgment.meta?.backend ?? backend.name ?? 'unknown',
|
|
359
254
|
model: judgment.meta?.modelSelection?.effectiveModel ?? null,
|
|
360
|
-
auditorSeenContext: JSON.stringify(context.turns),
|
|
361
|
-
config: auditorContextConfig,
|
|
362
255
|
});
|
|
363
256
|
const advice = projectParentAdvice(toolIds);
|
|
364
257
|
if (advice) writeCodexUserPromptContexts({ contexts: [advice], writeOutput });
|
|
@@ -848,14 +741,6 @@ function codexSessionId(payload) {
|
|
|
848
741
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
849
742
|
}
|
|
850
743
|
|
|
851
|
-
function requireCodexSessionId(payload) {
|
|
852
|
-
const value = codexSessionId(payload);
|
|
853
|
-
if (value) return value;
|
|
854
|
-
const err = new Error('session_id is required');
|
|
855
|
-
err.code = 'E_AUDITOR_CONTEXT_INPUT';
|
|
856
|
-
throw err;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
744
|
// v1.4.19: legacy pending migration is handled only by `discardLegacyPending` on
|
|
860
745
|
// UserPromptSubmit. Stop findings are never persisted for a future parent prompt.
|
|
861
746
|
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -78,7 +78,7 @@ export async function runDoctor() {
|
|
|
78
78
|
if (!sidecar.ok) warnings += 1;
|
|
79
79
|
|
|
80
80
|
const auditorContext = await inspectAuditorContextConfiguration({ projectRoot });
|
|
81
|
-
mark(auditorContext.ok, `
|
|
81
|
+
mark(auditorContext.ok, `evaluation context: ${auditorContext.mode}`, auditorContext.detail);
|
|
82
82
|
if (!auditorContext.ok) warnings += 1;
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -45,7 +45,7 @@ export async function runFactoryDiagnostics({
|
|
|
45
45
|
if (contextMode === null) {
|
|
46
46
|
checks.push(check('throughline_context', 'fail', 'invalid_context_configuration'));
|
|
47
47
|
} else if (contextMode === 'disabled') {
|
|
48
|
-
checks.push(check('throughline_context', 'skipped', '
|
|
48
|
+
checks.push(check('throughline_context', 'skipped', 'evaluation_evidence_disabled'));
|
|
49
49
|
} else {
|
|
50
50
|
const context = await inspectAuditorContextFn({ projectRoot });
|
|
51
51
|
checks.push(context?.ok === true && context?.mode === 'throughline'
|
package/src/cli/install.mjs
CHANGED
|
@@ -102,12 +102,12 @@ export async function runInstall({
|
|
|
102
102
|
};
|
|
103
103
|
await writeFile(markerPath, JSON.stringify(marker, null, 2) + '\n', 'utf8');
|
|
104
104
|
console.log(` wrote ${markerPath}`);
|
|
105
|
-
console.log(`
|
|
105
|
+
console.log(` evaluation context provider: ${preservedAuditorContext.mode} (${preservedAuditorContext.origin})`);
|
|
106
106
|
if (preservedAuditorContext.mode === 'throughline') {
|
|
107
|
-
console.log('
|
|
107
|
+
console.log(' captures bounded proposal-time observer evidence; this never gates auditing; disable evidence capture with:');
|
|
108
108
|
console.log(' spotter install -y --auditor-context disabled');
|
|
109
109
|
} else if (preservedAuditorContext.reason === 'throughline_unavailable') {
|
|
110
|
-
console.log(' Throughline was not found;
|
|
110
|
+
console.log(' Throughline was not found; proposal-time observer evidence is unavailable, but auditing remains active');
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -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
|
-
}
|
package/src/daemon/daemon.mjs
CHANGED
|
@@ -277,30 +277,7 @@ export async function startDaemon({
|
|
|
277
277
|
? payload.observation_id
|
|
278
278
|
: null;
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
logFn('user_input: audit skipped because fresh context was unavailable');
|
|
282
|
-
return { pass: true, missing_tools: [], reason: 'auditor_context_not_fresh' };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const hasContext = payload.context_status !== undefined || payload.recent_context !== undefined;
|
|
286
|
-
if (hasContext && (payload.context_status !== 'fresh' || !Array.isArray(payload.recent_context))) {
|
|
287
|
-
const err = new Error('context-bearing user_input payload must include fresh recent_context');
|
|
288
|
-
err.code = 'E_AUDITOR_CONTEXT_INPUT';
|
|
289
|
-
throw err;
|
|
290
|
-
}
|
|
291
|
-
if (hasContext && auditorBackend.name === 'haiku') {
|
|
292
|
-
const err = new Error('recent conversation context is not supported by the haiku auditor backend');
|
|
293
|
-
err.code = 'E_AUDITOR_CONTEXT_BACKEND_UNSUPPORTED';
|
|
294
|
-
throw err;
|
|
295
|
-
}
|
|
296
|
-
const judgment = await runAuditorJudgment(hasContext
|
|
297
|
-
? {
|
|
298
|
-
stage: 'user_input',
|
|
299
|
-
userInput,
|
|
300
|
-
recentContext: payload.recent_context,
|
|
301
|
-
contextStatus: 'fresh',
|
|
302
|
-
}
|
|
303
|
-
: { stage: 'user_input', userInput });
|
|
280
|
+
const judgment = await runAuditorJudgment({ stage: 'user_input', userInput });
|
|
304
281
|
const result = legacyResultFromJudgment(judgment);
|
|
305
282
|
const meta = judgment.meta ?? {};
|
|
306
283
|
logFn(
|
|
@@ -25,13 +25,9 @@ import { sendRequest, TransportError } from '../daemon/transport.mjs';
|
|
|
25
25
|
import { spawnDaemonAndWaitReady } from './spawn-daemon.mjs';
|
|
26
26
|
import { discardLegacyPending } from './pending-context.mjs';
|
|
27
27
|
import { projectBackendFailure, projectParentAdvice, projectToolIds } from './parent-output-projector.mjs';
|
|
28
|
-
import {
|
|
29
|
-
loadAuditorContext,
|
|
30
|
-
readProjectAuditorContextConfig,
|
|
31
|
-
} from '../core/auditor-context.mjs';
|
|
32
28
|
import { randomUUID } from 'node:crypto';
|
|
33
29
|
import { createEvaluationStore } from '../core/evaluation-store.mjs';
|
|
34
|
-
import {
|
|
30
|
+
import { loadEvaluationContext } from '../core/evaluation-context.mjs';
|
|
35
31
|
import { version } from '../version.mjs';
|
|
36
32
|
|
|
37
33
|
const TIMEOUT_MS = 50_000;
|
|
@@ -41,11 +37,9 @@ export async function runUserPrompt({
|
|
|
41
37
|
sendRequestFn = sendRequest,
|
|
42
38
|
spawnDaemonAndWaitReadyFn = spawnDaemonAndWaitReady,
|
|
43
39
|
discardLegacyPendingFn = discardLegacyPending,
|
|
44
|
-
readAuditorContextConfigFn = readProjectAuditorContextConfig,
|
|
45
|
-
loadAuditorContextFn = loadAuditorContext,
|
|
46
40
|
recordHookEventFn = recordClaudeHookEvent,
|
|
47
41
|
createEvaluationStoreFn = createEvaluationStore,
|
|
48
|
-
|
|
42
|
+
loadEvaluationContextFn = loadEvaluationContext,
|
|
49
43
|
randomUUIDFn = randomUUID,
|
|
50
44
|
spotterVersion = version,
|
|
51
45
|
now = Date.now,
|
|
@@ -62,22 +56,21 @@ export async function runUserPrompt({
|
|
|
62
56
|
const observationId = randomUUIDFn();
|
|
63
57
|
const projectRoot = findSpotterMarker(input.cwd);
|
|
64
58
|
const startedAt = Date.now();
|
|
65
|
-
let contextDurationMs = null;
|
|
66
59
|
let evaluationWriteFailed = false;
|
|
67
60
|
|
|
68
|
-
const safelyRecordEvaluation = async ({ auditStatus, result = null
|
|
61
|
+
const safelyRecordEvaluation = async ({ auditStatus, result = null }) => {
|
|
69
62
|
if (!projectRoot) return;
|
|
70
63
|
try {
|
|
71
64
|
const proposedToolIds = auditStatus === 'success' && result?.pass !== true
|
|
72
65
|
? projectToolIds(Array.isArray(result?.missing_tools) ? result.missing_tools.map((entry) => entry?.name) : [])
|
|
73
66
|
: [];
|
|
74
67
|
const proposalRecordedAtMs = proposedToolIds.length > 0 ? now() : null;
|
|
75
|
-
const
|
|
76
|
-
? await
|
|
68
|
+
const evaluationContext = proposedToolIds.length > 0
|
|
69
|
+
? await loadEvaluationContextFn({
|
|
77
70
|
projectRoot,
|
|
78
71
|
host: 'claude',
|
|
79
72
|
sessionId,
|
|
80
|
-
|
|
73
|
+
transcriptPath: input.transcript_path,
|
|
81
74
|
recordedAtMs: proposalRecordedAtMs,
|
|
82
75
|
})
|
|
83
76
|
: { status: 'not_requested', snapshot: null };
|
|
@@ -94,9 +87,9 @@ export async function runUserPrompt({
|
|
|
94
87
|
sessionId,
|
|
95
88
|
auditStatus,
|
|
96
89
|
requestText: prompt,
|
|
97
|
-
auditorSeenContext:
|
|
98
|
-
observerContextStatus:
|
|
99
|
-
observerSnapshot:
|
|
90
|
+
auditorSeenContext: null,
|
|
91
|
+
observerContextStatus: evaluationContext.status,
|
|
92
|
+
observerSnapshot: evaluationContext.snapshot,
|
|
100
93
|
proposedToolIds,
|
|
101
94
|
backend: result?.evaluation_meta?.backend ?? null,
|
|
102
95
|
model: result?.evaluation_meta?.model ?? null,
|
|
@@ -128,7 +121,6 @@ export async function runUserPrompt({
|
|
|
128
121
|
status: 'degraded',
|
|
129
122
|
code: failure.code,
|
|
130
123
|
reason,
|
|
131
|
-
contextDurationMs,
|
|
132
124
|
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
133
125
|
durationMs: Date.now() - startedAt,
|
|
134
126
|
},
|
|
@@ -151,108 +143,12 @@ export async function runUserPrompt({
|
|
|
151
143
|
}
|
|
152
144
|
};
|
|
153
145
|
|
|
154
|
-
const syncUserInputWithoutAudit = async (contextStatus) => {
|
|
155
|
-
try {
|
|
156
|
-
const response = await sendDaemonPayload({
|
|
157
|
-
user_input: prompt,
|
|
158
|
-
observation_id: observationId,
|
|
159
|
-
audit: false,
|
|
160
|
-
context_status: contextStatus,
|
|
161
|
-
});
|
|
162
|
-
if (response.ok !== true) {
|
|
163
|
-
await degrade({ code: response.error?.code ?? 'E_INTERNAL', reason: 'daemon_state_sync' });
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
return true;
|
|
167
|
-
} catch (err) {
|
|
168
|
-
await degrade({ code: err?.code, reason: 'daemon_state_sync' });
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
let context;
|
|
174
|
-
let auditorContextConfig;
|
|
175
|
-
const contextStartedAt = Date.now();
|
|
176
|
-
try {
|
|
177
|
-
const config = await readAuditorContextConfigFn(projectRoot);
|
|
178
|
-
auditorContextConfig = config;
|
|
179
|
-
contextDurationMs = Date.now() - contextStartedAt;
|
|
180
|
-
if (config.mode === 'disabled') {
|
|
181
|
-
if (!await syncUserInputWithoutAudit('disabled')) {
|
|
182
|
-
await safelyRecordEvaluation({ auditStatus: 'error' });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
await safelyRecordEvaluation({ auditStatus: 'skipped' });
|
|
186
|
-
await recordHookEventFn({
|
|
187
|
-
projectRoot,
|
|
188
|
-
event: {
|
|
189
|
-
hook: 'UserPromptSubmit',
|
|
190
|
-
status: 'skipped',
|
|
191
|
-
reason: 'context_disabled',
|
|
192
|
-
contextStatus: 'disabled',
|
|
193
|
-
contextDurationMs,
|
|
194
|
-
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
195
|
-
durationMs: Date.now() - startedAt,
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
context = await loadAuditorContextFn({
|
|
201
|
-
config,
|
|
202
|
-
host: 'claude',
|
|
203
|
-
sessionId,
|
|
204
|
-
projectRoot,
|
|
205
|
-
transcriptPath: requireString(input, 'transcript_path'),
|
|
206
|
-
});
|
|
207
|
-
contextDurationMs = Date.now() - contextStartedAt;
|
|
208
|
-
} catch (err) {
|
|
209
|
-
contextDurationMs = Date.now() - contextStartedAt;
|
|
210
|
-
await degrade({ code: err?.code, reason: 'auditor_context' });
|
|
211
|
-
await safelyRecordEvaluation({ auditStatus: 'error' });
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (context.status === 'unavailable' || context.status === 'schema_mismatch') {
|
|
216
|
-
await degrade({
|
|
217
|
-
code: context.status === 'unavailable'
|
|
218
|
-
? 'E_AUDITOR_CONTEXT_UNAVAILABLE'
|
|
219
|
-
: 'E_AUDITOR_CONTEXT_SCHEMA',
|
|
220
|
-
reason: 'auditor_context_status',
|
|
221
|
-
});
|
|
222
|
-
await safelyRecordEvaluation({ auditStatus: 'error' });
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (context.status !== 'fresh') {
|
|
226
|
-
if (!await syncUserInputWithoutAudit(context.status)) {
|
|
227
|
-
await safelyRecordEvaluation({ auditStatus: 'error' });
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
await safelyRecordEvaluation({ auditStatus: 'skipped' });
|
|
231
|
-
await recordHookEventFn({
|
|
232
|
-
projectRoot,
|
|
233
|
-
event: {
|
|
234
|
-
hook: 'UserPromptSubmit',
|
|
235
|
-
status: 'skipped',
|
|
236
|
-
reason: 'context_not_fresh',
|
|
237
|
-
contextStatus: context.status,
|
|
238
|
-
contextTurns: 0,
|
|
239
|
-
contextChars: 0,
|
|
240
|
-
contextDurationMs,
|
|
241
|
-
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
242
|
-
durationMs: Date.now() - startedAt,
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
146
|
let response;
|
|
249
147
|
try {
|
|
250
148
|
response = await sendDaemonPayload({
|
|
251
149
|
user_input: prompt,
|
|
252
150
|
observation_id: observationId,
|
|
253
151
|
audit: true,
|
|
254
|
-
context_status: 'fresh',
|
|
255
|
-
recent_context: context.turns,
|
|
256
152
|
});
|
|
257
153
|
} catch (err) {
|
|
258
154
|
await degrade({ code: err?.code ?? 'E_RESURRECT_FAILED', reason: 'transport_or_resurrect' });
|
|
@@ -278,7 +174,6 @@ export async function runUserPrompt({
|
|
|
278
174
|
await safelyRecordEvaluation({
|
|
279
175
|
auditStatus: 'success',
|
|
280
176
|
result,
|
|
281
|
-
auditContext: { config: auditorContextConfig, turns: context.turns },
|
|
282
177
|
});
|
|
283
178
|
if (advice) emitAdditionalContext(writeOutput, advice);
|
|
284
179
|
|
|
@@ -289,10 +184,6 @@ export async function runUserPrompt({
|
|
|
289
184
|
status: 'success',
|
|
290
185
|
pass: result.pass === true,
|
|
291
186
|
missingTools: toolIds,
|
|
292
|
-
contextStatus: 'fresh',
|
|
293
|
-
contextTurns: context.stats.returnedTurns,
|
|
294
|
-
contextChars: context.stats.chars,
|
|
295
|
-
contextDurationMs,
|
|
296
187
|
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
297
188
|
durationMs: Date.now() - startedAt,
|
|
298
189
|
},
|