diff --git a/CHANGELOG.md b/CHANGELOG.md index a7fe4e3..6362a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ 各節はそのversion公開時点の変更記録であり、後続versionにより置換された仕様を含む。 現行runtime契約は[`docs/00_overview.md`](https://github.com/kitepon/Spotter/blob/main/docs/00_overview.md)から辿る。 +## 1.9.1 — 2026-10-03 + +- runtime-error定期送信のlaunchd/systemd雛形を、NodeとCLI本体を絶対pathで起動する形へ修正。launchdの既定PATHにNodeが無く + `env node` shebangがexit 127で失敗する問題と、systemd user unitが導入時と異なるsystem Nodeを選ぶ問題を解消する。 +- Windows Task Schedulerへ1時間ごとの送信taskをconsole非表示で登録するinstallerを追加。 + ## 1.9.0 — 2026-10-03 - Spotter自身が端末内runtime-errorの累計をBugHubへ送るopt-in CLIを追加。署名済み受領だけでackし、既定では通信しない。 diff --git a/README.ja.md b/README.ja.md index 117f25a..2cfd8a7 100644 --- a/README.ja.md +++ b/README.ja.md @@ -314,8 +314,13 @@ BugHub担当が別途、端末ごとの合鍵を`~/.config/bughub/product-creden `spotter runtime-errors report`を実行し、継続時はlaunchd、systemd user timer、Windows Task Schedulerで 同じコマンドを1時間に1回起動します。未受領だけを送信し、署名を検証した200応答だけでackします。 配布物の`ops/runtime-reporting/`にlaunchdとsystemd user timerの雛形があります。 -`REPLACE_WITH_ABSOLUTE_SPOTTER_PATH`を導入済みCLIの絶対pathへ替えてから登録します。 -Windowsでは同じ利用者のTask Schedulerへ`spotter.cmd runtime-errors report`を1時間ごとに登録します。 +登録前に、`REPLACE_WITH_ABSOLUTE_NODE_PATH`をSpotterを導入したNode.jsの絶対path(`command -v node`)へ、 +`REPLACE_WITH_ABSOLUTE_SPOTTER_SCRIPT_PATH`を導入済みの`claude-spotter/bin/spotter.mjs`の絶対path +(`realpath "$(command -v spotter)"`)へ替えます。service managerはlogin shellのPATHを読みません。 +launchdの既定PATHにはHomebrewやnvmのdirectoryが無く、CLIの`env node` shebangは起動に失敗します。 +systemd user unitは別のsystem Nodeを選ぶことがあります。そのため雛形はNodeを直接起動します。 +Windowsでは同じ利用者で、導入済みpackageの`ops\runtime-reporting\windows\install-runtime-errors-task.ps1`を +実行します。1時間ごとの`spotter-runtime-errors` taskを、console windowを隠して登録します。 422が返った版は再送を止め、修正版を導入すると再試行します。無効化は`reporting.enabled`を`false`にします。 401/403で拒まれた合鍵は入れ替わるまで再送を止めます。時計ずれはackせず、時刻修正後に再試行します。 diff --git a/README.md b/README.md index cfb924a..d29a69c 100644 --- a/README.md +++ b/README.md @@ -343,9 +343,14 @@ BugHub's operator supplies a separate `0600` credential at `~/.config/bughub/product-credentials/spotter.json` (Windows: `%LOCALAPPDATA%\bughub\product-credentials\spotter.json`). Run `spotter runtime-errors report` once, then schedule the same command hourly with launchd, a systemd user timer, or Windows Task Scheduler. -The package includes launchd and systemd templates in `ops/runtime-reporting/`; replace -`REPLACE_WITH_ABSOLUTE_SPOTTER_PATH` with the installed CLI path before registering them. On Windows, -register an hourly task for the installed `spotter.cmd runtime-errors report` under the same user account. +The package includes launchd and systemd templates in `ops/runtime-reporting/`. Before registering them, +replace `REPLACE_WITH_ABSOLUTE_NODE_PATH` with the Node.js binary that installed Spotter (`command -v node`) +and `REPLACE_WITH_ABSOLUTE_SPOTTER_SCRIPT_PATH` with the installed `claude-spotter/bin/spotter.mjs` +(`realpath "$(command -v spotter)"`). The templates start Node directly because a service manager does not +read the login shell's PATH: launchd's default PATH has no Homebrew or nvm directory, so the CLI's +`env node` shebang fails there, and a systemd user unit may resolve a different system Node. On Windows, +run `ops\runtime-reporting\windows\install-runtime-errors-task.ps1` from the installed package as the +same user; it registers the hourly `spotter-runtime-errors` task with a hidden console window. Only unacknowledged records are sent. A signed 200 response advances the cursor; timeout, 5xx, or an unverified response leaves it pending. A 422 blocks further sends for that installed version until a corrected Spotter version is installed; the block marker is under the private `~/.spotter` state directory diff --git a/ops/runtime-reporting/dev.spotter.runtime-errors.plist b/ops/runtime-reporting/dev.spotter.runtime-errors.plist index f519cfd..f977bef 100644 --- a/ops/runtime-reporting/dev.spotter.runtime-errors.plist +++ b/ops/runtime-reporting/dev.spotter.runtime-errors.plist @@ -3,7 +3,8 @@ Labeldev.spotter.runtime-errors ProgramArguments - REPLACE_WITH_ABSOLUTE_SPOTTER_PATH + REPLACE_WITH_ABSOLUTE_NODE_PATH + REPLACE_WITH_ABSOLUTE_SPOTTER_SCRIPT_PATH runtime-errorsreport StartInterval3600 diff --git a/ops/runtime-reporting/spotter-runtime-errors.service b/ops/runtime-reporting/spotter-runtime-errors.service index 41710b5..d89e3a9 100644 --- a/ops/runtime-reporting/spotter-runtime-errors.service +++ b/ops/runtime-reporting/spotter-runtime-errors.service @@ -3,4 +3,4 @@ Description=Send pending Spotter runtime errors to owner-LAN BugHub [Service] Type=oneshot -ExecStart=REPLACE_WITH_ABSOLUTE_SPOTTER_PATH runtime-errors report +ExecStart=REPLACE_WITH_ABSOLUTE_NODE_PATH REPLACE_WITH_ABSOLUTE_SPOTTER_SCRIPT_PATH runtime-errors report diff --git a/ops/runtime-reporting/windows/install-runtime-errors-task.ps1 b/ops/runtime-reporting/windows/install-runtime-errors-task.ps1 new file mode 100644 index 0000000..5f63565 --- /dev/null +++ b/ops/runtime-reporting/windows/install-runtime-errors-task.ps1 @@ -0,0 +1,11 @@ +$ErrorActionPreference = 'Stop' +$ops = Split-Path -Parent $MyInvocation.MyCommand.Path +$powershell = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" +$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited +$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 5) +$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(5) -RepetitionInterval (New-TimeSpan -Hours 1) + +$action = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ops\spotter-runtime-errors.ps1`"" +Register-ScheduledTask -TaskName 'spotter-runtime-errors' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'Send pending Spotter runtime errors to owner-LAN BugHub hourly' -Force | Out-Null + +Start-ScheduledTask -TaskName 'spotter-runtime-errors' diff --git a/ops/runtime-reporting/windows/spotter-runtime-errors.ps1 b/ops/runtime-reporting/windows/spotter-runtime-errors.ps1 new file mode 100644 index 0000000..bfe55ab --- /dev/null +++ b/ops/runtime-reporting/windows/spotter-runtime-errors.ps1 @@ -0,0 +1,4 @@ +$ErrorActionPreference = 'Stop' +$spotter = Join-Path $env:APPDATA 'npm\spotter.cmd' +& $spotter runtime-errors report +exit $LASTEXITCODE diff --git a/package-lock.json b/package-lock.json index c68a65d..e4fed75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-spotter", - "version": "1.9.0", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-spotter", - "version": "1.9.0", + "version": "1.9.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 266b111..b016148 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-spotter", - "version": "1.9.0", + "version": "1.9.1", "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離", "type": "module", "bin": { diff --git a/test/runtime-reporting-ops.test.mjs b/test/runtime-reporting-ops.test.mjs new file mode 100644 index 0000000..b8043c5 --- /dev/null +++ b/test/runtime-reporting-ops.test.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const read = (path) => readFile(new URL(`../${path}`, import.meta.url), 'utf8'); + +test('systemd report service starts Node by absolute path instead of the spotter env shebang', async () => { + const service = await read('ops/runtime-reporting/spotter-runtime-errors.service'); + assert.match( + service, + /^ExecStart=REPLACE_WITH_ABSOLUTE_NODE_PATH REPLACE_WITH_ABSOLUTE_SPOTTER_SCRIPT_PATH runtime-errors report$/mu, + ); +}); + +test('launchd report agent starts Node by absolute path instead of the spotter env shebang', async () => { + const plist = await read('ops/runtime-reporting/dev.spotter.runtime-errors.plist'); + const args = [...plist.matchAll(/([^<]+)<\/string>/gu)].map((match) => match[1]); + assert.deepEqual(args, [ + 'dev.spotter.runtime-errors', + 'REPLACE_WITH_ABSOLUTE_NODE_PATH', + 'REPLACE_WITH_ABSOLUTE_SPOTTER_SCRIPT_PATH', + 'runtime-errors', + 'report', + ]); +}); + +test('Windows report task keeps the user profile, runs hourly, and hides its console window', async () => { + const installer = await read('ops/runtime-reporting/windows/install-runtime-errors-task.ps1'); + const runner = await read('ops/runtime-reporting/windows/spotter-runtime-errors.ps1'); + assert.match(installer, /-UserId \$env:USERNAME -LogonType Interactive/); + assert.match(installer, /-RepetitionInterval \(New-TimeSpan -Hours 1\)/); + const actions = installer.match(/New-ScheduledTaskAction[^\r\n]+/gu) ?? []; + assert.equal(actions.length, 1); + assert.match(actions[0], /-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass/); + assert.match(actions[0], /spotter-runtime-errors\.ps1/); + assert.match(runner, /runtime-errors report/); + assert.match(runner, /exit \$LASTEXITCODE/); +});