claude-spotter 1.4.28 → 1.5.0

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.ja.md +31 -0
  3. package/README.md +32 -0
  4. package/bin/spotter.mjs +15 -0
  5. package/docs/11_dashboard-operations.md +118 -0
  6. package/ops/dashboard/hub-config.json +1 -0
  7. package/ops/dashboard/launchd/dev.kitepon.spotter-dashboard-device.plist +21 -0
  8. package/ops/dashboard/launchd/dev.kitepon.spotter-dashboard-tunnel.plist +23 -0
  9. package/ops/dashboard/systemd/spotter-dashboard-device.service +13 -0
  10. package/ops/dashboard/systemd/spotter-dashboard-hub.service +12 -0
  11. package/ops/dashboard/systemd/spotter-dashboard-tunnel.service +14 -0
  12. package/ops/dashboard/windows/install-dashboard-tasks.ps1 +16 -0
  13. package/ops/dashboard/windows/spotter-dashboard-device.ps1 +4 -0
  14. package/ops/dashboard/windows/spotter-dashboard-tunnel.ps1 +7 -0
  15. package/package.json +9 -2
  16. package/scripts/verify-release-commit.mjs +50 -0
  17. package/scripts/verify-release-commit.test.mjs +48 -0
  18. package/src/cli/codex-hook-cmd.mjs +180 -1
  19. package/src/cli/dashboard-cmd.mjs +129 -0
  20. package/src/cli/diagnostics-cmd.mjs +1 -1
  21. package/src/cli/evaluation-cmd.mjs +129 -0
  22. package/src/core/codex-transcript.mjs +13 -3
  23. package/src/core/evaluation-context.mjs +184 -0
  24. package/src/core/evaluation-report.mjs +84 -0
  25. package/src/core/evaluation-store.mjs +461 -0
  26. package/src/core/evaluation-tool-id.mjs +382 -0
  27. package/src/daemon/daemon.mjs +93 -4
  28. package/src/dashboard/device-server.mjs +216 -0
  29. package/src/dashboard/hub-server.mjs +214 -0
  30. package/src/dashboard/model.mjs +14 -0
  31. package/src/dashboard/render.mjs +225 -0
  32. package/src/hooks/pre-tool-use.mjs +57 -1
  33. package/src/hooks/user-prompt.mjs +83 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0 — 2026-08-04
4
+
5
+ - **評価結果を端末別Web dashboardで表示。** 各端末の`~/.spotter/evaluation.db`をrequestごとに読み、
6
+ `S/P/I/C/A/M`、提案率`P/S`、採用率`A/C`、project/tool内訳、非採用caseと詳細を
7
+ dependency-freeのserver-side HTMLで表示する。request、auditorへ渡したcontext、Throughline
8
+ observer snapshotは混同せず別欄に置く。
9
+ - **端末選択hubを追加。** 静的な4端末mapから一覧を表示し、`/devices/<id>/`を各device serverへ
10
+ proxyする。healthは一覧request時に各端末1回だけ取得し、offlineやtimeoutは該当端末だけ502にする。
11
+ DB同期、background monitor、retry queue、reconcilerは追加しない。
12
+ - **運用面を同梱。** `spotter dashboard device` / `hub`、Mac LaunchAgent、Linux systemd user、
13
+ Windows Task Scheduler、SSH reverse tunnel、main-server hub config、Caddy/Cloudflare Accessの
14
+ 配備手順を追加した。評価データは端末内に留め、公開面だけをowner emailのAccessで保護する。
15
+ - **検証。** dashboard focused test 16件、fixture SQLiteからdevice HTTP・hub proxyまでの経路、
16
+ full suite 584件(582 pass / 2 platform skip)を通過した。
17
+
18
+ ## 1.4.29 — 2026-08-04
19
+
20
+ - **提案率とtool採用率の端末内評価を追加。** Claude / Codexの成功UserPromptSubmitを母数に、
21
+ safe projector後に実際に提示したtool itemと、同じturnでのcanonical tool利用を
22
+ `~/.spotter/evaluation.db`へ記録する。採用率の母数はoutcome確定itemだけとし、利用記録不全は
23
+ `outcome_missing`へ分離する。組込みtoolは評価対象外で、既存turn-end auditorのraw usedToolsは変更しない。
24
+ - **提案時の別文脈を保存。** proposal確定時刻を記録し、既存Throughline `observer-read`を一度だけ呼ぶ。
25
+ Spotterがauditorへ渡したcontextとは別fieldに保存し、失敗時はcontextだけunavailableとしてretryしない。
26
+ 返却されたhost / thread hashを提案元sessionと照合し、同じprojectの並行sessionを取り違えた場合も
27
+ snapshotを保存しない。
28
+ - **欠測とtool identityを率から分離。** Claudeの利用記録失敗、Codex transcript不全、Stopが30分以上
29
+ 欠落したopen proposalは`outcome_missing`として投影し、非採用へ混ぜない。Codex Skillは提案済みの
30
+ 正規`SKILL.md`を実際にreadしたcallだけを採用として認識する。現行Codexのouter `exec`内で実行される
31
+ nested MCP callと、同じturn内の複数`exec`も個別の利用入力として保持する。
32
+ - **初回同時openを直列化。** 2 projectが未作成の評価DBを同時に開いてもWAL設定前に
33
+ `database is locked`とならないよう、bounded busy timeoutをschema初期化より先に有効化する。
34
+ retry、lockfile、background回収は追加しない。
35
+ - **評価CLIを追加。** `spotter evaluation report`で全project / project / tool / host別の
36
+ `S/P/I/C/A/M`、`P/S`、`A/C`を表示し、`cases` / `case`で非採用caseのrequest、2種類の文脈、
37
+ 提案ID、利用ID、outcomeを確認できる。reportは保存済みSQLiteだけを読み、JSONはWindows
38
+ PowerShell 5.1でも壊れないASCII-safe形式で出力する。
39
+ - **検証・公開。** 固定fixture、Claude / Codex hook、Throughline束縛、2 process同時writeを含む
40
+ 568 tests(macOS / Linuxは566 pass / 2 skip、Windowsは556 pass / 12 platform skip)、
41
+ macOS / Linux / Windows × Node 22.13 / 22.xのCI run `30913375991`、75-file packを通過した。
42
+ 別projectの実Codex turnではThroughline文脈付きproposalから
43
+ `S=1 P=1 I=1 C=1 A=1 M=0`を記録し、Claude実turnのpass観測と両hostのproposal fixtureも確認した。
44
+ Windowsではouter `exec`内の絶対pathを壊さずSkill採用へ変換する回帰も固定した。
45
+
3
46
  ## 1.4.28 — 2026-07-21
4
47
 
5
48
  - **Windows Codex SessionStartの誤timeoutを修正。** detached refreshを起動するSpotter所有hookの
package/README.ja.md CHANGED
@@ -13,6 +13,16 @@
13
13
 
14
14
  > **気づく役と実行する役を分離する。** Claude Code の横で並走し、主役の Claude が**ツールを呼び忘れたとき**だけ静かに指摘する監査役。
15
15
 
16
+ [kitepon.dev](https://kitepon.dev/)を運営する[クオ(@QLyun35332)](https://x.com/QLyun35332)が
17
+ 開発・メンテナンスしています。
18
+
19
+ ## 所有境界
20
+
21
+ 本repositoryは監査挙動、project marker、installer、release、diagnosticsを所有します。
22
+ 製品横断のcatalogとhost統合は、kitepon.devの製品開発を支える内部基盤
23
+ [dotagents](https://github.com/kitepon-rgb/dotagents)が担当します。
24
+ MarkItDownは別区分の第三者CLIです。
25
+
16
26
  Claude には「使えるツールがあるのに、使うべきタイミングで使わない」という構造的な弱点があります。記録すべき決定を memory / caveat MCP に残さない、docs lookup MCP を呼ばずに古い知識で応答する、ブラウザ自動化 MCP で確認せず UI 状態を推測する — **「分からないと自覚できない」から、ツールを取りに行けない**。
17
27
 
18
28
  Spotter はツールカタログを完全に把握した別の監査エージェントで、ユーザー入力と主役 AI の応答を並走監査します。自動選択では Claude host は Codex CLI があればそれを、なければ session-scoped Haiku を選び、Codex host は Codex CLI を既定にします。明示 backend override は host より優先しますが、runtime failure で別 backend へ黙って切り替えません。応答前は検証済みtool IDだけを固定・非命令形の助言へ変換でき、応答後のfindingは構造eventに留めて後続turnへ注入しません。監査用AIの自由文が親セッションへ入ることはありません。**主役 AI が自覚して自己監査する**設計は本プロダクトの存在意義を破壊するため、hook 経由でその意思と独立に検出します。
@@ -207,6 +217,16 @@ spotter db rebuild # Claude local + Claude global DB を両方消してか
207
217
  spotter status # 稼働中の daemon 一覧
208
218
  spotter doctor # 環境診断 (Node / claude CLI / Codex readiness / tool-db 整合性)
209
219
  spotter diagnostics logs # daemon log から pass=false / backend latency / anomaly signal を集計
220
+ spotter evaluation report
221
+ # 端末内DBからproject横断の提案率・tool採用率を集計
222
+ spotter evaluation cases --outcome not-adopted
223
+ # 提案されたが同じturnで使われなかったtool itemを一覧
224
+ spotter evaluation case <observation-id>
225
+ # request、2種類の文脈、提案、利用、outcomeを確認
226
+ spotter dashboard device --id mac --name Mac
227
+ # この端末の評価DBを127.0.0.1:53940で配信
228
+ spotter dashboard hub --config dashboard-hub.json --host 172.18.0.1
229
+ # 端末一覧と/devices/<id>/の端末別proxyを配信
210
230
  spotter codex risk-check --findings findings.json --host-agent claude
211
231
  # Spotter finding を codex-sidecar に渡して read-only risk analysis
212
232
  spotter codex review|explore|opinion --findings findings.json --host-agent claude
@@ -223,6 +243,17 @@ spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.v2.js
223
243
  spotter uninstall # hook 登録を解除 (~/.spotter は残す)
224
244
  ```
225
245
 
246
+ ### 端末別評価dashboard
247
+
248
+ dashboardはlocal-firstで動く。各端末が自身の`~/.spotter/evaluation.db`を読み、hubは固定の
249
+ 端末・upstream対応だけを持つ。評価データをcloud DBへ複製しない。端末画面では
250
+ `S/P/I/C/A/M`、提案率、採用率、project/tool内訳、非採用case、別々に取得した2種類の文脈を
251
+ 確認できる。health確認は端末一覧request時だけなので、端末がofflineでもbackground監視や
252
+ retry queueを作らず、その端末だけを切り離せる。
253
+
254
+ 4端末のservice、reverse tunnel、Caddy/Cloudflare構成は
255
+ [docs/11_dashboard-operations.md](docs/11_dashboard-operations.md)を参照。
256
+
226
257
  Codex risk dispatch を daemon から非同期に流す場合:
227
258
 
228
259
  ```bash
package/README.md CHANGED
@@ -13,6 +13,16 @@
13
13
 
14
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
+ Built and maintained by [Quo](https://x.com/QLyun35332) at [kitepon.dev](https://kitepon.dev/en).
17
+
18
+ ## Ownership boundary
19
+
20
+ This repository owns auditor behavior, project markers, installers, release,
21
+ and diagnostics. The cross-product catalog and host integration are handled by
22
+ [dotagents](https://github.com/kitepon-rgb/dotagents), the internal development
23
+ toolchain behind kitepon.dev's products.
24
+ MarkItDown is managed separately as a third-party CLI.
25
+
16
26
  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
27
 
18
28
  Spotter runs a separate auditor with the full tool catalog and checks both the user's prompt and the primary agent's reply. Automatic selection uses Codex CLI on a Claude host when available, otherwise the session-scoped Haiku path; on a Codex host it defaults to Codex CLI. An explicit backend override takes precedence, but a runtime failure never silently switches backend. Before the primary reply, validated tool IDs may become fixed, non-directive advice; after the reply, findings remain structured events and are not injected into a later turn. Auditor prose never enters the parent session. **The primary agent is never asked to self-audit** — that would defeat the premise. Detection happens through hooks, independent of the primary agent's intent.
@@ -216,6 +226,16 @@ spotter doctor # environment check (Node / claude CLI / Codex readines
216
226
  spotter diagnostics logs # summarize daemon logs for pass=false / backend latency / anomaly signals
217
227
  spotter diagnostics runtime-errors
218
228
  # print the local allow-listed runtime-error aggregate snapshot (no network)
229
+ spotter evaluation report
230
+ # show cross-project proposal/adoption counts and rates from the local DB
231
+ spotter evaluation cases --outcome not-adopted
232
+ # list proposed tools that were not used in the same turn
233
+ spotter evaluation case <observation-id>
234
+ # inspect request, Spotter context, Throughline snapshot, proposal, usage, and outcome
235
+ spotter dashboard device --id mac --name Mac
236
+ # serve this terminal's local evaluation DB on 127.0.0.1:53940
237
+ spotter dashboard hub --config dashboard-hub.json --host 172.18.0.1
238
+ # list terminals and proxy /devices/<id>/ to their local servers
219
239
  spotter codex risk-check --findings findings.json --host-agent claude
220
240
  # run read-only codex-sidecar risk analysis for Spotter findings
221
241
  spotter codex review|explore|opinion --findings findings.json --host-agent claude
@@ -232,6 +252,18 @@ spotter auditor model-matrix --fixtures test/fixtures/auditor-model-matrix.v2.js
232
252
  spotter uninstall # remove hooks from this project (leaves ~/.spotter intact)
233
253
  ```
234
254
 
255
+ ### Device-routed evaluation dashboard
256
+
257
+ The dashboard is local-first. Every terminal reads its own `~/.spotter/evaluation.db`; the hub
258
+ keeps only a static device-to-upstream map and does not copy evaluation data into a cloud database.
259
+ The device view shows `S/P/I/C/A/M`, proposal and adoption rates, project/tool breakdowns,
260
+ non-adopted cases, and the two separately captured context sources. The hub checks health only
261
+ when the device list is requested, so an offline terminal is isolated without a background monitor
262
+ or retry queue.
263
+
264
+ The reference four-terminal service, reverse-tunnel, and Caddy/Cloudflare layout is documented in
265
+ [docs/11_dashboard-operations.md](docs/11_dashboard-operations.md).
266
+
235
267
  Optional async Codex risk dispatch:
236
268
 
237
269
  ```bash
package/bin/spotter.mjs CHANGED
@@ -11,6 +11,8 @@ import { runCodexCommand } from '../src/cli/codex-cmd.mjs';
11
11
  import { runCodexHookCommand } from '../src/cli/codex-hook-cmd.mjs';
12
12
  import { runAuditorCommand } from '../src/cli/auditor-cmd.mjs';
13
13
  import { runDiagnosticsCommand } from '../src/cli/diagnostics-cmd.mjs';
14
+ import { runEvaluationCommand } from '../src/cli/evaluation-cmd.mjs';
15
+ import { runDashboardCommand } from '../src/cli/dashboard-cmd.mjs';
14
16
  import { runFactoryDiagnostics } from '../src/cli/factory-diagnostics.mjs';
15
17
  import { runDaemonStart } from '../src/cli/daemon-cmd.mjs';
16
18
  import { runSessionStart } from '../src/hooks/session-start.mjs';
@@ -45,6 +47,13 @@ Usage:
45
47
  spotter diagnostics logs [--json] summarize daemon logs for precision diagnostics
46
48
  spotter diagnostics runtime-errors [snapshot|ack|resolve|reopen|compact]
47
49
  consume the local allow-listed aggregate store
50
+ spotter evaluation report [filters] [--json]
51
+ spotter evaluation cases --outcome OUTCOME [filters] [--json]
52
+ spotter evaluation case OBSERVATION_ID [--json]
53
+ read saved proposal-adoption observations
54
+ spotter dashboard device --id ID [--name NAME] [--host HOST] [--port PORT] [--db PATH]
55
+ spotter dashboard hub --config FILE [--host HOST] [--port PORT]
56
+ serve the local device-routed evaluation dashboard
48
57
  spotter codex risk-check --findings FILE
49
58
  run read-only codex-sidecar risk analysis
50
59
  spotter codex review|explore|opinion --findings FILE
@@ -124,6 +133,12 @@ async function main() {
124
133
  }
125
134
  await runDiagnosticsCommand({ argv: rest });
126
135
  return;
136
+ case 'evaluation':
137
+ await runEvaluationCommand({ argv: rest });
138
+ return;
139
+ case 'dashboard':
140
+ await runDashboardCommand({ argv: rest });
141
+ return;
127
142
  case 'daemon': {
128
143
  const sub = rest[0];
129
144
  if (sub === 'start') { await runDaemonStart({ argv: rest.slice(1) }); return; }
@@ -0,0 +1,118 @@
1
+ # Spotter評価dashboard運用
2
+
3
+ ## 固定構成
4
+
5
+ 各端末のdevice serverは`127.0.0.1:53940`だけで待ち受ける。main-serverのhubは
6
+ Docker Caddyから到達できる`172.18.0.1:53940`で待ち受ける。評価DBは各端末の
7
+ `~/.spotter/evaluation.db`をその場で読み、端末外へ複製しない。
8
+
9
+ | 端末 | device ID | main-server側upstream |
10
+ |---|---|---|
11
+ | main-server Ubuntu | `main-server` | `127.0.0.1:53940` |
12
+ | Mac | `mac` | `127.0.0.1:53941` |
13
+ | FOX WSL2 | `fox-wsl` | `127.0.0.1:53942` |
14
+ | FOX Windows native | `fox-windows` | `127.0.0.1:53943` |
15
+
16
+ hub設定の正本は`ops/dashboard/hub-config.json`である。hubは一覧request時に各upstreamの
17
+ healthを1回だけ確認し、端末をonline/offline表示する。常時監視、再試行queue、DB同期は行わない。
18
+
19
+ ## Mac
20
+
21
+ `ops/dashboard/launchd/`の2 plistを`~/Library/LaunchAgents/`へ配置する。既存labelを更新する時は
22
+ `launchctl bootout gui/$(id -u) <plist>`の後、`launchctl bootstrap gui/$(id -u) <plist>`を実行する。
23
+ deviceはMac実DB、tunnelはmain-serverの`53941`へ接続する。
24
+
25
+ 確認:
26
+
27
+ ```sh
28
+ curl --fail http://127.0.0.1:53940/_spotter/health
29
+ ssh main-server curl --fail http://127.0.0.1:53941/_spotter/health
30
+ ```
31
+
32
+ ## main-server Ubuntu
33
+
34
+ `ops/dashboard/systemd/spotter-dashboard-device.service`と
35
+ `spotter-dashboard-hub.service`を`~/.config/systemd/user/`へ配置する。
36
+ `~/.config/spotter/dashboard-device.env`は次の2行、hub configはrepo正本をコピーする。
37
+
38
+ ```ini
39
+ SPOTTER_DEVICE_ID=main-server
40
+ SPOTTER_DEVICE_NAME=main-server
41
+ ```
42
+
43
+ ```sh
44
+ systemctl --user daemon-reload
45
+ systemctl --user enable --now spotter-dashboard-device.service spotter-dashboard-hub.service
46
+ curl --fail http://127.0.0.1:53940/_spotter/health
47
+ curl --fail http://172.18.0.1:53940/
48
+ ```
49
+
50
+ systemd user managerの`PATH`から`spotter`が見えない場合だけ、ユーザー環境の実際のnpm binを
51
+ unitの`Environment=PATH=...`として追加する。別の起動経路へfallbackしない。
52
+
53
+ ## FOX WSL2
54
+
55
+ device unitとtunnel unitを`~/.config/systemd/user/`へ配置する。device env:
56
+
57
+ ```ini
58
+ SPOTTER_DEVICE_ID=fox-wsl
59
+ SPOTTER_DEVICE_NAME=FOX-WSL2
60
+ ```
61
+
62
+ tunnel env:
63
+
64
+ ```ini
65
+ SPOTTER_REMOTE_FORWARD=127.0.0.1:53942:127.0.0.1:53940
66
+ SPOTTER_TUNNEL_TARGET=main-server
67
+ ```
68
+
69
+ ```sh
70
+ systemctl --user daemon-reload
71
+ systemctl --user enable --now spotter-dashboard-device.service spotter-dashboard-tunnel.service
72
+ ```
73
+
74
+ ## FOX Windows native
75
+
76
+ 管理者権限は不要。`ops/dashboard/windows/`を固定pathへ配置し、Windows側から解決できる
77
+ main-serverのSSH host名またはIPを明示して次を1回実行する。
78
+
79
+ ```powershell
80
+ .\install-dashboard-tasks.ps1 -MainServer '<main-server-host-or-ip>'
81
+ ```
82
+
83
+ 登録される2 taskはログオン時にdevice serverとreverse tunnelを起動する。確認:
84
+
85
+ ```powershell
86
+ Invoke-RestMethod http://127.0.0.1:53940/_spotter/health
87
+ Get-ScheduledTask -TaskName 'Spotter dashboard *' | Get-ScheduledTaskInfo
88
+ ```
89
+
90
+ ## 公開経路
91
+
92
+ Caddyへ次を追加する。`spotter.kitepon.dev`はcase詳細に会話文脈を含むため、Cloudflare側では
93
+ 同hostname全体をAccess applicationの対象にし、owner emailだけをallowする。
94
+
95
+ ```caddyfile
96
+ spotter.kitepon.dev {
97
+ import cf-origin-tls
98
+ import security-headers-base
99
+ reverse_proxy 172.18.0.1:53940
100
+ }
101
+ ```
102
+
103
+ 既存Cloudflare Tunnelのcatch-all 404より前に、hostname `spotter.kitepon.dev`、service
104
+ `https://caddy:443`、`noTLSVerify: true`、`matchSNItoHost: true`のingressを追加する。
105
+ DNSは同tunnel UUIDの`cfargotunnel.com`へCNAMEする。
106
+
107
+ ## 受入と切り分け
108
+
109
+ 確認順序はdevice loopback、main-serverの各upstream、hub、Caddy、Cloudflare Tunnel、Accessとする。
110
+ 端末がofflineならhub一覧のその端末だけがofflineになる。選択時の502は該当upstreamのserviceまたは
111
+ reverse tunnelを確認する。hubや別端末を再起動する必要はない。
112
+
113
+ 公開受入:
114
+
115
+ 1. 未認証`https://spotter.kitepon.dev/`がCloudflare Accessへredirectされる。
116
+ 2. 認証後の`/`が4端末を表示する。
117
+ 3. online端末のoverview、project/tool内訳、非採用case、case詳細を表示できる。
118
+ 4. 1端末を停止しても一覧と他端末が表示でき、停止端末だけoffline/502になる。
@@ -0,0 +1 @@
1
+ {"devices":[{"id":"main-server","name":"main-server","upstream":"http://127.0.0.1:53940"},{"id":"mac","name":"Mac","upstream":"http://127.0.0.1:53941"},{"id":"fox-wsl","name":"FOX WSL2","upstream":"http://127.0.0.1:53942"},{"id":"fox-windows","name":"FOX Windows native","upstream":"http://127.0.0.1:53943"}]}
@@ -0,0 +1,21 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key><string>dev.kitepon.spotter-dashboard-device</string>
6
+ <key>ProgramArguments</key>
7
+ <array>
8
+ <string>/opt/homebrew/bin/spotter</string>
9
+ <string>dashboard</string><string>device</string>
10
+ <string>--id</string><string>mac</string>
11
+ <string>--name</string><string>Mac</string>
12
+ <string>--host</string><string>127.0.0.1</string>
13
+ <string>--port</string><string>53940</string>
14
+ </array>
15
+ <key>RunAtLoad</key><true/>
16
+ <key>KeepAlive</key><true/>
17
+ <key>ThrottleInterval</key><integer>10</integer>
18
+ <key>StandardOutPath</key><string>/Users/kite/.spotter/dashboard-device.log</string>
19
+ <key>StandardErrorPath</key><string>/Users/kite/.spotter/dashboard-device.log</string>
20
+ </dict>
21
+ </plist>
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key><string>dev.kitepon.spotter-dashboard-tunnel</string>
6
+ <key>ProgramArguments</key>
7
+ <array>
8
+ <string>/usr/bin/ssh</string><string>-N</string>
9
+ <string>-o</string><string>BatchMode=yes</string>
10
+ <string>-o</string><string>ExitOnForwardFailure=yes</string>
11
+ <string>-o</string><string>ServerAliveInterval=30</string>
12
+ <string>-o</string><string>ServerAliveCountMax=3</string>
13
+ <string>-o</string><string>TCPKeepAlive=yes</string>
14
+ <string>-R</string><string>127.0.0.1:53941:127.0.0.1:53940</string>
15
+ <string>main-server</string>
16
+ </array>
17
+ <key>RunAtLoad</key><true/>
18
+ <key>KeepAlive</key><true/>
19
+ <key>ThrottleInterval</key><integer>10</integer>
20
+ <key>StandardOutPath</key><string>/Users/kite/.spotter/dashboard-tunnel.log</string>
21
+ <key>StandardErrorPath</key><string>/Users/kite/.spotter/dashboard-tunnel.log</string>
22
+ </dict>
23
+ </plist>
@@ -0,0 +1,13 @@
1
+ [Unit]
2
+ Description=Spotter evaluation dashboard device server
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ EnvironmentFile=%h/.config/spotter/dashboard-device.env
8
+ ExecStart=/usr/bin/env spotter dashboard device --id ${SPOTTER_DEVICE_ID} --name ${SPOTTER_DEVICE_NAME} --host 127.0.0.1 --port 53940
9
+ Restart=on-failure
10
+ RestartSec=10
11
+
12
+ [Install]
13
+ WantedBy=default.target
@@ -0,0 +1,12 @@
1
+ [Unit]
2
+ Description=Spotter evaluation dashboard hub
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ ExecStart=/usr/bin/env spotter dashboard hub --config %h/.config/spotter/dashboard-hub.json --host 172.18.0.1 --port 53940
8
+ Restart=on-failure
9
+ RestartSec=10
10
+
11
+ [Install]
12
+ WantedBy=default.target
@@ -0,0 +1,14 @@
1
+ [Unit]
2
+ Description=Spotter evaluation dashboard reverse tunnel
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ EnvironmentFile=%h/.config/spotter/dashboard-tunnel.env
9
+ ExecStart=/usr/bin/ssh -N -o BatchMode=yes -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o TCPKeepAlive=yes -R ${SPOTTER_REMOTE_FORWARD} ${SPOTTER_TUNNEL_TARGET}
10
+ Restart=on-failure
11
+ RestartSec=10
12
+
13
+ [Install]
14
+ WantedBy=default.target
@@ -0,0 +1,16 @@
1
+ param([Parameter(Mandatory = $true)][string]$MainServer)
2
+ $ErrorActionPreference = 'Stop'
3
+ $ops = Split-Path -Parent $MyInvocation.MyCommand.Path
4
+ $powershell = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe"
5
+ $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited
6
+ $settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
7
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
8
+
9
+ $deviceAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-device.ps1`""
10
+ Register-ScheduledTask -TaskName 'Spotter dashboard device' -Action $deviceAction -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
11
+
12
+ $tunnelAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-tunnel.ps1`" -MainServer `"$MainServer`""
13
+ Register-ScheduledTask -TaskName 'Spotter dashboard tunnel' -Action $tunnelAction -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
14
+
15
+ Start-ScheduledTask -TaskName 'Spotter dashboard device'
16
+ Start-ScheduledTask -TaskName 'Spotter dashboard tunnel'
@@ -0,0 +1,4 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ $spotter = Join-Path $env:APPDATA 'npm\spotter.cmd'
3
+ & $spotter dashboard device --id fox-windows --name 'FOX Windows native' --host 127.0.0.1 --port 53940
4
+ exit $LASTEXITCODE
@@ -0,0 +1,7 @@
1
+ param([Parameter(Mandatory = $true)][string]$MainServer)
2
+ $ErrorActionPreference = 'Stop'
3
+ & "$env:WINDIR\System32\OpenSSH\ssh.exe" -N `
4
+ -o BatchMode=yes -o ExitOnForwardFailure=yes `
5
+ -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o TCPKeepAlive=yes `
6
+ -R '127.0.0.1:53943:127.0.0.1:53940' $MainServer
7
+ exit $LASTEXITCODE
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.4.28",
3
+ "version": "1.5.0",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,8 @@
11
11
  },
12
12
  "scripts": {
13
13
  "test": "node --test",
14
+ "verify:release-commit": "node scripts/verify-release-commit.mjs",
15
+ "prepublishOnly": "npm run verify:release-commit && npm test",
14
16
  "postinstall": "node scripts/postinstall.mjs",
15
17
  "preuninstall": "node scripts/preuninstall.mjs"
16
18
  },
@@ -23,7 +25,10 @@
23
25
  "audit",
24
26
  "spotter"
25
27
  ],
26
- "author": "kitepon",
28
+ "author": {
29
+ "name": "Quo / クオ at kitepon.dev",
30
+ "url": "https://kitepon.dev/"
31
+ },
27
32
  "license": "MIT",
28
33
  "repository": {
29
34
  "type": "git",
@@ -43,6 +48,8 @@
43
48
  "bin",
44
49
  "src",
45
50
  "scripts",
51
+ "ops/dashboard",
52
+ "docs/11_dashboard-operations.md",
46
53
  "README.md",
47
54
  "LICENSE",
48
55
  "CHANGELOG.md"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ // publishする成果が、既定ブランチへ着地済みのcommitから作られていることを保証する。
4
+ // 着地していないbranchからpublishすると、そのbranchが取り残された時点で公開物が
5
+ // 後続releaseから消え、統合契約だけが存在しない面を指し続ける(0.3.0の孤児releaseが実例)。
6
+
7
+ import assert from "node:assert/strict";
8
+ import { spawnSync } from "node:child_process";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const scriptPath = fileURLToPath(import.meta.url);
13
+ const defaultProjectDirectory = path.dirname(path.dirname(scriptPath));
14
+
15
+ export function verifyReleaseCommit({
16
+ projectDirectory = defaultProjectDirectory,
17
+ spawn = spawnSync,
18
+ } = {}) {
19
+ const git = (...args) => {
20
+ const result = spawn("git", args, { cwd: projectDirectory, encoding: "utf8" });
21
+ return { ok: result.status === 0, stdout: (result.stdout ?? "").trim() };
22
+ };
23
+
24
+ const head = git("rev-parse", "HEAD");
25
+ assert.ok(head.ok, "git HEAD を解決できません");
26
+ const originHead = git("symbolic-ref", "--quiet", "refs/remotes/origin/HEAD");
27
+ const defaultRef = originHead.ok && originHead.stdout
28
+ ? originHead.stdout.replace("refs/remotes/", "")
29
+ : "origin/main";
30
+ const defaultResolved = git("rev-parse", "--verify", `${defaultRef}^{commit}`);
31
+ assert.ok(defaultResolved.ok, `既定ブランチ ${defaultRef} を解決できません。git fetch origin を先に実行してください。`);
32
+ const isAncestor = git("merge-base", "--is-ancestor", head.stdout, defaultRef);
33
+ assert.ok(
34
+ isAncestor.ok,
35
+ `publish対象 ${head.stdout.slice(0, 12)} が ${defaultRef} の祖先ではありません。`
36
+ + " 先に既定ブランチへ着地させてpushしてから publish してください。"
37
+ );
38
+ const dirty = git("status", "--porcelain", "--untracked-files=normal");
39
+ assert.ok(dirty.ok, "working treeの状態を確認できません");
40
+ assert.equal(
41
+ dirty.stdout,
42
+ "",
43
+ `working treeに未commitの変更があります。publish対象commitとpayloadが一致しません:\n${dirty.stdout}`
44
+ );
45
+ return `release commit ${head.stdout.slice(0, 12)} is landed on ${defaultRef}.`;
46
+ }
47
+
48
+ if (process.argv[1] && path.resolve(process.argv[1]) === scriptPath) {
49
+ console.log(verifyReleaseCommit());
50
+ }
@@ -0,0 +1,48 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import test from "node:test";
7
+
8
+ import { verifyReleaseCommit } from "./verify-release-commit.mjs";
9
+
10
+ function git(cwd, ...args) {
11
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
12
+ }
13
+
14
+ async function createReleaseFixture(t) {
15
+ const root = await mkdtemp(path.join(tmpdir(), "spotter-release-gate-"));
16
+ const remote = path.join(root, "origin.git");
17
+ const work = path.join(root, "work");
18
+ t.after(async () => rm(root, { recursive: true, force: true }));
19
+ await mkdir(work);
20
+ git(root, "init", "--bare", remote);
21
+ git(work, "init", "--initial-branch=main");
22
+ git(work, "config", "user.name", "Spotter release gate test");
23
+ git(work, "config", "user.email", "spotter-release-gate@example.invalid");
24
+ await writeFile(path.join(work, ".gitignore"), "ignored.tmp\n", "utf8");
25
+ await writeFile(path.join(work, "tracked.txt"), "tracked\n", "utf8");
26
+ git(work, "add", ".gitignore", "tracked.txt");
27
+ git(work, "commit", "-m", "fixture");
28
+ git(work, "remote", "add", "origin", remote);
29
+ git(work, "push", "--set-upstream", "origin", "main");
30
+ return work;
31
+ }
32
+
33
+ test("release gate passes for a commit landed on main", async (t) => {
34
+ const work = await createReleaseFixture(t);
35
+ assert.match(verifyReleaseCommit({ projectDirectory: work }), /landed on origin\/main/);
36
+ });
37
+
38
+ test("release gate rejects an untracked publish payload", async (t) => {
39
+ const work = await createReleaseFixture(t);
40
+ await writeFile(path.join(work, "untracked.txt"), "must block publish\n", "utf8");
41
+ assert.throws(() => verifyReleaseCommit({ projectDirectory: work }), /working treeに未commitの変更があります/);
42
+ });
43
+
44
+ test("release gate permits ignored generated output", async (t) => {
45
+ const work = await createReleaseFixture(t);
46
+ await writeFile(path.join(work, "ignored.tmp"), "generated\n", "utf8");
47
+ assert.match(verifyReleaseCommit({ projectDirectory: work }), /landed on origin\/main/);
48
+ });