claude-spotter 1.5.10 → 1.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  各節はそのversion公開時点の変更記録であり、後続versionにより置換された仕様を含む。
4
4
  現行runtime契約は[`docs/00_overview.md`](docs/00_overview.md)から辿る。
5
5
 
6
+ ## 1.5.12 — 2026-08-18
7
+
8
+ - **Stop未観測の評価turnを次prompt時に採点して閉じる。** 実測でoutcome_missing 2,091件の94%が
9
+ 「Stop未観測のまま次promptが来てincomplete廃棄」だった(steering・キュー済みprompt・Esc中断で
10
+ prompt:StopはClaude 3:1 / Codex 24:1)。`recordTurn`はopenな前turnを白紙で捨てず、収集済み
11
+ `used_tool_ids`で`adopted` / `not_adopted`へ確定する。`usage_status=incomplete`の印が付いたturn
12
+ だけが従来どおり`outcome_missing`に残る。
13
+ - **A/C率の表示を「提案適合率(上限)」へ改名。** 同一turn内の利用は提案の文脈適合の上限しか
14
+ 示さず、Spotter起因の成果を証明しない。CLI報告は`tool fit rate (upper bound)`、dashboardは
15
+ 「提案適合率(上限)」と表示する。内部schema(`toolAdoptionRate`キー)と集計式は変更しない。
16
+ - 過去に保存済みの`outcome_missing`itemは遡って再採点しない(closeの証拠が残っていないため)。
17
+
18
+ ## 1.5.11 — 2026-08-15
19
+
20
+ - **Windows dashboard taskのconsole windowを隠す。** device serverとreverse tunnelの
21
+ PowerShell actionへ`-NonInteractive -WindowStyle Hidden`を指定し、ログオン時に黒い窓を表示しない。
22
+ - **既存のユーザー実行契約を維持する。** Task Schedulerの`Interactive` principal、APPDATA上の
23
+ npm shim、SSH鍵とknown_hostsのユーザープロファイルは変更せず、起動表示だけを修正する。
24
+ - **非Claude Hookの無出力契約をNode 24でも満たす。** UserPromptSubmitとPreToolUseはunsupported判定後に
25
+ 評価SQLiteを遅延読込し、negative fixtureへ`ExperimentalWarning`を出さない。
26
+
6
27
  ## 1.5.10 — 2026-08-14
7
28
 
8
29
  - **非ClaudeのHook入力を副作用前に無視する。** 非空の`sessionId`と`hookEventName`を持ち、
package/README.ja.md CHANGED
@@ -224,7 +224,7 @@ spotter diagnostics factory
224
224
  spotter diagnostics runtime-errors
225
225
  # opt-in端末内runtime error集計をread-only表示(network送信なし)
226
226
  spotter evaluation report
227
- # 端末内DBからproject横断の提案率・tool採用率を集計
227
+ # 端末内DBからproject横断の提案率・提案適合率(上限)を集計
228
228
  spotter evaluation cases --outcome not-adopted
229
229
  # 提案されたが同じturnで使われなかったtool itemを一覧
230
230
  spotter evaluation case <observation-id>
@@ -253,13 +253,15 @@ spotter uninstall # hook 登録を解除 (~/.spotter は残す)
253
253
 
254
254
  dashboardはlocal-firstで動く。各端末が自身の`~/.spotter/evaluation.db`を読み、hubは固定の
255
255
  端末・upstream対応だけを持つ。評価データをcloud DBへ複製しない。端末画面では
256
- 対象ターン、ツール提案あり、提案ツール数、利用判定済み、実際に使用、判定不能、提案率、採用率、
256
+ 対象ターン、ツール提案あり、提案ツール数、利用判定済み、実際に使用、判定不能、提案率、提案適合率(上限。同一ターン内の利用はSpotter起因を証明しない)、
257
257
  project/tool内訳、非採用case、監査対象request、任意の提案時Throughline証拠を確認できる。
258
258
  health確認は端末一覧request時だけなので、端末がofflineでもbackground監視や
259
259
  retry queueを作らず、その端末だけを切り離せる。
260
260
 
261
261
  4端末のservice、reverse tunnel、Caddy/Cloudflare構成は
262
262
  [docs/11_dashboard-operations.md](docs/11_dashboard-operations.md)を参照。
263
+ Windows同梱のTask Scheduler installerはnpm・SSH用の対話ユーザープロファイルを維持しつつ、
264
+ dashboardの2つのPowerShell actionを非対話・console非表示で起動する。
263
265
 
264
266
  Codex risk dispatch を daemon から非同期に流す場合:
265
267
 
package/README.md CHANGED
@@ -232,7 +232,7 @@ spotter diagnostics factory
232
232
  spotter diagnostics runtime-errors
233
233
  # print the local allow-listed runtime-error aggregate snapshot (no network)
234
234
  spotter evaluation report
235
- # show cross-project proposal/adoption counts and rates from the local DB
235
+ # show cross-project proposal counts and fit (upper-bound) rates from the local DB
236
236
  spotter evaluation cases --outcome not-adopted
237
237
  # list proposed tools that were not used in the same turn
238
238
  spotter evaluation case <observation-id>
@@ -261,14 +261,17 @@ spotter uninstall # remove hooks from this project (leaves ~/.spotter int
261
261
 
262
262
  The dashboard is local-first. Every terminal reads its own `~/.spotter/evaluation.db`; the hub
263
263
  keeps only a static device-to-upstream map and does not copy evaluation data into a cloud database.
264
- The device view shows Japanese labels for every evaluation metric, proposal and adoption rates
265
- with their numerator and denominator, project/tool breakdowns, non-adopted cases, the request
264
+ The device view shows Japanese labels for every evaluation metric, the proposal rate and the
265
+ fit rate (an upper bound: same-turn usage does not prove Spotter caused it) with their
266
+ numerator and denominator, project/tool breakdowns, non-adopted cases, the request
266
267
  audited by Spotter, and optional proposal-time Throughline evidence. The hub checks health only
267
268
  when the device list is requested, so an offline terminal is isolated without a background monitor
268
269
  or retry queue.
269
270
 
270
271
  The reference four-terminal service, reverse-tunnel, and Caddy/Cloudflare layout is documented in
271
272
  [docs/11_dashboard-operations.md](docs/11_dashboard-operations.md).
273
+ On Windows, the bundled Task Scheduler installer keeps the interactive user's profile for npm and
274
+ SSH while starting both dashboard PowerShell actions non-interactively with hidden console windows.
272
275
 
273
276
  Optional async Codex risk dispatch:
274
277
 
@@ -1,7 +1,7 @@
1
1
  # Spotter評価dashboard運用
2
2
 
3
- 現行npm配布版: **v1.5.10**(2026-08-14)。v1.5.10は非Claude Hook入力の副作用前no-op releaseであり、
4
- dashboard routing構成はv1.5.3から変更していない。
3
+ 現行npm配布版: **v1.5.12**(2026-08-18)。v1.5.12は評価の採点方式と表示名
4
+ (提案適合率(上限))を変更し、dashboardのrouting構成は変更していない。
5
5
 
6
6
  この文書はservice設定の正本であり、各端末に現在installされているnpm versionの台帳ではない。
7
7
  端末versionは対象端末で`spotter --version`を実行して確認する。
@@ -86,20 +86,53 @@ systemctl --user enable --now spotter-dashboard-device.service spotter-dashboard
86
86
 
87
87
  ## FOX Windows native
88
88
 
89
- 管理者権限は不要。`ops/dashboard/windows/`を固定pathへ配置し、Windows側から解決できる
90
- main-serverのSSH host名またはIPを明示して次を1回実行する。
89
+ 同梱された`ops/dashboard/windows/`のPowerShellを固定pathへ配置する。npm global版から
90
+ `~/.spotter/dashboard/`へ反映し、Windows側から解決できるmain-serverのSSH host名またはIPを
91
+ 明示してinstallerを実行する。npm更新後も同じ手順で配布scriptとtask定義を更新する。
91
92
 
92
93
  ```powershell
93
- .\install-dashboard-tasks.ps1 -MainServer '<main-server-host-or-ip>'
94
+ $source = Join-Path (npm root -g) 'claude-spotter\ops\dashboard\windows'
95
+ $target = Join-Path $HOME '.spotter\dashboard'
96
+ New-Item -ItemType Directory -Path $target -Force | Out-Null
97
+ Copy-Item -Path (Join-Path $source '*.ps1') -Destination $target -Force
98
+ & (Join-Path $target 'install-dashboard-tasks.ps1') -MainServer '<main-server-host-or-ip>'
94
99
  ```
95
100
 
96
- 登録される2 taskはログオン時にdevice serverとreverse tunnelを起動する。確認:
101
+ 新規taskまたは現在ユーザーが所有するtaskなら管理者権限は不要。既存taskが
102
+ `BUILTIN\Administrators`所有で`Register-ScheduledTask`が`Access is denied`になった場合だけ、
103
+ 同じinstallerを管理者PowerShellから再実行する。principalは`Interactive` / `Limited`のままで、
104
+ APPDATAのnpm shim、SSH鍵、known_hostsを現在ユーザープロファイルから読む。
105
+
106
+ 登録される2 taskはログオン時にdevice serverとreverse tunnelをhidden起動し、console windowを
107
+ 表示しない。task actionに`-NonInteractive -WindowStyle Hidden`が含まれることとdevice healthを確認する。
97
108
 
98
109
  ```powershell
99
110
  Invoke-RestMethod http://127.0.0.1:53944/_spotter/health
100
- Get-ScheduledTask -TaskName 'Spotter dashboard *' | Get-ScheduledTaskInfo
111
+ Get-ScheduledTask -TaskName 'Spotter dashboard *' |
112
+ Select-Object TaskName, State, @{Name='Arguments'; Expression={$_.Actions.Arguments}}
113
+ ```
114
+
115
+ task更新時に旧deviceの子processだけが残ると、healthは旧processから返る一方、新taskはport競合で
116
+ `Ready` / result `2`になる。この組合せを観測した場合だけ、53944 listenerのcommand lineを先に確認する。
117
+
118
+ ```powershell
119
+ $connection = Get-NetTCPConnection -LocalAddress 127.0.0.1 -LocalPort 53944 -State Listen
120
+ $owner = Get-CimInstance Win32_Process -Filter "ProcessId = $($connection.OwningProcess)"
121
+ $owner | Select-Object ProcessId, ParentProcessId, Name, CommandLine
101
122
  ```
102
123
 
124
+ `CommandLine`が`claude-spotter`の`dashboard device`かつtaskが`Ready`であることを確認できた時だけ、
125
+ その旧PIDを停止してtaskを起動し直す。名前だけで全Node processを停止しない。
126
+
127
+ ```powershell
128
+ Stop-Process -Id $owner.ProcessId
129
+ Start-ScheduledTask -TaskName 'Spotter dashboard device'
130
+ Invoke-RestMethod http://127.0.0.1:53944/_spotter/health
131
+ ```
132
+
133
+ v1.5.11のFOX Windows native実測は
134
+ [`evidence/dashboard-windows-hidden-v1.5.11.md`](evidence/dashboard-windows-hidden-v1.5.11.md)に記録する。
135
+
103
136
  ## 公開経路
104
137
 
105
138
  Caddyへ次を追加する。`spotter.kitepon.dev`はcase詳細に会話文脈を含むため、Cloudflare側では
@@ -6,10 +6,10 @@ $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interac
6
6
  $settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
7
7
  $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
8
8
 
9
- $deviceAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-device.ps1`""
9
+ $deviceAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-device.ps1`""
10
10
  Register-ScheduledTask -TaskName 'Spotter dashboard device' -Action $deviceAction -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
11
11
 
12
- $tunnelAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-tunnel.ps1`" -MainServer `"$MainServer`""
12
+ $tunnelAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-tunnel.ps1`" -MainServer `"$MainServer`""
13
13
  Register-ScheduledTask -TaskName 'Spotter dashboard tunnel' -Action $tunnelAction -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
14
14
 
15
15
  Start-ScheduledTask -TaskName 'Spotter dashboard device'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.5.10",
3
+ "version": "1.5.12",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,7 +68,9 @@ function appendSummary(lines, label, summary, prefix = '') {
68
68
  lines.push(`${prefix}${label}`);
69
69
  lines.push(`${prefix} S=${summary.S} P=${summary.P} I=${summary.I} C=${summary.C} A=${summary.A} M=${summary.M}`);
70
70
  lines.push(`${prefix} proposal rate: ${fraction(summary.P, summary.S)}`);
71
- lines.push(`${prefix} tool adoption rate: ${fraction(summary.A, summary.C)}`);
71
+ // Same-turn usage proves at most that a proposal fit the context; it cannot
72
+ // attribute the usage to Spotter. Label the A/C ratio as an upper bound.
73
+ lines.push(`${prefix} tool fit rate (upper bound): ${fraction(summary.A, summary.C)}`);
72
74
  }
73
75
 
74
76
  function fraction(numerator, denominator) {
@@ -50,12 +50,18 @@ export class EvaluationStore {
50
50
  recordTurn(input) {
51
51
  const turn = normalizeTurn(input);
52
52
  return this.#transaction(() => {
53
+ // A still-open previous turn means its Stop was never observed (steering /
54
+ // queued prompt / Esc interrupt). Grade it with the usage evidence collected
55
+ // so far instead of discarding it: only a turn already marked incomplete
56
+ // keeps outcome_missing.
53
57
  const previous = this.database.prepare(`
54
- SELECT observation_id FROM evaluation_turns
58
+ SELECT observation_id, used_tool_ids, usage_status FROM evaluation_turns
55
59
  WHERE session_id = ? AND completed_at_ms IS NULL
56
60
  ORDER BY recorded_at_ms DESC LIMIT 1
57
61
  `).get(turn.sessionId);
58
- if (previous) this.#close(previous.observation_id, [], 'incomplete', turn.recordedAtMs);
62
+ if (previous) {
63
+ this.#close(previous.observation_id, parseToolIds(previous.used_tool_ids), previous.usage_status, turn.recordedAtMs);
64
+ }
59
65
 
60
66
  this.database.prepare(`
61
67
  INSERT INTO evaluation_turns (
@@ -127,13 +127,13 @@ function renderFilters(filters, action) {
127
127
  function renderRates(summary) {
128
128
  const proposal = rate(metricNumber(summary, 'P'), metricNumber(summary, 'S'));
129
129
  const adoption = rate(metricNumber(summary, 'A'), metricNumber(summary, 'C'));
130
- return `<dl class="rates"><div><dt>提案率<small>ツール提案あり ÷ 対象ターン</small></dt><dd>${escapeHtml(proposal)}</dd></div><div><dt>採用率<small>実際に使用 ÷ 利用判定済み</small></dt><dd>${escapeHtml(adoption)}</dd></div></dl>`;
130
+ return `<dl class="rates"><div><dt>提案率<small>ツール提案あり ÷ 対象ターン</small></dt><dd>${escapeHtml(proposal)}</dd></div><div><dt>提案適合率(上限)<small>実際に使用 ÷ 利用判定済み</small></dt><dd>${escapeHtml(adoption)}</dd></div></dl>`;
131
131
  }
132
132
 
133
133
  function renderBreakdown(id, title, rows) {
134
134
  return `<section aria-labelledby="${id}">
135
135
  <h2 id="${id}">${title}</h2>
136
- ${rows.length === 0 ? '<p class="empty">該当するデータはありません。</p>' : `<div class="table-wrap"><table><thead><tr><th scope="col">項目</th>${METRIC_KEYS.map((key) => `<th scope="col">${METRIC_LABELS[key]}</th>`).join('')}<th scope="col">提案率</th><th scope="col">採用率</th></tr></thead><tbody>${rows.map(([label, summary]) => `<tr><th scope="row">${escapeHtml(label)}</th>${METRIC_KEYS.map((key) => `<td>${escapeHtml(metric(summary, key))}</td>`).join('')}<td>${escapeHtml(rate(metricNumber(summary, 'P'), metricNumber(summary, 'S')))}</td><td>${escapeHtml(rate(metricNumber(summary, 'A'), metricNumber(summary, 'C')))}</td></tr>`).join('')}</tbody></table></div>`}
136
+ ${rows.length === 0 ? '<p class="empty">該当するデータはありません。</p>' : `<div class="table-wrap"><table><thead><tr><th scope="col">項目</th>${METRIC_KEYS.map((key) => `<th scope="col">${METRIC_LABELS[key]}</th>`).join('')}<th scope="col">提案率</th><th scope="col">提案適合率(上限)</th></tr></thead><tbody>${rows.map(([label, summary]) => `<tr><th scope="row">${escapeHtml(label)}</th>${METRIC_KEYS.map((key) => `<td>${escapeHtml(metric(summary, key))}</td>`).join('')}<td>${escapeHtml(rate(metricNumber(summary, 'P'), metricNumber(summary, 'S')))}</td><td>${escapeHtml(rate(metricNumber(summary, 'A'), metricNumber(summary, 'C')))}</td></tr>`).join('')}</tbody></table></div>`}
137
137
  </section>`;
138
138
  }
139
139
 
@@ -17,7 +17,6 @@ import {
17
17
  } from './lib.mjs';
18
18
  import { sendRequest } from '../daemon/transport.mjs';
19
19
  import { canonicalizeToolId } from '../core/evaluation-tool-id.mjs';
20
- import { createEvaluationStore } from '../core/evaluation-store.mjs';
21
20
 
22
21
  const TIMEOUT_MS = 1_000;
23
22
 
@@ -25,7 +24,7 @@ export async function runPreToolUse({
25
24
  readInput = readStdinJson,
26
25
  sendRequestFn = sendRequest,
27
26
  recordHookEventFn = recordClaudeHookEvent,
28
- createEvaluationStoreFn = createEvaluationStore,
27
+ createEvaluationStoreFn = null,
29
28
  writeError = (text) => process.stderr.write(text),
30
29
  } = {}) {
31
30
  if (isChildCall()) return;
@@ -33,6 +32,8 @@ export async function runPreToolUse({
33
32
  if (isUnsupportedNonClaudeEnvelope(input)) return;
34
33
  if (isSubagentCall(input)) return;
35
34
  if (isOutsideSpotterProject(input)) return;
35
+ const evaluationStoreFactory = createEvaluationStoreFn
36
+ ?? (await import('../core/evaluation-store.mjs')).createEvaluationStore;
36
37
 
37
38
  const sessionId = requireString(input, 'session_id');
38
39
  const toolName = requireString(input, 'tool_name');
@@ -45,7 +46,7 @@ export async function runPreToolUse({
45
46
  const invalidateEvaluation = () => {
46
47
  if (!evaluationObserved || evaluationInvalidated) return;
47
48
  evaluationInvalidated = true;
48
- closeEvaluationAsIncomplete({ sessionId, createEvaluationStoreFn, writeError });
49
+ closeEvaluationAsIncomplete({ sessionId, createEvaluationStoreFn: evaluationStoreFactory, writeError });
49
50
  };
50
51
 
51
52
  try {
@@ -27,7 +27,6 @@ import { spawnDaemonAndWaitReady } from './spawn-daemon.mjs';
27
27
  import { discardLegacyPending } from './pending-context.mjs';
28
28
  import { projectBackendFailure, projectParentAdvice, projectToolIds } from './parent-output-projector.mjs';
29
29
  import { randomUUID } from 'node:crypto';
30
- import { createEvaluationStore } from '../core/evaluation-store.mjs';
31
30
  import { loadEvaluationContext } from '../core/evaluation-context.mjs';
32
31
  import { version } from '../version.mjs';
33
32
 
@@ -39,7 +38,7 @@ export async function runUserPrompt({
39
38
  spawnDaemonAndWaitReadyFn = spawnDaemonAndWaitReady,
40
39
  discardLegacyPendingFn = discardLegacyPending,
41
40
  recordHookEventFn = recordClaudeHookEvent,
42
- createEvaluationStoreFn = createEvaluationStore,
41
+ createEvaluationStoreFn = null,
43
42
  loadEvaluationContextFn = loadEvaluationContext,
44
43
  randomUUIDFn = randomUUID,
45
44
  spotterVersion = version,
@@ -52,6 +51,8 @@ export async function runUserPrompt({
52
51
  if (isUnsupportedNonClaudeEnvelope(input)) return;
53
52
  if (isSubagentCall(input)) return;
54
53
  if (isOutsideSpotterProject(input)) return;
54
+ const evaluationStoreFactory = createEvaluationStoreFn
55
+ ?? (await import('../core/evaluation-store.mjs')).createEvaluationStore;
55
56
 
56
57
  const sessionId = requireString(input, 'session_id');
57
58
  const prompt = requireString(input, 'prompt');
@@ -76,7 +77,7 @@ export async function runUserPrompt({
76
77
  recordedAtMs: proposalRecordedAtMs,
77
78
  })
78
79
  : { status: 'not_requested', snapshot: null };
79
- const store = createEvaluationStoreFn();
80
+ const store = evaluationStoreFactory();
80
81
  try {
81
82
  store.recordTurn({
82
83
  observationId,