claude-spotter 1.4.19 → 1.4.20
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 +10 -2
- package/README.ja.md +49 -1
- package/README.md +53 -1
- package/bin/spotter.mjs +59 -4
- package/package.json +1 -1
- package/src/cli/auditor-cmd.mjs +1 -1
- package/src/cli/auditor-model-matrix-cmd.mjs +15 -8
- package/src/cli/codex-hook-cmd.mjs +98 -4
- package/src/cli/doctor.mjs +42 -1
- package/src/cli/install.mjs +15 -0
- package/src/core/auditor-backend.mjs +7 -0
- package/src/core/auditor-context.mjs +198 -0
- package/src/core/auditor-prompt-data.mjs +38 -0
- package/src/core/auditor-response.mjs +7 -14
- package/src/core/codex-cli-backend.mjs +35 -17
- package/src/core/codex-risk-dispatch.mjs +9 -1
- package/src/core/codex-sidecar-runner.mjs +14 -1
- package/src/daemon/daemon.mjs +26 -2
- package/src/daemon/haiku-caller.mjs +11 -3
- package/src/hooks/user-prompt.mjs +105 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.20 — 2026-07-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Throughline監査文脈のopt-in配布。** 新規projectは既定`disabled`のまま、ownerがabsoluteなThroughline commandとrepeatableな先頭引数で有効化できる。freshな完了L2 user/assistant pairだけを`N=2`、body 600文字、total 4,000文字に制限し、Codex CLIへstdinで渡す。Haikuはこのcontext経路では呼ばず、fresh以外でもAIを呼ばない。connector障害は固定警告に限定する。親出力は安全なcatalog tool ID由来の固定・非命令形助言だけで、L2、reason、provider rawを反射しない。7日・30 fresh resultのproduction default昇格gateは未完了のため、配布後もproject opt-inを維持する。
|
|
8
|
+
|
|
3
9
|
## 1.4.19
|
|
4
10
|
|
|
5
11
|
親セッションの暴走を誘発できたHook出力の信頼境界を修正する。監査用AIは内部で構造化判定を返すだけとし、
|
|
@@ -29,8 +35,10 @@ AI/backend/provider sentinel、unsafe ID、legacy pending非読取unlink、Stop
|
|
|
29
35
|
読込例外の固定degradationを確認した。敵対的再監査は初回BLOCKER 2件(Codex読込例外の境界外、
|
|
30
36
|
UI可視性の過剰主張)を検出・修正し、再監査BLOCKER 0。`npm pack --dry-run`は62 files、global
|
|
31
37
|
`spotter 1.4.19`へlocal installし、projector smoke・Hook diagnostics・global UserPromptSubmit smokeを
|
|
32
|
-
確認した。公開前release gate
|
|
33
|
-
|
|
38
|
+
確認した。公開前release gateでfull test、pack、秘密混入を再検証し、公開SHA`5393919`のCIは
|
|
39
|
+
macOS/Linux/Windows × Node 22.5/22.xの6/6 green。`v1.4.19` tag、npm `latest`、
|
|
40
|
+
[GitHub Release](https://github.com/kitepon-rgb/Spotter/releases/tag/v1.4.19)、registry由来global installを
|
|
41
|
+
1.4.19へ揃え、global projector smokeとHook diagnosticsもgreen。
|
|
34
42
|
|
|
35
43
|
## 1.4.18
|
|
36
44
|
|
package/README.ja.md
CHANGED
|
@@ -146,6 +146,54 @@ flowchart LR
|
|
|
146
146
|
|
|
147
147
|
両者に共通するのは **「主体に頼らない仕組み」**。併用できます。
|
|
148
148
|
|
|
149
|
+
### 任意の Throughline auditor context canary
|
|
150
|
+
|
|
151
|
+
新規の project install では auditor context は **disabled** が既定です。これは project が所有する
|
|
152
|
+
opt-in であり、Spotter 全体の production 既定ではありません。現在の候補は Spotter リポジトリだけの
|
|
153
|
+
canary です。auditor-context オプションなしで `spotter install` を再実行した場合、既存プロジェクトの
|
|
154
|
+
設定は保持されます。無効化へ戻すには次を実行します。
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
spotter install -y --auditor-context disabled
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
project を opt-in するには、絶対パスの Throughline 実行ファイルと、必要なら繰り返し指定できる
|
|
161
|
+
`--throughline-arg` を設定します。
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
spotter install -y --auditor-context throughline \
|
|
165
|
+
--throughline-command /absolute/path/to/throughline
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Windows では shell injection を避けるため `.cmd` / `.bat` wrapper を意図的に拒否します。絶対パスの
|
|
169
|
+
`node.exe` を command にし、絶対パスの `throughline.mjs` を繰り返し引数として渡してください。
|
|
170
|
+
|
|
171
|
+
```powershell
|
|
172
|
+
spotter install -y --auditor-context throughline `
|
|
173
|
+
--throughline-command 'C:\Program Files\nodejs\node.exe' `
|
|
174
|
+
--throughline-arg 'C:\absolute\path\to\throughline\bin\throughline.mjs'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
この connector の AI 呼出しは Codex CLI のみです。コンテキストを argv には載せず、AI へは stdin 経由で
|
|
178
|
+
渡します。Haiku はこの context path に未対応であり、呼び出しません。Throughline 結果が `fresh` の時だけ
|
|
179
|
+
AI 呼出し候補になり、それ以外の status では AI を呼びません。enabled connector の障害は hidden fallback
|
|
180
|
+
ではなく固定 warning として出します。
|
|
181
|
+
|
|
182
|
+
Throughline から渡すのは fresh な完了済み L2 user/assistant pair だけです。直近 2 pair (N=2)、各 body は
|
|
183
|
+
600 文字、合計は 4,000 文字に制限します。Spotter は Throughline の L2、`reason`、`raw` を親へ反射せず、
|
|
184
|
+
親には安全な catalog tool ID から作る固定・非命令形の助言だけを渡します。`spotter doctor` は command / args
|
|
185
|
+
を表示せず、auditor-context mode と固定の availability detail だけを表示します。
|
|
186
|
+
|
|
187
|
+
v2 model-matrix では context choice を明示できます。
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.v2.json \
|
|
191
|
+
--recent-turns 2 --body-cap 600
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
評価結論は現在 N=2 / 600 です。ただし rollout 承認ではありません。Spotter リポジトリ canary の
|
|
195
|
+
7 日・30 fresh result gate は未完了です。
|
|
196
|
+
|
|
149
197
|
## よく使うコマンド
|
|
150
198
|
|
|
151
199
|
```bash
|
|
@@ -172,7 +220,7 @@ spotter codex-hook install
|
|
|
172
220
|
# Codex native hooks の修復 / 明示登録 (通常は spotter install が実行)
|
|
173
221
|
spotter codex-hook diagnostics
|
|
174
222
|
# Codex hook の登録/readiness を診断。trust は /hooks で review
|
|
175
|
-
spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.
|
|
223
|
+
spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.v2.json --recent-turns 2 --body-cap 600
|
|
176
224
|
# pinned auditor model profile を再現可能に比較する experimental eval
|
|
177
225
|
spotter uninstall # hook 登録を解除 (~/.spotter は残す)
|
|
178
226
|
```
|
package/README.md
CHANGED
|
@@ -146,6 +146,58 @@ The audited catalog is host-local: Claude uses `<project>/.spotter/tool-db.json`
|
|
|
146
146
|
|
|
147
147
|
Both share the principle of **"don't rely on the primary agent to do it itself."** They compose well — you can run them together.
|
|
148
148
|
|
|
149
|
+
### Optional Throughline auditor context canary
|
|
150
|
+
|
|
151
|
+
Fresh project installs keep auditor context **disabled**. This is a project-owned
|
|
152
|
+
opt-in, not a production-wide default: the current candidate is a canary in the
|
|
153
|
+
Spotter repository only. Re-running `spotter install` without auditor-context
|
|
154
|
+
options preserves the project's existing setting. Disable it again with:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
spotter install -y --auditor-context disabled
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
To opt a project in, configure a direct absolute Throughline executable and any
|
|
161
|
+
leading arguments with repeatable `--throughline-arg`:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
spotter install -y --auditor-context throughline \
|
|
165
|
+
--throughline-command /absolute/path/to/throughline
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
On Windows, `.cmd` and `.bat` wrappers are deliberately rejected to avoid shell
|
|
169
|
+
injection. Point at an absolute `node.exe` and pass the absolute
|
|
170
|
+
`throughline.mjs` path as a repeated argument instead:
|
|
171
|
+
|
|
172
|
+
```powershell
|
|
173
|
+
spotter install -y --auditor-context throughline `
|
|
174
|
+
--throughline-command 'C:\Program Files\nodejs\node.exe' `
|
|
175
|
+
--throughline-arg 'C:\absolute\path\to\throughline\bin\throughline.mjs'
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The connector is Codex CLI-only. It sends no context in argv: the bounded
|
|
179
|
+
projection is supplied to that AI over stdin. Haiku does not support this
|
|
180
|
+
context path and is never called for it. Only a `fresh` Throughline result is
|
|
181
|
+
eligible for an AI call; every other status skips AI. An enabled connector
|
|
182
|
+
failure becomes a fixed warning, not a hidden fallback.
|
|
183
|
+
|
|
184
|
+
Throughline contributes only fresh, completed L2 user/assistant pairs: two
|
|
185
|
+
recent pairs (N=2), each body capped at 600 characters and 4,000 characters in
|
|
186
|
+
total. Spotter never reflects Throughline L2, `reason`, or `raw` to the parent.
|
|
187
|
+
The parent receives only fixed non-imperative advice built from safe catalog tool
|
|
188
|
+
IDs. `spotter doctor` displays the auditor-context mode and a fixed availability
|
|
189
|
+
detail without printing its command or arguments.
|
|
190
|
+
|
|
191
|
+
The v2 model-matrix can make the context choice explicit:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.v2.json \
|
|
195
|
+
--recent-turns 2 --body-cap 600
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Current evaluation conclusion is N=2 / 600. It is not a rollout approval: the
|
|
199
|
+
Spotter-repository canary still lacks its 7-day and 30-fresh-result gates.
|
|
200
|
+
|
|
149
201
|
## Common commands
|
|
150
202
|
|
|
151
203
|
```bash
|
|
@@ -173,7 +225,7 @@ spotter codex-hook install
|
|
|
173
225
|
# repair / explicitly register Codex native hooks (normally handled by spotter install)
|
|
174
226
|
spotter codex-hook diagnostics
|
|
175
227
|
# check Codex hook registration/readiness; trust is reviewed with /hooks
|
|
176
|
-
spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.
|
|
228
|
+
spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.v2.json --recent-turns 2 --body-cap 600
|
|
177
229
|
# experimental reproducible comparison of pinned auditor model profiles
|
|
178
230
|
spotter uninstall # remove hooks from this project (leaves ~/.spotter intact)
|
|
179
231
|
```
|
package/bin/spotter.mjs
CHANGED
|
@@ -21,7 +21,9 @@ import { runSessionEnd } from '../src/hooks/session-end.mjs';
|
|
|
21
21
|
const USAGE = `spotter — Claude Code tool-call auditor
|
|
22
22
|
|
|
23
23
|
Usage:
|
|
24
|
-
spotter install [-y]
|
|
24
|
+
spotter install [-y] [--auditor-context disabled|throughline]
|
|
25
|
+
[--throughline-command ABS] [--throughline-arg VALUE]
|
|
26
|
+
register hooks in <cwd>/.claude/settings.json
|
|
25
27
|
and create <cwd>/.spotter/marker.json
|
|
26
28
|
(run inside each project you want audited)
|
|
27
29
|
spotter install --user [-y] legacy: register globally in ~/.claude/settings.json
|
|
@@ -75,9 +77,8 @@ async function main() {
|
|
|
75
77
|
const [cmd, ...rest] = argv;
|
|
76
78
|
switch (cmd) {
|
|
77
79
|
case 'install': {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
await runInstall({ target, autoYes });
|
|
80
|
+
const options = parseInstallArgs(rest);
|
|
81
|
+
await runInstall(options);
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
83
84
|
case 'uninstall': {
|
|
@@ -140,6 +141,60 @@ async function main() {
|
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
function parseInstallArgs(argv) {
|
|
145
|
+
let target = 'project';
|
|
146
|
+
let autoYes = false;
|
|
147
|
+
let mode = null;
|
|
148
|
+
let command = null;
|
|
149
|
+
const args = [];
|
|
150
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
151
|
+
const arg = argv[index];
|
|
152
|
+
if (arg === '--user') {
|
|
153
|
+
if (target === 'user') throw invalidInstallArgs();
|
|
154
|
+
target = 'user';
|
|
155
|
+
} else if (arg === '-y' || arg === '--yes') {
|
|
156
|
+
if (autoYes) throw invalidInstallArgs();
|
|
157
|
+
autoYes = true;
|
|
158
|
+
} else if (arg === '--auditor-context') {
|
|
159
|
+
if (mode !== null) throw invalidInstallArgs();
|
|
160
|
+
mode = argv[++index];
|
|
161
|
+
if (mode !== 'disabled' && mode !== 'throughline') throw invalidInstallArgs();
|
|
162
|
+
} else if (arg === '--throughline-command') {
|
|
163
|
+
if (command !== null) throw invalidInstallArgs();
|
|
164
|
+
command = argv[++index];
|
|
165
|
+
if (typeof command !== 'string' || command.length === 0) throw invalidInstallArgs();
|
|
166
|
+
} else if (arg === '--throughline-arg') {
|
|
167
|
+
const value = argv[++index];
|
|
168
|
+
if (typeof value !== 'string' || value.length === 0) throw invalidInstallArgs();
|
|
169
|
+
args.push(value);
|
|
170
|
+
} else {
|
|
171
|
+
throw invalidInstallArgs();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (mode === null && (command !== null || args.length > 0)) throw invalidInstallArgs();
|
|
175
|
+
if (mode === 'disabled' && (command !== null || args.length > 0)) throw invalidInstallArgs();
|
|
176
|
+
if (mode === 'throughline' && (command === null || !isAbsoluteCommand(command) || isShellWrapper(command))) throw invalidInstallArgs();
|
|
177
|
+
const auditorContext = mode === null ? undefined : mode === 'disabled'
|
|
178
|
+
? { mode: 'disabled' }
|
|
179
|
+
: { mode: 'throughline', command, args };
|
|
180
|
+
return { target, autoYes, auditorContext };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isAbsoluteCommand(value) {
|
|
184
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isShellWrapper(value) {
|
|
188
|
+
return /\.(?:cmd|bat)$/i.test(value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function invalidInstallArgs() {
|
|
192
|
+
const err = new Error('invalid install arguments');
|
|
193
|
+
err.stack = '';
|
|
194
|
+
err.exitCode = 2;
|
|
195
|
+
return err;
|
|
196
|
+
}
|
|
197
|
+
|
|
143
198
|
main().catch((err) => {
|
|
144
199
|
process.stderr.write(`spotter: ${err.stack || err.message || err}\n`);
|
|
145
200
|
process.exit(err.exitCode ?? 2);
|
package/package.json
CHANGED
package/src/cli/auditor-cmd.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export async function runAuditorCommand({ argv = process.argv.slice(2) } = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
if (sub === 'model-matrix') {
|
|
39
39
|
if (argv.slice(1).includes('--help') || argv.slice(1).includes('-h')) {
|
|
40
|
-
process.stdout.write(`Usage: spotter auditor model-matrix --fixtures FILE [--profile baseline|luna|terra|terra-medium]...\n [--repeat N] [--project DIR] [--output FILE]\n`);
|
|
40
|
+
process.stdout.write(`Usage: spotter auditor model-matrix --fixtures FILE [--profile baseline|luna|terra|terra-medium]...\n [--repeat N] [--recent-turns 0|1|2|3] [--body-cap CHARS] [--project DIR] [--output FILE]\n`);
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
await runAuditorModelMatrixCommand({ argv: argv.slice(1) });
|
|
@@ -4,7 +4,7 @@ import { promisify } from 'node:util';
|
|
|
4
4
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { version } from '../version.mjs';
|
|
7
|
-
import { createCodexCliAuditorBackend } from '../core/codex-cli-backend.mjs';
|
|
7
|
+
import { CODEX_AUDITOR_PROMPT_VERSION, createCodexCliAuditorBackend } from '../core/codex-cli-backend.mjs';
|
|
8
8
|
import { CODEX_AUDITOR_MODEL_POLICY, resolveCodexAuditorModelSelection } from '../core/codex-auditor-model-policy.mjs';
|
|
9
9
|
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
@@ -21,6 +21,7 @@ export async function runAuditorModelMatrixCommand({
|
|
|
21
21
|
const raw = await readFileFn(opts.fixturesPath);
|
|
22
22
|
const fixtureBytes = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
23
23
|
const fixture = parseAndValidateFixture(fixtureBytes.toString('utf8'));
|
|
24
|
+
const contextOptions = resolveContextOptions(opts, fixture.schema);
|
|
24
25
|
if (fixture.cases.length * opts.repeat * opts.profiles.length > MAX_MODEL_MATRIX_RUNS) {
|
|
25
26
|
throw new Error(`model-matrix run count exceeds maximum ${MAX_MODEL_MATRIX_RUNS}`);
|
|
26
27
|
}
|
|
@@ -40,7 +41,7 @@ export async function runAuditorModelMatrixCommand({
|
|
|
40
41
|
const modelSelection = selections[profile];
|
|
41
42
|
try {
|
|
42
43
|
const backend = backends[profile];
|
|
43
|
-
const judgment = await backend.judge({ ...toAuditorInput(item), meta: { caseId: item.id, repeat, profile } });
|
|
44
|
+
const judgment = await backend.judge({ ...toAuditorInput(item, contextOptions), meta: { caseId: item.id, repeat, profile } });
|
|
44
45
|
if (!sameSelection(modelSelection, judgment?.meta?.modelSelection)) {
|
|
45
46
|
throw new Error('judgment model selection does not match backend model selection');
|
|
46
47
|
}
|
|
@@ -87,6 +88,7 @@ export async function runAuditorModelMatrixCommand({
|
|
|
87
88
|
schema: 'spotter.auditor_model_matrix.v1', generatedAt: generatedAt(), packageVersion: version,
|
|
88
89
|
fixture: { schema: fixture.schema, path: safeFixturePath(opts.fixturesPath, opts.projectRoot), sha256: createHash('sha256').update(fixtureBytes).digest('hex'), cases: fixture.cases.length, catalogCount: fixture.catalog.length },
|
|
89
90
|
codexCli: cliVersion, policy: { schema: CODEX_AUDITOR_MODEL_POLICY.schema, version: CODEX_AUDITOR_MODEL_POLICY.policyVersion },
|
|
91
|
+
auditorPromptVersion: CODEX_AUDITOR_PROMPT_VERSION,
|
|
90
92
|
profiles: Object.fromEntries(Object.entries(selections).map(([profile, selection]) => [profile, {
|
|
91
93
|
model: selection.effectiveModel,
|
|
92
94
|
reasoningEffort: selection.effectiveReasoningEffort,
|
|
@@ -96,7 +98,7 @@ export async function runAuditorModelMatrixCommand({
|
|
|
96
98
|
selection,
|
|
97
99
|
}])), runs, summary: summarize(runs, opts.profiles), usageStatus: usage.status, tokenUsage: usage.summary,
|
|
98
100
|
costStatus: 'not-available-chatgpt-plan', cost: null,
|
|
99
|
-
evaluation: { repeat: opts.repeat, profiles: opts.profiles, maxRuns: MAX_MODEL_MATRIX_RUNS },
|
|
101
|
+
evaluation: { repeat: opts.repeat, profiles: opts.profiles, maxRuns: MAX_MODEL_MATRIX_RUNS, recentTurns: contextOptions.recentTurns, bodyCap: contextOptions.bodyCap },
|
|
100
102
|
executionOrdering: 'case-repeat-profile', promotionEligible: false, blockingReasons: blockingReasons(runs, usage.status),
|
|
101
103
|
};
|
|
102
104
|
const json = JSON.stringify(artifact, null, 2) + '\n';
|
|
@@ -106,7 +108,7 @@ export async function runAuditorModelMatrixCommand({
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
function parseArgs(argv) {
|
|
109
|
-
const opts = { fixturesPath: null, profiles: [], repeat: 1, projectRoot: process.cwd(), outputPath: null };
|
|
111
|
+
const opts = { fixturesPath: null, profiles: [], repeat: 1, projectRoot: process.cwd(), outputPath: null, recentTurns: null, bodyCap: null };
|
|
110
112
|
for (let i = 0; i < argv.length; i += 1) {
|
|
111
113
|
const arg = argv[i]; const value = () => { const v = argv[++i]; if (!v || v.startsWith('--')) throw new Error(`${arg} requires a value`); return v; };
|
|
112
114
|
if (arg === '--fixtures') opts.fixturesPath = resolve(value());
|
|
@@ -114,10 +116,14 @@ function parseArgs(argv) {
|
|
|
114
116
|
else if (arg === '--repeat') opts.repeat = Number(value());
|
|
115
117
|
else if (arg === '--project') opts.projectRoot = resolve(value());
|
|
116
118
|
else if (arg === '--output') opts.outputPath = resolve(value());
|
|
119
|
+
else if (arg === '--recent-turns') opts.recentTurns = Number(value());
|
|
120
|
+
else if (arg === '--body-cap') opts.bodyCap = Number(value());
|
|
117
121
|
else throw new Error(`unknown auditor model-matrix option: ${arg}`);
|
|
118
122
|
}
|
|
119
123
|
if (!opts.fixturesPath) throw new Error('--fixtures FILE is required');
|
|
120
124
|
if (!Number.isInteger(opts.repeat) || opts.repeat < 1) throw new Error('--repeat must be a positive integer');
|
|
125
|
+
if (opts.recentTurns !== null && (!Number.isInteger(opts.recentTurns) || opts.recentTurns < 0 || opts.recentTurns > 3)) throw new Error('--recent-turns must be 0, 1, 2, or 3');
|
|
126
|
+
if (opts.bodyCap !== null && (!Number.isInteger(opts.bodyCap) || opts.bodyCap <= 0)) throw new Error('--body-cap must be a positive integer');
|
|
121
127
|
opts.profiles = opts.profiles.length ? opts.profiles : [...DEFAULT_PROFILES];
|
|
122
128
|
if (new Set(opts.profiles).size !== opts.profiles.length || opts.profiles.some((profile) => !ALLOWED_PROFILES.includes(profile))) throw new Error('--profile must be baseline, luna, terra, or terra-medium without duplicates');
|
|
123
129
|
return opts;
|
|
@@ -126,7 +132,7 @@ function parseArgs(argv) {
|
|
|
126
132
|
function parseAndValidateFixture(raw) {
|
|
127
133
|
let fixture; try { fixture = JSON.parse(raw); } catch { throw new Error('fixture must be valid JSON'); }
|
|
128
134
|
objectOnly(fixture, ['schema', 'catalog', 'cases'], 'fixture');
|
|
129
|
-
if (
|
|
135
|
+
if (!['spotter.auditor_model_fixtures.v1', 'spotter.auditor_model_fixtures.v2'].includes(fixture.schema)) throw new Error('unsupported fixture schema');
|
|
130
136
|
if (!Array.isArray(fixture.catalog) || !Array.isArray(fixture.cases) || fixture.cases.length === 0) throw new Error('fixture catalog and non-empty cases are required');
|
|
131
137
|
const catalogNames = new Set();
|
|
132
138
|
for (const tool of fixture.catalog) { objectOnly(tool, ['name', 'description'], 'catalog tool'); clean(tool.name, 'catalog name'); clean(tool.description, 'catalog description'); if (catalogNames.has(tool.name)) throw new Error('duplicate catalog name'); catalogNames.add(tool.name); }
|
|
@@ -135,7 +141,7 @@ function parseAndValidateFixture(raw) {
|
|
|
135
141
|
objectOnly(item, ['id', 'stage', 'input', 'expected'], 'case'); clean(item.id, 'case id');
|
|
136
142
|
if (ids.has(item.id)) throw new Error('duplicate case id'); ids.add(item.id);
|
|
137
143
|
if (!['user_input', 'turn_end'].includes(item.stage)) throw new Error('case stage is invalid');
|
|
138
|
-
validateInput(item.stage, item.input); objectOnly(item.expected, ['pass', 'missingTools'], 'case expected');
|
|
144
|
+
validateInput(fixture.schema, item.stage, item.input); objectOnly(item.expected, ['pass', 'missingTools'], 'case expected');
|
|
139
145
|
if (typeof item.expected.pass !== 'boolean' || !Array.isArray(item.expected.missingTools)
|
|
140
146
|
|| new Set(item.expected.missingTools).size !== item.expected.missingTools.length
|
|
141
147
|
|| item.expected.missingTools.some((name) => typeof name !== 'string' || !catalogNames.has(name))
|
|
@@ -145,8 +151,9 @@ function parseAndValidateFixture(raw) {
|
|
|
145
151
|
}
|
|
146
152
|
function objectOnly(value, keys, label) { if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some((key) => !keys.includes(key))) throw new Error(`${label} has invalid fields`); }
|
|
147
153
|
function clean(value, label) { if (typeof value !== 'string' || value.length === 0 || value.trim() !== value) throw new Error(`${label} must be a clean non-empty string`); }
|
|
148
|
-
function validateInput(stage, input) { if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('case input is invalid'); if (stage === 'user_input') { objectOnly(input, ['userInput'], 'user_input input'); clean(input.userInput, 'userInput'); } else { objectOnly(input, ['finalResponse', 'usedTools'], 'turn_end input'); clean(input.finalResponse, 'finalResponse'); if (!Array.isArray(input.usedTools) || input.usedTools.some((tool) => typeof tool !== 'string' || tool.length === 0 || tool.trim() !== tool) || new Set(input.usedTools).size !== input.usedTools.length) throw new Error('usedTools is invalid'); } }
|
|
149
|
-
function
|
|
154
|
+
function validateInput(schema, stage, input) { if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('case input is invalid'); if (stage === 'user_input') { objectOnly(input, schema === 'spotter.auditor_model_fixtures.v2' ? ['userInput', 'recentContext'] : ['userInput'], 'user_input input'); clean(input.userInput, 'userInput'); if (schema === 'spotter.auditor_model_fixtures.v2') { if (!Array.isArray(input.recentContext) || input.recentContext.length < 1 || input.recentContext.length > 3) throw new Error('recentContext must contain one to three turns'); for (const turn of input.recentContext) { objectOnly(turn, ['user', 'assistant'], 'recentContext turn'); clean(turn.user, 'recentContext user'); clean(turn.assistant, 'recentContext assistant'); } } } else { objectOnly(input, ['finalResponse', 'usedTools'], 'turn_end input'); clean(input.finalResponse, 'finalResponse'); if (!Array.isArray(input.usedTools) || input.usedTools.some((tool) => typeof tool !== 'string' || tool.length === 0 || tool.trim() !== tool) || new Set(input.usedTools).size !== input.usedTools.length) throw new Error('usedTools is invalid'); } }
|
|
155
|
+
function resolveContextOptions(opts, schema) { if (schema === 'spotter.auditor_model_fixtures.v1') { if (opts.recentTurns !== null || opts.bodyCap !== null) throw new Error('--recent-turns and --body-cap require a v2 fixture'); return { recentTurns: null, bodyCap: null }; } return { recentTurns: opts.recentTurns ?? 2, bodyCap: opts.bodyCap ?? 1200 }; }
|
|
156
|
+
function toAuditorInput(item, contextOptions) { if (item.stage !== 'user_input') return { stage: item.stage, finalResponse: item.input.finalResponse, usedTools: item.input.usedTools }; const input = { stage: item.stage, userInput: item.input.userInput }; if (contextOptions.recentTurns > 0 && item.input.recentContext) input.recentContext = item.input.recentContext.slice(-contextOptions.recentTurns).map((turn) => ({ user: turn.user.slice(-contextOptions.bodyCap), assistant: turn.assistant.slice(-contextOptions.bodyCap) })); return input; }
|
|
150
157
|
function isCleanString(value) { return typeof value === 'string' && value.length > 0 && value.trim() === value; }
|
|
151
158
|
function cleanStrings(values = []) { return [...new Set(values.filter(isCleanString))]; }
|
|
152
159
|
function successRun({ order, item, repeat, profile, modelSelection, durationMs, schemaSuccess, actualPass, actualTools, invalidFindingCount, droppedTools, droppedToolCount, anomalyTypes, anomalyCount, tokenUsage }) { const expectedTools = item.expected.missingTools; const fp = [...actualTools.filter((tool) => !expectedTools.includes(tool)), ...droppedTools.filter((tool) => !expectedTools.includes(tool))]; const fn = expectedTools.filter((tool) => !actualTools.includes(tool)); return { order, caseId: item.id, repeat, profile, status: 'success', durationMs, schemaSuccess, exactMatch: schemaSuccess && actualPass === item.expected.pass && actualTools.length === expectedTools.length && fp.length === 0 && fn.length === 0 && droppedToolCount === 0 && anomalyCount === 0, expected: item.expected, actual: { pass: actualPass, missingTools: actualTools, invalidFindingCount, droppedCatalogExternalNames: droppedTools, droppedCatalogExternalNameCount: droppedToolCount, anomalies: anomalyTypes, anomalyCount }, falsePositiveTools: fp, falseNegativeTools: fn, modelSelection, tokenUsage }; }
|
|
@@ -28,13 +28,16 @@ import {
|
|
|
28
28
|
hookEventsPath,
|
|
29
29
|
summarizeHookEvents,
|
|
30
30
|
} from '../core/hook-event-log.mjs';
|
|
31
|
+
import {
|
|
32
|
+
loadAuditorContext,
|
|
33
|
+
readProjectAuditorContextConfig,
|
|
34
|
+
} from '../core/auditor-context.mjs';
|
|
31
35
|
|
|
32
36
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
33
37
|
const PACKAGE_ROOT = resolve(HERE, '..', '..');
|
|
34
38
|
const SPOTTER_BIN = join(PACKAGE_ROOT, 'bin', 'spotter.mjs');
|
|
35
39
|
const CODEX_HOOK_TIMEOUT_SEC = 60;
|
|
36
40
|
const DEFAULT_CODEX_HOOK_AUDITOR_TIMEOUT_MS = 20_000;
|
|
37
|
-
const SHORT_PROMPT_MAX_CHARS = 10;
|
|
38
41
|
const DEFAULT_CODEX_STOP_SHORT_FINAL_MAX_CHARS = 120;
|
|
39
42
|
const CODEX_HOOK_FEATURE_NAMES = ['hooks', 'codex_hooks'];
|
|
40
43
|
|
|
@@ -114,6 +117,8 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
114
117
|
createAuditorBackendFn = createAuditorBackend,
|
|
115
118
|
recordHookEventFn = appendCodexHookEvent,
|
|
116
119
|
discardLegacyPendingFn = discardLegacyPending,
|
|
120
|
+
readAuditorContextConfigFn = readProjectAuditorContextConfig,
|
|
121
|
+
loadAuditorContextFn = loadAuditorContext,
|
|
117
122
|
writeOutput = (text) => process.stdout.write(text),
|
|
118
123
|
writeError = (text) => process.stderr.write(text),
|
|
119
124
|
} = {}) {
|
|
@@ -122,16 +127,88 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
122
127
|
const projectRoot = findSpotterMarker(input.cwd);
|
|
123
128
|
if (!projectRoot) return;
|
|
124
129
|
const startedAt = Date.now();
|
|
130
|
+
let contextDurationMs = null;
|
|
125
131
|
|
|
126
132
|
const prompt = requireString(input, 'prompt');
|
|
127
133
|
const legacyPending = await discardLegacyPendingFn({ projectRoot, sessionId: codexSessionId(input) });
|
|
128
|
-
|
|
134
|
+
let context;
|
|
135
|
+
const contextStartedAt = Date.now();
|
|
136
|
+
try {
|
|
137
|
+
const config = await readAuditorContextConfigFn(projectRoot);
|
|
138
|
+
contextDurationMs = Date.now() - contextStartedAt;
|
|
139
|
+
if (config.mode === 'disabled') {
|
|
140
|
+
await recordCodexHookEventSafe(recordHookEventFn, {
|
|
141
|
+
projectRoot,
|
|
142
|
+
event: {
|
|
143
|
+
hook: 'UserPromptSubmit',
|
|
144
|
+
status: 'skipped',
|
|
145
|
+
reason: 'context_disabled',
|
|
146
|
+
contextStatus: 'disabled',
|
|
147
|
+
contextDurationMs,
|
|
148
|
+
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
149
|
+
durationMs: Date.now() - startedAt,
|
|
150
|
+
},
|
|
151
|
+
}, writeError);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
context = await loadAuditorContextFn({
|
|
155
|
+
config,
|
|
156
|
+
host: 'codex',
|
|
157
|
+
sessionId: requireCodexSessionId(input),
|
|
158
|
+
projectRoot,
|
|
159
|
+
transcriptPath: requireString(input, 'transcript_path'),
|
|
160
|
+
});
|
|
161
|
+
contextDurationMs = Date.now() - contextStartedAt;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
contextDurationMs = Date.now() - contextStartedAt;
|
|
164
|
+
const failure = projectBackendFailure(err?.code);
|
|
165
|
+
safeWriteError(writeError, failure.stderr);
|
|
166
|
+
await recordCodexHookEventSafe(recordHookEventFn, {
|
|
167
|
+
projectRoot,
|
|
168
|
+
event: {
|
|
169
|
+
hook: 'UserPromptSubmit',
|
|
170
|
+
status: 'error',
|
|
171
|
+
code: failure.code,
|
|
172
|
+
reason: 'auditor_context',
|
|
173
|
+
contextDurationMs,
|
|
174
|
+
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
175
|
+
durationMs: Date.now() - startedAt,
|
|
176
|
+
},
|
|
177
|
+
}, writeError);
|
|
178
|
+
writeCodexSystemMessage({ systemMessage: failure.systemMessage, writeOutput });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (context.status === 'unavailable' || context.status === 'schema_mismatch') {
|
|
182
|
+
const failure = projectBackendFailure(context.status === 'unavailable'
|
|
183
|
+
? 'E_AUDITOR_CONTEXT_UNAVAILABLE'
|
|
184
|
+
: 'E_AUDITOR_CONTEXT_SCHEMA');
|
|
185
|
+
safeWriteError(writeError, failure.stderr);
|
|
186
|
+
await recordCodexHookEventSafe(recordHookEventFn, {
|
|
187
|
+
projectRoot,
|
|
188
|
+
event: {
|
|
189
|
+
hook: 'UserPromptSubmit',
|
|
190
|
+
status: 'error',
|
|
191
|
+
code: failure.code,
|
|
192
|
+
reason: 'auditor_context_status',
|
|
193
|
+
contextDurationMs,
|
|
194
|
+
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
195
|
+
durationMs: Date.now() - startedAt,
|
|
196
|
+
},
|
|
197
|
+
}, writeError);
|
|
198
|
+
writeCodexSystemMessage({ systemMessage: failure.systemMessage, writeOutput });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (context.status !== 'fresh') {
|
|
129
202
|
await recordCodexHookEventSafe(recordHookEventFn, {
|
|
130
203
|
projectRoot,
|
|
131
204
|
event: {
|
|
132
205
|
hook: 'UserPromptSubmit',
|
|
133
206
|
status: 'skipped',
|
|
134
|
-
reason: '
|
|
207
|
+
reason: 'context_not_fresh',
|
|
208
|
+
contextStatus: context.status,
|
|
209
|
+
contextTurns: 0,
|
|
210
|
+
contextChars: 0,
|
|
211
|
+
contextDurationMs,
|
|
135
212
|
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
136
213
|
durationMs: Date.now() - startedAt,
|
|
137
214
|
},
|
|
@@ -145,7 +222,12 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
145
222
|
try {
|
|
146
223
|
catalog = await readLocalFn({ projectRoot, hostAgent: 'codex' });
|
|
147
224
|
backend = createCodexHookAuditorBackend({ catalog, projectRoot, createAuditorBackendFn });
|
|
148
|
-
judgment = await backend.judge({
|
|
225
|
+
judgment = await backend.judge({
|
|
226
|
+
stage: 'user_input',
|
|
227
|
+
userInput: prompt,
|
|
228
|
+
recentContext: context.turns,
|
|
229
|
+
contextStatus: 'fresh',
|
|
230
|
+
});
|
|
149
231
|
} catch (err) {
|
|
150
232
|
const failure = projectBackendFailure(err?.code);
|
|
151
233
|
safeWriteError(writeError, failure.stderr);
|
|
@@ -172,6 +254,10 @@ export async function runCodexUserPromptSubmitHook({
|
|
|
172
254
|
backend: judgment.meta?.backend ?? backend.name ?? 'unknown',
|
|
173
255
|
pass: judgment.pass,
|
|
174
256
|
missingTools: projectToolIds(judgment.findings.map((finding) => finding.toolName)),
|
|
257
|
+
contextStatus: 'fresh',
|
|
258
|
+
contextTurns: context.stats.returnedTurns,
|
|
259
|
+
contextChars: context.stats.chars,
|
|
260
|
+
contextDurationMs,
|
|
175
261
|
...compactCodexModelSelectionForEvent(judgment.meta?.modelSelection),
|
|
176
262
|
legacyPendingDiagnostic: legacyPending.diagnostic,
|
|
177
263
|
backendDurationMs: judgment.meta?.durationMs ?? null,
|
|
@@ -621,6 +707,14 @@ function codexSessionId(payload) {
|
|
|
621
707
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
622
708
|
}
|
|
623
709
|
|
|
710
|
+
function requireCodexSessionId(payload) {
|
|
711
|
+
const value = codexSessionId(payload);
|
|
712
|
+
if (value) return value;
|
|
713
|
+
const err = new Error('session_id is required');
|
|
714
|
+
err.code = 'E_AUDITOR_CONTEXT_INPUT';
|
|
715
|
+
throw err;
|
|
716
|
+
}
|
|
717
|
+
|
|
624
718
|
// v1.4.19: legacy pending migration is handled only by `discardLegacyPending` on
|
|
625
719
|
// UserPromptSubmit. Stop findings are never persisted for a future parent prompt.
|
|
626
720
|
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { access, readFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { isAbsolute, join } from 'node:path';
|
|
6
6
|
import { execFile } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import { loadDb, globalDbPath, localDbPath } from '../tool-db/loader.mjs';
|
|
@@ -75,6 +75,10 @@ export async function runDoctor() {
|
|
|
75
75
|
const sidecar = await codexSidecarAuditorReadiness(projectRoot);
|
|
76
76
|
mark(sidecar.ok, `codex-sidecar auditor: ${sidecar.status}`, sidecar.detail);
|
|
77
77
|
if (!sidecar.ok) warnings += 1;
|
|
78
|
+
|
|
79
|
+
const auditorContext = await inspectAuditorContextConfiguration({ projectRoot });
|
|
80
|
+
mark(auditorContext.ok, `auditor context: ${auditorContext.mode}`, auditorContext.detail);
|
|
81
|
+
if (!auditorContext.ok) warnings += 1;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
// tool-db (host-specific global caches). Since v1.2.0 these are not part of
|
|
@@ -132,6 +136,43 @@ export async function inspectCodexHookConfiguration({ projectRoot = null, diagno
|
|
|
132
136
|
};
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
export async function inspectAuditorContextConfiguration({
|
|
140
|
+
projectRoot,
|
|
141
|
+
readFileFn = readFile,
|
|
142
|
+
accessFn = access,
|
|
143
|
+
} = {}) {
|
|
144
|
+
const disabled = { ok: true, mode: 'disabled', detail: 'disabled' };
|
|
145
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) return disabled;
|
|
146
|
+
|
|
147
|
+
let marker;
|
|
148
|
+
try {
|
|
149
|
+
marker = JSON.parse(await readFileFn(join(projectRoot, '.spotter', 'marker.json'), 'utf8'));
|
|
150
|
+
} catch {
|
|
151
|
+
return { ok: false, mode: 'unknown', detail: 'marker unreadable' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const config = marker?.auditorContext;
|
|
155
|
+
if (config === undefined || config?.mode === 'disabled') return disabled;
|
|
156
|
+
if (config?.mode !== 'throughline') {
|
|
157
|
+
return { ok: false, mode: 'unknown', detail: 'invalid configuration' };
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
typeof config.command !== 'string' ||
|
|
161
|
+
!isAbsolute(config.command) ||
|
|
162
|
+
/\.(?:cmd|bat)$/i.test(config.command) ||
|
|
163
|
+
!Array.isArray(config.args) ||
|
|
164
|
+
config.args.some((arg) => typeof arg !== 'string' || arg.length === 0)
|
|
165
|
+
) {
|
|
166
|
+
return { ok: false, mode: 'throughline', detail: 'invalid configuration' };
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await accessFn(config.command);
|
|
170
|
+
} catch {
|
|
171
|
+
return { ok: false, mode: 'throughline', detail: 'command unavailable' };
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, mode: 'throughline', detail: 'command available' };
|
|
174
|
+
}
|
|
175
|
+
|
|
135
176
|
function formatCodexAuditorModelSelection({ backend, selection }) {
|
|
136
177
|
if (backend && backend !== 'codex-cli') return 'auditor-model=not-applicable';
|
|
137
178
|
if (!selection || typeof selection !== 'object') return 'auditor-model=unknown';
|
package/src/cli/install.mjs
CHANGED
|
@@ -57,6 +57,7 @@ export async function runInstall({
|
|
|
57
57
|
refreshFn = refresh,
|
|
58
58
|
codexCliPresentFn = isCodexCliPresent,
|
|
59
59
|
installCodexHooksFn = installCodexHooks,
|
|
60
|
+
auditorContext,
|
|
60
61
|
} = {}) {
|
|
61
62
|
const settingsPath = target === 'user'
|
|
62
63
|
? join(homedir(), '.claude', 'settings.json')
|
|
@@ -84,10 +85,14 @@ export async function runInstall({
|
|
|
84
85
|
const markerDir = join(cwd, '.spotter');
|
|
85
86
|
const markerPath = join(markerDir, 'marker.json');
|
|
86
87
|
await mkdir(markerDir, { recursive: true });
|
|
88
|
+
const preservedAuditorContext = auditorContext === undefined
|
|
89
|
+
? await readExistingAuditorContext(markerPath)
|
|
90
|
+
: auditorContext;
|
|
87
91
|
const marker = {
|
|
88
92
|
markerVersion: MARKER_VERSION,
|
|
89
93
|
spotterVersion: SPOTTER_VERSION,
|
|
90
94
|
installedAt: new Date().toISOString(),
|
|
95
|
+
auditorContext: preservedAuditorContext,
|
|
91
96
|
};
|
|
92
97
|
await writeFile(markerPath, JSON.stringify(marker, null, 2) + '\n', 'utf8');
|
|
93
98
|
console.log(` wrote ${markerPath}`);
|
|
@@ -171,6 +176,16 @@ export async function runInstall({
|
|
|
171
176
|
}
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
async function readExistingAuditorContext(markerPath) {
|
|
180
|
+
try {
|
|
181
|
+
const marker = JSON.parse(await readFile(markerPath, 'utf8'));
|
|
182
|
+
return marker?.auditorContext ?? { mode: 'disabled' };
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err?.code === 'ENOENT') return { mode: 'disabled' };
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
174
189
|
export function codexInstallNextSteps(projectRoot) {
|
|
175
190
|
return [
|
|
176
191
|
'review the three Spotter hooks with Codex /hooks',
|
|
@@ -89,6 +89,13 @@ export function createHaikuAuditorBackend({
|
|
|
89
89
|
},
|
|
90
90
|
async judge(input = {}) {
|
|
91
91
|
const stage = validateStage(input.stage);
|
|
92
|
+
if (input.recentContext !== undefined) {
|
|
93
|
+
throw new AuditorBackendError(
|
|
94
|
+
'E_AUDITOR_CONTEXT_BACKEND_UNSUPPORTED',
|
|
95
|
+
'recent conversation context is not supported by the haiku auditor backend',
|
|
96
|
+
{ backend: 'haiku', stage },
|
|
97
|
+
);
|
|
98
|
+
}
|
|
92
99
|
const prompt = buildStagePrompt(stage, input);
|
|
93
100
|
let raw, meta;
|
|
94
101
|
try {
|