claude-spotter 1.4.13 → 1.4.15
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 +70 -1
- package/README.ja.md +18 -10
- package/README.md +19 -11
- package/package.json +1 -1
- package/src/core/codex-cli-backend.mjs +40 -1
- package/src/hooks/lib.mjs +33 -13
- package/src/hooks/pre-tool-use.mjs +9 -6
- package/src/hooks/stop.mjs +19 -11
- package/src/hooks/user-prompt.mjs +33 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.15
|
|
4
|
+
|
|
5
|
+
**codex ログイン失効でサイレントに死に、host の Claude が無反応になる実害バグを根治**。codex auditor の
|
|
6
|
+
ログインが失効 (`token_revoked` / `refresh_token_reused` / `401 Unauthorized`) すると、`spotter install`
|
|
7
|
+
済みプロジェクトで毎ターン入力が消えて「Claude が一切反応しない」状態になっていた。実セッションの daemon
|
|
8
|
+
ログ (`handler error on user_input/turn_end: E_CODEX_CLI_EXIT`) + コード監査 + 公式 hook 仕様の裏取りで
|
|
9
|
+
根本原因を 2 点に特定: (A) codex 異常終了時、stderr/stdout に入っているログイン失効の痕跡を捨てて全部
|
|
10
|
+
`E_CODEX_CLI_EXIT` に潰しており、auth 失敗を区別できていなかった。(B) `UserPromptSubmit` hook が daemon
|
|
11
|
+
エラーを一律 `die(exit 2)` で処理しており、Claude Code は入力時 hook の exit 2 を **ブロッキング扱い =
|
|
12
|
+
プロンプト消去** とするため、失効が永続する限り毎ターン入力が消えていた。
|
|
13
|
+
|
|
14
|
+
### 変更点
|
|
15
|
+
|
|
16
|
+
- **編集 [src/core/codex-cli-backend.mjs](src/core/codex-cli-backend.mjs)**: codex の非ゼロ終了時に
|
|
17
|
+
stdout+stderr をスキャンし、ログイン失効の痕跡があれば新コード `E_CODEX_CLI_AUTH` (対処法
|
|
18
|
+
「`codex login`」を含むメッセージ) を投げる。痕跡が無ければ従来通り `E_CODEX_CLI_EXIT`。新規 export
|
|
19
|
+
`isCodexAuthFailure`。分類は非ゼロ終了経路のみ (auth 失敗は <1s で即終了するため timeout 経路は対象外)。
|
|
20
|
+
- **編集 [src/hooks/lib.mjs](src/hooks/lib.mjs)**: `formatSpotterWarning({code,message})` を新設
|
|
21
|
+
(`[Spotter からの警告]` ブロック、`E_CODEX_CLI_AUTH` は `codex login` を案内、他は理由コード入りの汎用
|
|
22
|
+
文面)。exit-code 契約コメントを「audit 失敗は loud degradation = exit 0 + additionalContext、exit 2 は
|
|
23
|
+
malformed envelope 専用」に更新。
|
|
24
|
+
- **編集 [src/hooks/user-prompt.mjs](src/hooks/user-prompt.mjs)**: daemon/transport/resurrect 失敗で
|
|
25
|
+
`die(exit 2)` する代わりに `degrade()` — 警告を `additionalContext` (drain 済み pending と merge) で出して
|
|
26
|
+
**exit 0 でプロンプトを通す**。失効に限らずあらゆる監査失敗で host が固まらない。throw 値の `.message`
|
|
27
|
+
アクセスを optional chaining 化し、非 Error throw が top-level catch (exit 2) に抜ける穴も塞いだ。
|
|
28
|
+
- **編集 [src/hooks/stop.mjs](src/hooks/stop.mjs)**: backend/transport エラーと marker 消失 (TOCTOU) で
|
|
29
|
+
`die(exit 2 = 継続強制)` をやめ、`status:"degraded"` 記録 + exit 0。pending は積まない (verdict 未生成)。
|
|
30
|
+
loud な警告は次の `UserPromptSubmit` が配信。
|
|
31
|
+
- **編集 [src/hooks/pre-tool-use.mjs](src/hooks/pre-tool-use.mjs)**: daemon/transport エラーで
|
|
32
|
+
`die(exit 2 = ツール拒否)` をやめ、`status:"degraded"` 記録 + exit 0 (ツール許可)。記録は best-effort
|
|
33
|
+
telemetry でありツールを止める理由にならない。
|
|
34
|
+
- **更新 [docs/SPOTTER_CLAUDE_CONTRACT.md](docs/SPOTTER_CLAUDE_CONTRACT.md)**: `UserPromptSubmit` /
|
|
35
|
+
`PreToolUse` / `Stop` の失敗時 exit-code 契約を新挙動に追従。
|
|
36
|
+
- **追記 [docs/open-issues.md](docs/open-issues.md)**: auth-freeze バグの解決を記録。`Stop` 失敗が
|
|
37
|
+
セッション最終ターンだと deferred-delivery の性質上サイレントになる残課題を P2 に追記。
|
|
38
|
+
- **テスト 11 件追加** ([test/hooks.test.mjs](test/hooks.test.mjs) /
|
|
39
|
+
[test/codex-cli-backend.test.mjs](test/codex-cli-backend.test.mjs)): auth 分類 / `formatSpotterWarning` /
|
|
40
|
+
UserPromptSubmit の loud degrade (auth/汎用/pending merge/resurrect 失敗/非 Error throw) / Stop degrade /
|
|
41
|
+
PreToolUse degrade。`node --test` 344 pass / 1 skip 緑。
|
|
42
|
+
|
|
43
|
+
### 検証
|
|
44
|
+
|
|
45
|
+
実プロジェクトの監査経路 (`createCodexCliAuditorBackend.judge` を実コードで起動) で、再ログイン後に
|
|
46
|
+
`pass` verdict が返ること、および fake spawn でログイン失効 stderr → `E_CODEX_CLI_AUTH` 分類 → hook が
|
|
47
|
+
`[Spotter からの警告]` を additionalContext に出して exit 0 することを確認。多エージェントの敵対的レビュー
|
|
48
|
+
(host-freeze 完全性 / §0 silent-fallback / 分類器精度+docs) で HIGH 2 / MEDIUM 3 を検出し全て反映。
|
|
49
|
+
|
|
50
|
+
## 1.4.14
|
|
51
|
+
|
|
52
|
+
**README 等の公開資産から内部コードネーム "Bell" を撤去**。Spotter の内部設計議論で使われている
|
|
53
|
+
"Bell" (主役の Claude を指す呼称) は private な codename であり、README や OG banner で
|
|
54
|
+
公開すべきものではないという運用判断を反映。コード・内部ドキュメント (CLAUDE.md / docs/) には
|
|
55
|
+
影響なし。npm tarball 同梱の README を新版で配るため patch bump。
|
|
56
|
+
|
|
57
|
+
### 変更点
|
|
58
|
+
|
|
59
|
+
- **編集 [README.md](README.md) / [README.ja.md](README.ja.md)**:
|
|
60
|
+
"Bell" を文脈に応じて `Claude` / `your primary Claude` / `the primary Claude` に置換、
|
|
61
|
+
`(Bell)` 括弧書きは削除。日本語版の `主体 (Bell) に` は `主体に` に整形。
|
|
62
|
+
- **編集 [.github/og.svg](.github/og.svg) と再生成された [.github/og.png](.github/og.png)**:
|
|
63
|
+
OG banner の bullet text 2 件を Claude 表記に。`svgexport` で 1280×640 PNG を再レンダリング。
|
|
64
|
+
- **編集 [.github/concept.svg](.github/concept.svg)**:
|
|
65
|
+
内部 HTML コメント `Bell side` を `Primary Claude side` に。
|
|
66
|
+
|
|
67
|
+
### 検証
|
|
68
|
+
|
|
69
|
+
- `grep -rn "Bell" README.md README.ja.md .github/` で公開資産に Bell 残存なしを確認。
|
|
70
|
+
- `node --test` 334 tests / 333 pass / 1 skip 緑 (README / asset 変更のみで test には影響しない)。
|
|
71
|
+
|
|
3
72
|
## 1.4.13
|
|
4
73
|
|
|
5
74
|
**Spotter 監査文面の末尾「監査役を明示してください」念押し行を削除**。
|
|
@@ -1044,7 +1113,7 @@ Haiku 突然死 (shutdown ログなしで daemon 再起動する事象、v0.12.0
|
|
|
1044
1113
|
|
|
1045
1114
|
### 事の発端
|
|
1046
1115
|
|
|
1047
|
-
v0.8.0 の `spotter db refresh` 実測で x-api (HTTP MCP) が 401 Unauthorized で落ちていた。`claude mcp list` では `x-api: https://kitepon.
|
|
1116
|
+
v0.8.0 の `spotter db refresh` 実測で x-api (HTTP MCP) が 401 Unauthorized で落ちていた。`claude mcp list` では `x-api: https://kitepon.dev/mcp (HTTP)` と表示され URL は拾えるが、Spotter の refresh プロセスから叩くと認証情報がないため拒否。ユーザーの指摘で `.mcp.json` を直接 cat したところ、実態は **stdio** で `env: {X_BEARER_TOKEN: "..."}` を持つ設定だった。CLI 表示と actual config が食い違っていた (CLI の cache の古さと思われる)。
|
|
1048
1117
|
|
|
1049
1118
|
判明した設計上の転換点:
|
|
1050
1119
|
|
package/README.ja.md
CHANGED
|
@@ -11,17 +11,25 @@
|
|
|
11
11
|
|
|
12
12
|
**[English](README.md) · 日本語**
|
|
13
13
|
|
|
14
|
-
> **気づく役と実行する役を分離する。** Claude Code
|
|
14
|
+
> **気づく役と実行する役を分離する。** Claude Code の横で並走し、主役の Claude が**ツールを呼び忘れたとき**だけ静かに指摘する監査役。
|
|
15
15
|
|
|
16
16
|
Claude には「使えるツールがあるのに、使うべきタイミングで使わない」という構造的な弱点があります。記録すべき決定を memory / caveat MCP に残さない、docs lookup MCP を呼ばずに古い知識で応答する、ブラウザ自動化 MCP で確認せず UI 状態を推測する — **「分からないと自覚できない」から、ツールを取りに行けない**。
|
|
17
17
|
|
|
18
|
-
Spotter はツールカタログを完全に把握した別エージェント (Claude Haiku 4.5) をセッション毎に常駐させ、
|
|
18
|
+
Spotter はツールカタログを完全に把握した別エージェント (Claude Haiku 4.5) をセッション毎に常駐させ、Claude の発話予定と応答を並走監査します。見落としを検出すると透明化された指摘として Claude に届け、補正応答を促します。**Claude が自覚して呼ぶ**設計は本プロダクトの存在意義を破壊するため、Claude から呼ぶのではなく hook 経由で Claude の意思と独立に検出する構造を取っています。
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src=".github/concept.svg" alt="Claude が答え、Spotter が見ている" width="80%">
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<sub><b>Claude</b> が答える(実行する役) · <b>Spotter</b> が見ている(気づく役・沈黙監査)</sub>
|
|
26
|
+
</p>
|
|
19
27
|
|
|
20
28
|
## 30 秒で見るポイント
|
|
21
29
|
|
|
22
30
|
Spotter が拾うのは、たとえばこういう瞬間です。
|
|
23
31
|
|
|
24
|
-
| 状況 |
|
|
32
|
+
| 状況 | Claude の応答 | Spotter の指摘 |
|
|
25
33
|
|---|---|---|
|
|
26
34
|
| 「この OAuth の落とし穴を覚えて」 | 了解だけして進める | memory / caveat MCP の使用機会 |
|
|
27
35
|
| 「このパッケージの最新版 API は?」 | 学習時点の知識で答える | docs lookup MCP の照会機会 |
|
|
@@ -33,7 +41,7 @@ Spotter が拾うのは、たとえばこういう瞬間です。
|
|
|
33
41
|
判定軸は 2 段階:
|
|
34
42
|
|
|
35
43
|
- **入力時 (`stage=user_input`)**: ユーザー要請に対し、ローカルカタログの description から用途が明確に該当するツールを列挙する **要請充足チェック**
|
|
36
|
-
- **応答後 (`stage=turn_end`)**:
|
|
44
|
+
- **応答後 (`stage=turn_end`)**: Claude の最終応答に対し、事実の断定 / 記録すべき新情報 / 既知情報の参照それぞれに、カタログ上のツール (検証 / 登録 / 照会) を差し込める余地がないかを問う **ツール適用機会の監査**
|
|
37
45
|
|
|
38
46
|
## インストール
|
|
39
47
|
|
|
@@ -86,8 +94,8 @@ Codex native `Stop` は遅延配送で、不足ツールの指摘を queue し
|
|
|
86
94
|
```mermaid
|
|
87
95
|
flowchart TD
|
|
88
96
|
U([User 発話]) --> UPH[UserPromptSubmit hook<br/>Spotter が発話とカタログから一次判定]
|
|
89
|
-
UPH --> BT[
|
|
90
|
-
BT --> BA([
|
|
97
|
+
UPH --> BT[Claude Thinking<br/>Spotter の推奨を<br/>additionalContext で受信]
|
|
98
|
+
BT --> BA([Claude の最初の応答])
|
|
91
99
|
BA --> SH[Stop hook<br/>応答と使用済みツールから最終チェック]
|
|
92
100
|
SH --> DEC{見落とし<br/>あり?}
|
|
93
101
|
DEC -->|なし| DONE([完了])
|
|
@@ -135,7 +143,7 @@ flowchart LR
|
|
|
135
143
|
| 対象 | コンテキスト肥大化 | ツール取りこぼし |
|
|
136
144
|
| 仕組み | hook で記憶退避 | hook でサブエージェント並走 |
|
|
137
145
|
|
|
138
|
-
両者に共通するのは
|
|
146
|
+
両者に共通するのは **「主体に頼らない仕組み」**。併用できます。
|
|
139
147
|
|
|
140
148
|
## よく使うコマンド
|
|
141
149
|
|
|
@@ -196,16 +204,16 @@ Codex CLI auditor の子プロセスは、hook 判定を安く速く保つため
|
|
|
196
204
|
|
|
197
205
|
- v1.4.8 以降、Claude / Codex 両 host で `Stop` hook は **遅延配送 (deferred delivery)** に統一されました。`Stop` で見落としツールを検出した場合、Spotter は `<projectRoot>/.spotter/pending/<sessionId>.json` に指摘を積み、次の same-session `UserPromptSubmit` で `additionalContext` として配信します。当ターンの最初の応答は transcript にそのまま残るため、`decision:"block"` で補正サイクルを回す方式の「最終応答が補正中心になって元の文脈が迷子」問題が解消します (Codex 側は `Stop Blocked` / exit code 1 回避も兼ねる)
|
|
198
206
|
- pending ファイルは Claude / Codex が同じパス (`.spotter/pending/`) を共有します。host-neutral 設計です
|
|
199
|
-
- **JSON スキーマ違反は v0.5.0 以降「想定済み異常」として silent pass + session renew で回復**します (role collapse 検知パス、daemon ログに `role_collapse_reset` を残す)。一方 **Haiku timeout は引き続き throw** され、UserPromptSubmit がブロックされてユーザー入力が
|
|
207
|
+
- **JSON スキーマ違反は v0.5.0 以降「想定済み異常」として silent pass + session renew で回復**します (role collapse 検知パス、daemon ログに `role_collapse_reset` を残す)。一方 **Haiku timeout は引き続き throw** され、UserPromptSubmit がブロックされてユーザー入力が Claude に届かない症状として顕在化します (timeout は v0.5.0 で 30s、v0.13.1 で 45s に拡張)。timeout の fail-open 化 (pass 扱い) は §0 改訂とセットで今後検討
|
|
200
208
|
|
|
201
209
|
<details>
|
|
202
210
|
<summary><strong>📋 最近のハイライト</strong></summary>
|
|
203
211
|
|
|
204
|
-
- **プラグイン形式の MCP サーバー対応** — `plugin:everything-claude-code:context7` のように名前に内部コロンを含むサーバーを正しくパースし、配下のツールをカタログに取り込めるようになった (旧版はこの形式のサーバーをすべて単一の `"plugin"` に潰して、
|
|
212
|
+
- **プラグイン形式の MCP サーバー対応** — `plugin:everything-claude-code:context7` のように名前に内部コロンを含むサーバーを正しくパースし、配下のツールをカタログに取り込めるようになった (旧版はこの形式のサーバーをすべて単一の `"plugin"` に潰して、Claude の監査から silent に脱落させていた)
|
|
205
213
|
- **プロジェクト単位の監査隔離** — daemon が監査に使うのはローカル DB のみ。グローバル DB は description 再利用キャッシュに役割限定。**他プロジェクト**でインストールしたツールが現プロジェクトの監査に混入することはない
|
|
206
214
|
- **手放しでカタログ維持** — `spotter install` が Claude DB を自動 seed、Claude / Codex それぞれの SessionStart が host-local DB を bg refresh する。手書き管理は一切不要
|
|
207
215
|
- **Codex native hooks** — Codex host は primary auditor backend として Codex CLI を使い、`.spotter/tool-db.codex.json` を Claude DB と分離し、backend failure は Haiku fallback ではなく明示 error として扱う
|
|
208
|
-
- **監査対象** — ユーザー追加分 (MCP / スキル / サブエージェント) のみ。Claude Code 本体側のツールは意図的に対象外 (
|
|
216
|
+
- **監査対象** — ユーザー追加分 (MCP / スキル / サブエージェント) のみ。Claude Code 本体側のツールは意図的に対象外 (Claude は元から自発率が高いため)
|
|
209
217
|
- **実装規範** — フォールバック禁止 / silent fallback 禁止 / 暫定コード禁止 ([CLAUDE.md §0](CLAUDE.md))
|
|
210
218
|
|
|
211
219
|
リリース履歴の全文は [CHANGELOG](CHANGELOG.md) を参照。
|
package/README.md
CHANGED
|
@@ -11,17 +11,25 @@
|
|
|
11
11
|
|
|
12
12
|
**English · [日本語](README.ja.md)**
|
|
13
13
|
|
|
14
|
-
> **Separate the spotter from the doer.** Spotter runs alongside Claude Code and quietly flags the moments when
|
|
14
|
+
> **Separate the spotter from the doer.** Spotter runs alongside Claude Code and quietly flags the moments when your primary Claude **forgets to use a tool it has access to**.
|
|
15
15
|
|
|
16
16
|
Claude has a structural blind spot: **it can't reach for a tool it doesn't realize it needs**. It may skip a project memory MCP when a decision should be recorded, answer from stale memory instead of a docs-lookup MCP, or reason about UI state without a browser-automation MCP. The model can't always tell when it doesn't know — so the tool stays unused.
|
|
17
17
|
|
|
18
|
-
Spotter pins a second agent (Claude Haiku 4.5) next to
|
|
18
|
+
Spotter pins a second agent (Claude Haiku 4.5) next to Claude. The second agent has the full tool catalog memorized and audits both the user's prompt and Claude's reply in parallel. When it spots a missed tool, it injects a transparent recommendation into Claude's context and, if needed, asks Claude to amend its answer. **Claude is never asked to self-audit** — that would defeat the entire premise. Detection happens through hooks, independent of Claude's intent.
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src=".github/concept.svg" alt="Claude answers · Spotter watches" width="80%">
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<sub><b>Claude</b> answers (the doer) · <b>Spotter</b> watches (the auditor, silent)</sub>
|
|
26
|
+
</p>
|
|
19
27
|
|
|
20
28
|
## See it in 30 seconds
|
|
21
29
|
|
|
22
30
|
Examples of what Spotter catches:
|
|
23
31
|
|
|
24
|
-
| Situation | What
|
|
32
|
+
| Situation | What Claude would do | What Spotter flags |
|
|
25
33
|
|---|---|---|
|
|
26
34
|
| "Please remember this OAuth gotcha" | Acknowledge and move on | Missed call to a memory / caveat MCP |
|
|
27
35
|
| "How does this package API work in the latest version?" | Answer from training-time knowledge | Missed call to a docs-lookup MCP |
|
|
@@ -33,7 +41,7 @@ Examples of what Spotter catches:
|
|
|
33
41
|
Spotter audits in two stages:
|
|
34
42
|
|
|
35
43
|
- **`stage=user_input`** — given the user's prompt, list any local catalog tools whose description clearly applies. A *prompt-fulfillment* check
|
|
36
|
-
- **`stage=turn_end`** — given
|
|
44
|
+
- **`stage=turn_end`** — given Claude's final reply, look for places where a catalog tool (verification / recording / lookup) could plug in. A *missed-opportunity* audit. Zero findings is fine; tools already used in this turn are not re-flagged
|
|
37
45
|
|
|
38
46
|
## Install
|
|
39
47
|
|
|
@@ -86,8 +94,8 @@ the next same-session `UserPromptSubmit`.
|
|
|
86
94
|
```mermaid
|
|
87
95
|
flowchart TD
|
|
88
96
|
U([User prompt]) --> UPH[UserPromptSubmit hook<br/>Spotter audits prompt against catalog]
|
|
89
|
-
UPH --> BT[
|
|
90
|
-
BT --> BA([
|
|
97
|
+
UPH --> BT[Claude thinking<br/>receives Spotter's recommendations<br/>as additionalContext]
|
|
98
|
+
BT --> BA([Claude's first answer])
|
|
91
99
|
BA --> SH[Stop hook<br/>Spotter re-audits answer + tools used]
|
|
92
100
|
SH --> DEC{Missed<br/>tool?}
|
|
93
101
|
DEC -->|No| DONE([Done])
|
|
@@ -135,7 +143,7 @@ The audited catalog is host-local: Claude uses `<project>/.spotter/tool-db.json`
|
|
|
135
143
|
| Target | Context bloat | Missed tool calls |
|
|
136
144
|
| Mechanism | Hook-driven memory eviction | Hook-driven sub-agent in parallel |
|
|
137
145
|
|
|
138
|
-
Both share the principle of **"don't rely on the primary agent
|
|
146
|
+
Both share the principle of **"don't rely on the primary agent to do it itself."** They compose well — you can run them together.
|
|
139
147
|
|
|
140
148
|
## Common commands
|
|
141
149
|
|
|
@@ -197,18 +205,18 @@ those values for smoke tests or controlled experiments.
|
|
|
197
205
|
|
|
198
206
|
## Known limitations
|
|
199
207
|
|
|
200
|
-
- The `Stop` hook fires **after**
|
|
208
|
+
- The `Stop` hook fires **after** Claude's first answer has already been streamed to the user. When Spotter sends Claude back, the user sees both the original answer and the corrected one. Detection accuracy in `UserPromptSubmit` (the *pre-response* stage) is therefore Spotter's primary axis of quality
|
|
201
209
|
- `Stop` hook is **deferred** for both Claude and Codex hosts as of v1.4.8. When Spotter finds a missed tool at `Stop`, it appends the finding to `<projectRoot>/.spotter/pending/<sessionId>.json` and surfaces it on the next same-session `UserPromptSubmit` as `additionalContext`. The original assistant message stays as the turn's final transcript entry — no `decision:"block"` re-generation cycle. The same pending file is shared by Claude and Codex (host-neutral path)
|
|
202
|
-
- **Since v0.5.0, JSON schema violations from Haiku are treated as expected-anomalies** (silent pass + session renew, logged as `role_collapse_reset`) — this is the role-collapse recovery path. **Haiku timeouts still throw**, which surfaces as `UserPromptSubmit` blocking the user's prompt from reaching
|
|
210
|
+
- **Since v0.5.0, JSON schema violations from Haiku are treated as expected-anomalies** (silent pass + session renew, logged as `role_collapse_reset`) — this is the role-collapse recovery path. **Haiku timeouts still throw**, which surfaces as `UserPromptSubmit` blocking the user's prompt from reaching Claude. Timeouts have been raised twice (30s in v0.5.0, 45s in v0.13.1); making timeouts fail-open is deferred until §0 is revisited
|
|
203
211
|
|
|
204
212
|
<details>
|
|
205
213
|
<summary><strong>📋 Recent highlights</strong></summary>
|
|
206
214
|
|
|
207
|
-
- **Plugin-scoped MCP servers** — names like `plugin:everything-claude-code:context7` (with internal colons) are now parsed correctly and their tools enter the catalog. Earlier versions silently collapsed all plugin MCP servers into a single literal `"plugin"`, dropping their tools from
|
|
215
|
+
- **Plugin-scoped MCP servers** — names like `plugin:everything-claude-code:context7` (with internal colons) are now parsed correctly and their tools enter the catalog. Earlier versions silently collapsed all plugin MCP servers into a single literal `"plugin"`, dropping their tools from Claude's audit
|
|
208
216
|
- **Per-project / per-host audit isolation** — the daemon audits against the local DB only; global DBs are host-specific description caches. Tools discovered in *other* projects or another host can never bleed into this project's audit set
|
|
209
217
|
- **Zero-touch catalog** — `spotter install` seeds the Claude DB automatically; Claude and Codex SessionStart hooks keep their host-local DBs fresh in the background. You never have to maintain the tool list by hand
|
|
210
218
|
- **Codex native hooks** — Codex host uses Codex CLI as the primary auditor backend, keeps a separate `.spotter/tool-db.codex.json`, and surfaces backend failures explicitly instead of falling back to Haiku
|
|
211
|
-
- **Audit scope** — only user-added surface (MCP servers / skills / sub-agents). Claude Code's built-in tools are intentionally out of scope;
|
|
219
|
+
- **Audit scope** — only user-added surface (MCP servers / skills / sub-agents). Claude Code's built-in tools are intentionally out of scope; Claude already uses those reliably
|
|
212
220
|
- **Implementation invariants** — no fallbacks, no silent failures, no provisional code (see [§0 in CLAUDE.md](CLAUDE.md))
|
|
213
221
|
|
|
214
222
|
Full release history: [CHANGELOG](CHANGELOG.md).
|
package/package.json
CHANGED
|
@@ -12,6 +12,37 @@ const DEFAULT_CODEX_CLI_REASONING_EFFORT = 'low';
|
|
|
12
12
|
const STDERR_LIMIT = 32 * 1024;
|
|
13
13
|
const STDOUT_LIMIT = 64 * 1024;
|
|
14
14
|
|
|
15
|
+
// codex prints auth/login failures to BOTH stdout (the JSON error stream, e.g.
|
|
16
|
+
// {"type":"error","message":"...sign in again..."}) and stderr (codex_login::auth::manager,
|
|
17
|
+
// e.g. "401 Unauthorized ... token_revoked"). We scan the combined text for these markers so a
|
|
18
|
+
// revoked/expired login is classified as the distinct, actionable E_CODEX_CLI_AUTH instead of
|
|
19
|
+
// being collapsed into the generic E_CODEX_CLI_EXIT. Conservative case-insensitive substring
|
|
20
|
+
// match keyed on the wording codex actually emits; if codex changes its wording the generic
|
|
21
|
+
// path still degrades loudly (the hook no longer freezes the host on either code). Classification
|
|
22
|
+
// runs only on the nonzero-exit path below — auth failures are observed to exit immediately
|
|
23
|
+
// (<1s), so the timeout path is not expected to see them; if codex ever starts hanging on auth,
|
|
24
|
+
// the timeout would surface as the generic E_CODEX_CLI_TIMEOUT rather than E_CODEX_CLI_AUTH.
|
|
25
|
+
const CODEX_AUTH_FAILURE_MARKERS = [
|
|
26
|
+
'token_revoked',
|
|
27
|
+
'refresh_token_reused',
|
|
28
|
+
'refresh token was already used',
|
|
29
|
+
'refresh token has already been used',
|
|
30
|
+
'invalidated oauth token',
|
|
31
|
+
'401 unauthorized',
|
|
32
|
+
'sign in again',
|
|
33
|
+
'sign back in',
|
|
34
|
+
'log out and sign in',
|
|
35
|
+
'not logged in',
|
|
36
|
+
'please log in',
|
|
37
|
+
'please login',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export function isCodexAuthFailure(text) {
|
|
41
|
+
if (typeof text !== 'string' || text.length === 0) return false;
|
|
42
|
+
const haystack = text.toLowerCase();
|
|
43
|
+
return CODEX_AUTH_FAILURE_MARKERS.some((marker) => haystack.includes(marker));
|
|
44
|
+
}
|
|
45
|
+
|
|
15
46
|
export const CODEX_AUDITOR_SCHEMA = {
|
|
16
47
|
type: 'object',
|
|
17
48
|
additionalProperties: false,
|
|
@@ -295,9 +326,17 @@ async function runCodexExec({
|
|
|
295
326
|
child.on('close', (code) => {
|
|
296
327
|
settle(() => {
|
|
297
328
|
if (code !== 0) {
|
|
329
|
+
const diag = { ...diagnostics(), exitCode: code };
|
|
330
|
+
if (isCodexAuthFailure(`${diag.stdout}\n${diag.stderr}`)) {
|
|
331
|
+
reject(new AuditorBackendError('E_CODEX_CLI_AUTH', 'codex-cli auth failed — codex login required (run `codex login`)', {
|
|
332
|
+
backend: 'codex-cli',
|
|
333
|
+
diagnostics: diag,
|
|
334
|
+
}));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
298
337
|
reject(new AuditorBackendError('E_CODEX_CLI_EXIT', `codex-cli exited with code ${code}`, {
|
|
299
338
|
backend: 'codex-cli',
|
|
300
|
-
diagnostics:
|
|
339
|
+
diagnostics: diag,
|
|
301
340
|
}));
|
|
302
341
|
return;
|
|
303
342
|
}
|
package/src/hooks/lib.mjs
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// Shared hook plumbing — stdin read, error → exit-code mapping per §14.3 / §14.4.
|
|
2
2
|
//
|
|
3
3
|
// Exit code contract (§14.3 / §14.4):
|
|
4
|
-
// 0 = success (normal flow)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// 0 = success (normal flow), OR a LOUD degradation: the audit could not run but the failure
|
|
5
|
+
// is surfaced in-band via a `[Spotter からの警告]` additionalContext block, so the host
|
|
6
|
+
// stays responsive and the user's prompt is never erased. UserPromptSubmit uses this for
|
|
7
|
+
// any daemon / auditor-backend failure (e.g. codex login expired) — see user-prompt.mjs.
|
|
8
|
+
// 1 = expected abnormal, non-blocking (Claude Code proceeds; first stderr line shown to user).
|
|
9
|
+
// 2 = unexpected protocol/contract violation (malformed Claude Code envelope, missing required
|
|
10
|
+
// field). Reserved for cases where there is no real user prompt worth preserving — a
|
|
11
|
+
// UserPromptSubmit exit 2 BLOCKS and erases the prompt, so audit failures must NOT use it.
|
|
7
12
|
//
|
|
8
|
-
//
|
|
13
|
+
// A loud degradation (visible warning naming the cause + remedy) is NOT a silent fallback: the
|
|
14
|
+
// user is explicitly told the audit did not run. The forbidden pattern (§14.1) is the SILENT
|
|
15
|
+
// exit-0 that leaves the user believing they are protected when they are not.
|
|
9
16
|
//
|
|
10
17
|
// v0.2 gate helpers (plan §18 / C:\Users\kite_\.claude\plans\10-cuddly-codd.md):
|
|
11
18
|
// - isChildCall(): env-var gate for Spotter's own child backend invocations
|
|
@@ -114,15 +121,6 @@ export function optionalString(input, key) {
|
|
|
114
121
|
return value;
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
// Map a TransportError / HaikuError into an exit code.
|
|
118
|
-
export function exitCodeFor(err) {
|
|
119
|
-
if (err && typeof err.code === 'string') {
|
|
120
|
-
if (err.code === 'E_UNREACHABLE') return 1;
|
|
121
|
-
return 2;
|
|
122
|
-
}
|
|
123
|
-
return 2;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
124
|
export function die(message, exitCode = 2) {
|
|
127
125
|
process.stderr.write(`spotter-hook: ${message}\n`);
|
|
128
126
|
process.exit(exitCode);
|
|
@@ -162,3 +160,25 @@ export function formatTransparentBlockReason(missingTools) {
|
|
|
162
160
|
...lines,
|
|
163
161
|
].join('\n');
|
|
164
162
|
}
|
|
163
|
+
|
|
164
|
+
// Loud degradation notice (§0 / §14.1): the auditor could not run this turn. Surfaced via the
|
|
165
|
+
// same additionalContext channel as findings so the host stays responsive AND the user is told
|
|
166
|
+
// they are temporarily unprotected (and, for an expired codex login, exactly how to recover).
|
|
167
|
+
// This is the opposite of a silent fallback.
|
|
168
|
+
export function formatSpotterWarning({ code, message } = {}) {
|
|
169
|
+
const header = '[Spotter からの警告]';
|
|
170
|
+
if (code === 'E_CODEX_CLI_AUTH') {
|
|
171
|
+
return [
|
|
172
|
+
header,
|
|
173
|
+
'Spotter の監査エンジン (codex) のログインが失効しているため、今回の入力は監査できませんでした。',
|
|
174
|
+
'この応答を続ける前に、ユーザーに「端末で `codex login` を実行して再ログインすれば Spotter の監査が復旧する」ことを伝えてください。',
|
|
175
|
+
].join('\n');
|
|
176
|
+
}
|
|
177
|
+
const lines = [
|
|
178
|
+
header,
|
|
179
|
+
`Spotter は今回の入力を監査できませんでした (理由コード: ${code ?? 'unknown'})。この応答は監査されていません。`,
|
|
180
|
+
'この応答を続ける前に、ユーザーに Spotter の監査が一時的に無効になっていることを伝えてください。',
|
|
181
|
+
];
|
|
182
|
+
if (typeof message === 'string' && message.length > 0) lines.push(`詳細: ${message}`);
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import {
|
|
8
8
|
readStdinJson,
|
|
9
9
|
requireString,
|
|
10
|
-
exitCodeFor,
|
|
11
10
|
die,
|
|
12
11
|
isChildCall,
|
|
13
12
|
isSubagentCall,
|
|
@@ -23,7 +22,6 @@ export async function runPreToolUse({
|
|
|
23
22
|
readInput = readStdinJson,
|
|
24
23
|
sendRequestFn = sendRequest,
|
|
25
24
|
recordHookEventFn = recordClaudeHookEvent,
|
|
26
|
-
dieFn = die,
|
|
27
25
|
} = {}) {
|
|
28
26
|
if (isChildCall()) return;
|
|
29
27
|
const input = await readInput();
|
|
@@ -43,17 +41,21 @@ export async function runPreToolUse({
|
|
|
43
41
|
timeoutMs: TIMEOUT_MS,
|
|
44
42
|
});
|
|
45
43
|
if (response.ok !== true) {
|
|
44
|
+
// Recording is best-effort telemetry, not an audit verdict. On a daemon-side error, allow
|
|
45
|
+
// the tool (exit 0) — a PreToolUse exit 2 would DENY the tool, which must never happen
|
|
46
|
+
// just because Spotter could not record it. The audit at user_input/turn_end still warns
|
|
47
|
+
// loudly if the backend is down.
|
|
46
48
|
await recordHookEventFn({
|
|
47
49
|
projectRoot,
|
|
48
50
|
event: {
|
|
49
51
|
hook: 'PreToolUse',
|
|
50
|
-
status: '
|
|
52
|
+
status: 'degraded',
|
|
51
53
|
toolName,
|
|
52
54
|
code: response.error?.code ?? 'E_INTERNAL',
|
|
55
|
+
reason: 'daemon_error',
|
|
53
56
|
durationMs: Date.now() - startedAt,
|
|
54
57
|
},
|
|
55
58
|
});
|
|
56
|
-
dieFn(`daemon error on tool_used: ${response.error?.code ?? '?'}: ${response.error?.message ?? ''}`, 2);
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
61
|
await recordHookEventFn({
|
|
@@ -66,17 +68,18 @@ export async function runPreToolUse({
|
|
|
66
68
|
},
|
|
67
69
|
});
|
|
68
70
|
} catch (err) {
|
|
71
|
+
// Transport failure during best-effort recording: allow the tool (no exit 2 deny).
|
|
69
72
|
await recordHookEventFn({
|
|
70
73
|
projectRoot,
|
|
71
74
|
event: {
|
|
72
75
|
hook: 'PreToolUse',
|
|
73
|
-
status: '
|
|
76
|
+
status: 'degraded',
|
|
74
77
|
toolName,
|
|
75
78
|
code: err?.code ?? 'E_INTERNAL',
|
|
79
|
+
reason: 'transport',
|
|
76
80
|
durationMs: Date.now() - startedAt,
|
|
77
81
|
},
|
|
78
82
|
});
|
|
79
|
-
dieFn(`pre-tool-use transport failure: ${err.code ?? '?'}: ${err.message}`, exitCodeFor(err));
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
package/src/hooks/stop.mjs
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
// queue and folds the entries into `additionalContext`, so Bell receives the audit finding
|
|
11
11
|
// alongside the next user input. The original A reply stays as the turn's final message.
|
|
12
12
|
//
|
|
13
|
-
// `decision:"block"` is no longer emitted from this hook. Backend / transport errors
|
|
14
|
-
//
|
|
13
|
+
// `decision:"block"` is no longer emitted from this hook. Backend / transport errors do NOT
|
|
14
|
+
// force a continuation (a Stop exit 2 would block stopping = harmful noise on a Spotter-side
|
|
15
|
+
// failure); they are recorded as `degraded` and exit 0. The next UserPromptSubmit surfaces the
|
|
16
|
+
// loud `[Spotter からの警告]`. No verdict is produced on failure, so nothing is queued — this is
|
|
17
|
+
// still not a silent "all clear" (the surfacing just moves to the next turn — see lib.mjs).
|
|
15
18
|
// `stop_hook_active:true` is still observed: the daemon early-passes on it, so we just
|
|
16
19
|
// receive `pass:true` and return without writing pending context.
|
|
17
20
|
//
|
|
@@ -20,7 +23,6 @@
|
|
|
20
23
|
import {
|
|
21
24
|
readStdinJson,
|
|
22
25
|
requireString,
|
|
23
|
-
exitCodeFor,
|
|
24
26
|
die,
|
|
25
27
|
findSpotterMarker,
|
|
26
28
|
formatTransparentBlockReason,
|
|
@@ -41,7 +43,6 @@ export async function runStop({
|
|
|
41
43
|
appendPendingContextFn = appendPendingContext,
|
|
42
44
|
getLastAssistantTextFn = getLastAssistantText,
|
|
43
45
|
recordHookEventFn = recordClaudeHookEvent,
|
|
44
|
-
dieFn = die,
|
|
45
46
|
} = {}) {
|
|
46
47
|
if (isChildCall()) return;
|
|
47
48
|
const input = await readInput();
|
|
@@ -71,30 +72,36 @@ export async function runStop({
|
|
|
71
72
|
timeoutMs: TIMEOUT_MS,
|
|
72
73
|
});
|
|
73
74
|
} catch (err) {
|
|
75
|
+
// Spotter-side failure (daemon unreachable, etc.): record + exit 0. Do NOT force the model to
|
|
76
|
+
// continue (a Stop exit 2 blocks stopping). The next UserPromptSubmit surfaces the loud
|
|
77
|
+
// warning; no pending is written, so this is not a silent "all clear" — there was no verdict.
|
|
74
78
|
await recordHookEventFn({
|
|
75
79
|
projectRoot,
|
|
76
80
|
event: {
|
|
77
81
|
hook: 'Stop',
|
|
78
|
-
status: '
|
|
82
|
+
status: 'degraded',
|
|
79
83
|
code: err?.code ?? 'E_INTERNAL',
|
|
84
|
+
reason: 'transport',
|
|
80
85
|
durationMs: Date.now() - startedAt,
|
|
81
86
|
},
|
|
82
87
|
});
|
|
83
|
-
dieFn(`stop transport failure: ${err.code ?? '?'}: ${err.message}`, exitCodeFor(err));
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
if (response.ok !== true) {
|
|
92
|
+
// Auditor backend failed (e.g. codex login expired): record + exit 0. Forcing a continuation
|
|
93
|
+
// (exit 2) on a Spotter-side failure is harmful; the loud warning is delivered by the next
|
|
94
|
+
// UserPromptSubmit. No verdict was produced, so nothing is queued.
|
|
88
95
|
await recordHookEventFn({
|
|
89
96
|
projectRoot,
|
|
90
97
|
event: {
|
|
91
98
|
hook: 'Stop',
|
|
92
|
-
status: '
|
|
99
|
+
status: 'degraded',
|
|
93
100
|
code: response.error?.code ?? 'E_INTERNAL',
|
|
101
|
+
reason: 'daemon_error',
|
|
94
102
|
durationMs: Date.now() - startedAt,
|
|
95
103
|
},
|
|
96
104
|
});
|
|
97
|
-
dieFn(`daemon error on turn_end: ${response.error?.code ?? '?'}: ${response.error?.message ?? ''}`, 2);
|
|
98
105
|
return;
|
|
99
106
|
}
|
|
100
107
|
|
|
@@ -117,9 +124,10 @@ export async function runStop({
|
|
|
117
124
|
// decision:"block". Using the same transparent block-reason wording keeps the user-facing
|
|
118
125
|
// text identical to the prior block flow.
|
|
119
126
|
if (!projectRoot) {
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
127
|
+
// TOCTOU: the marker disappeared between the isOutsideSpotterProject guard and now. We cannot
|
|
128
|
+
// persist the finding (no project dir to write into), but a Stop exit 2 would force the model
|
|
129
|
+
// to continue — the harmful pattern this hook avoids. Drop the unpersistable finding and exit
|
|
130
|
+
// 0; the next UserPromptSubmit re-audits from a fresh marker walk.
|
|
123
131
|
return;
|
|
124
132
|
}
|
|
125
133
|
const text = formatTransparentBlockReason(result.missing_tools);
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
import {
|
|
17
17
|
readStdinJson,
|
|
18
18
|
requireString,
|
|
19
|
-
exitCodeFor,
|
|
20
19
|
die,
|
|
21
20
|
formatTransparentContext,
|
|
21
|
+
formatSpotterWarning,
|
|
22
22
|
isChildCall,
|
|
23
23
|
isSubagentCall,
|
|
24
24
|
isOutsideSpotterProject,
|
|
@@ -39,7 +39,6 @@ export async function runUserPrompt({
|
|
|
39
39
|
drainPendingContextsFn = drainPendingContexts,
|
|
40
40
|
recordHookEventFn = recordClaudeHookEvent,
|
|
41
41
|
writeOutput = (text) => process.stdout.write(text),
|
|
42
|
-
dieFn = die,
|
|
43
42
|
} = {}) {
|
|
44
43
|
if (isChildCall()) return;
|
|
45
44
|
const input = await readInput();
|
|
@@ -58,6 +57,26 @@ export async function runUserPrompt({
|
|
|
58
57
|
? await drainPendingContextsFn({ projectRoot, sessionId })
|
|
59
58
|
: [];
|
|
60
59
|
|
|
60
|
+
// Loud degradation (§0 / §14.1): the audit could not run, but the user's prompt is valid.
|
|
61
|
+
// Surface a visible [Spotter からの警告] (merged with any drained pending context) and exit 0
|
|
62
|
+
// so the prompt reaches the host — never erase it with a blocking exit 2.
|
|
63
|
+
const degrade = async ({ code, message, reason }) => {
|
|
64
|
+
const contexts = pendingContexts.slice();
|
|
65
|
+
contexts.push(formatSpotterWarning({ code, message }));
|
|
66
|
+
emitAdditionalContext(writeOutput, contexts);
|
|
67
|
+
await recordHookEventFn({
|
|
68
|
+
projectRoot,
|
|
69
|
+
event: {
|
|
70
|
+
hook: 'UserPromptSubmit',
|
|
71
|
+
status: 'degraded',
|
|
72
|
+
code: code ?? 'E_INTERNAL',
|
|
73
|
+
reason,
|
|
74
|
+
pendingContextCount: pendingContexts.length,
|
|
75
|
+
durationMs: Date.now() - startedAt,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
61
80
|
if ([...prompt.trim()].length <= SHORT_PROMPT_MAX_CHARS) {
|
|
62
81
|
if (pendingContexts.length > 0) {
|
|
63
82
|
emitAdditionalContext(writeOutput, pendingContexts);
|
|
@@ -88,67 +107,31 @@ export async function runUserPrompt({
|
|
|
88
107
|
response = await sendUserInput();
|
|
89
108
|
} catch (err) {
|
|
90
109
|
if (err instanceof TransportError && err.code === 'E_UNREACHABLE') {
|
|
91
|
-
// v0.12.0: daemon is gone (heartbeat shutdown, crash, missing). Resurrect and retry.
|
|
92
|
-
|
|
93
|
-
dieFn(`user-prompt: cannot resurrect daemon — no .spotter/marker.json above cwd=${input.cwd}`, 2);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
110
|
+
// v0.12.0: daemon is gone (heartbeat shutdown, crash, missing). Resurrect and retry once.
|
|
111
|
+
// projectRoot is guaranteed non-null here (isOutsideSpotterProject early-returned otherwise).
|
|
96
112
|
try {
|
|
97
113
|
await spawnDaemonAndWaitReadyFn({ sessionId, projectRoot });
|
|
98
|
-
} catch (spawnErr) {
|
|
99
|
-
await recordHookEventFn({
|
|
100
|
-
projectRoot,
|
|
101
|
-
event: {
|
|
102
|
-
hook: 'UserPromptSubmit',
|
|
103
|
-
status: 'error',
|
|
104
|
-
code: spawnErr?.code ?? 'E_RESURRECT_FAILED',
|
|
105
|
-
durationMs: Date.now() - startedAt,
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
dieFn(`user-prompt: daemon resurrect failed: ${spawnErr.message}`, spawnErr.exitCode ?? 2);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
114
|
response = await sendUserInput();
|
|
113
|
-
} catch (
|
|
114
|
-
await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
status: 'error',
|
|
119
|
-
code: retryErr?.code ?? 'E_INTERNAL',
|
|
120
|
-
durationMs: Date.now() - startedAt,
|
|
121
|
-
},
|
|
115
|
+
} catch (recoverErr) {
|
|
116
|
+
await degrade({
|
|
117
|
+
code: recoverErr?.code ?? 'E_RESURRECT_FAILED',
|
|
118
|
+
message: recoverErr?.message ?? '',
|
|
119
|
+
reason: 'resurrect_failed',
|
|
122
120
|
});
|
|
123
|
-
dieFn(`user-prompt transport failure after resurrect: ${retryErr.code ?? '?'}: ${retryErr.message}`, exitCodeFor(retryErr));
|
|
124
121
|
return;
|
|
125
122
|
}
|
|
126
123
|
} else {
|
|
127
|
-
await
|
|
128
|
-
projectRoot,
|
|
129
|
-
event: {
|
|
130
|
-
hook: 'UserPromptSubmit',
|
|
131
|
-
status: 'error',
|
|
132
|
-
code: err?.code ?? 'E_INTERNAL',
|
|
133
|
-
durationMs: Date.now() - startedAt,
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
dieFn(`user-prompt transport failure: ${err.code ?? '?'}: ${err.message}`, exitCodeFor(err));
|
|
124
|
+
await degrade({ code: err?.code ?? 'E_INTERNAL', message: err?.message ?? '', reason: 'transport' });
|
|
137
125
|
return;
|
|
138
126
|
}
|
|
139
127
|
}
|
|
140
128
|
|
|
141
129
|
if (response.ok !== true) {
|
|
142
|
-
await
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
status: 'error',
|
|
147
|
-
code: response.error?.code ?? 'E_INTERNAL',
|
|
148
|
-
durationMs: Date.now() - startedAt,
|
|
149
|
-
},
|
|
130
|
+
await degrade({
|
|
131
|
+
code: response.error?.code ?? 'E_INTERNAL',
|
|
132
|
+
message: response.error?.message ?? '',
|
|
133
|
+
reason: 'daemon_error',
|
|
150
134
|
});
|
|
151
|
-
dieFn(`daemon error on user_input: ${response.error?.code ?? '?'}: ${response.error?.message ?? ''}`, 2);
|
|
152
135
|
return;
|
|
153
136
|
}
|
|
154
137
|
|