Skip to content

Commit 524dc4a

Browse files
committed
Add long-running Plannotator daemon runtime
Single daemon process per machine manages session lifecycle, serves browser UIs at /s/<sessionId>, and exposes control APIs. CLI commands auto-start the daemon and create sessions via HTTP. Includes session store, auth tokens, lock files, and event broadcasting.
1 parent 3e46189 commit 524dc4a

64 files changed

Lines changed: 5433 additions & 1437 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,22 +170,29 @@ jobs:
170170
local ok=0
171171
172172
for _ in $(seq 1 60); do
173-
if curl -sf "http://127.0.0.1:${port}${endpoint}" -o /dev/null 2>/dev/null; then
174-
ok=1
175-
break
173+
local sessions
174+
sessions="$(curl -sf "http://127.0.0.1:${port}/daemon/sessions" 2>/dev/null || true)"
175+
if [ -n "$sessions" ]; then
176+
local session_url
177+
session_url="$(python3 -c 'import json,sys; data=json.load(sys.stdin); sessions=data.get("sessions") or []; print(sessions[0].get("url","") if sessions else "")' <<< "$sessions")"
178+
if [ -n "$session_url" ] && curl -sf "${session_url}${endpoint}" -o /dev/null 2>/dev/null; then
179+
ok=1
180+
break
181+
fi
176182
fi
177183
sleep 0.5
178184
done
179185
180186
kill "$pid" 2>/dev/null || true
181187
wait "$pid" 2>/dev/null || true
188+
PLANNOTATOR_PORT="$port" "$BINARY" daemon stop >/dev/null 2>&1 || true
182189
183190
if [ "$ok" = "0" ]; then
184-
echo "FAIL: ${label} did not respond on :${port}${endpoint}"
191+
echo "FAIL: ${label} did not expose a daemon-scoped ${endpoint}"
185192
exit 1
186193
fi
187194
188-
echo "OK: ${label} responded on :${port}${endpoint}"
195+
echo "OK: ${label} exposed daemon-scoped ${endpoint}"
189196
}
190197
191198
# 2. review: exercises server startup, bundled HTML, git diff, and HTTP.
@@ -232,9 +239,14 @@ jobs:
232239
try {
233240
for ($i = 0; $i -lt 60; $i++) {
234241
try {
235-
Invoke-WebRequest -Uri "http://127.0.0.1:$Port$Endpoint" -UseBasicParsing -TimeoutSec 1 | Out-Null
236-
$ok = $true
237-
break
242+
$sessionsResponse = Invoke-WebRequest -Uri "http://127.0.0.1:$Port/daemon/sessions" -UseBasicParsing -TimeoutSec 1
243+
$sessionsBody = $sessionsResponse.Content | ConvertFrom-Json
244+
if ($sessionsBody.sessions.Count -gt 0) {
245+
$sessionUrl = $sessionsBody.sessions[0].url
246+
Invoke-WebRequest -Uri "$sessionUrl$Endpoint" -UseBasicParsing -TimeoutSec 1 | Out-Null
247+
$ok = $true
248+
break
249+
}
238250
} catch {
239251
if ($process.HasExited) {
240252
break
@@ -247,6 +259,7 @@ jobs:
247259
Stop-Process -Id $process.Id -Force
248260
Wait-Process -Id $process.Id -ErrorAction SilentlyContinue
249261
}
262+
& $binary daemon stop *> $null
250263
Remove-Item Env:\PLANNOTATOR_PORT -ErrorAction SilentlyContinue
251264
}
252265
@@ -255,10 +268,10 @@ jobs:
255268
Get-Content $stdout -ErrorAction SilentlyContinue
256269
Write-Host "stderr:"
257270
Get-Content $stderr -ErrorAction SilentlyContinue
258-
throw "FAIL: $Label did not respond on :$Port$Endpoint"
271+
throw "FAIL: $Label did not expose a daemon-scoped $Endpoint"
259272
}
260273
261-
Write-Host "OK: $Label responded on :$Port$Endpoint"
274+
Write-Host "OK: $Label exposed daemon-scoped $Endpoint"
262275
}
263276
264277
# 2. review: exercises server startup, bundled HTML, git diff, and HTTP.

AGENTS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ plannotator/
4444
│ │ ├── index.ts # startPlannotatorServer(), handleServerReady()
4545
│ │ ├── review.ts # startReviewServer(), handleReviewServerReady()
4646
│ │ ├── annotate.ts # startAnnotateServer(), handleAnnotateServerReady()
47+
│ │ ├── daemon/ # Long-running daemon runtime, state, client, and session store
4748
│ │ ├── storage.ts # Re-exports from @plannotator/shared/storage
4849
│ │ ├── share-url.ts # Server-side share URL generation for remote sessions
4950
│ │ ├── remote.ts # isRemoteSession(), getServerPort()
@@ -99,6 +100,8 @@ Plannotator has one server implementation:
99100

100101
Claude Code runs this server through the released `plannotator` binary entrypoint. OpenCode and Pi do not package their own server implementations; they call the same binary through the plugin protocol in `packages/shared/plugin-protocol.ts`. Runtime-agnostic logic (store, validation, types) lives in `packages/shared/`.
101102

103+
Daemon-backed commands run through one long-running `plannotator` process per user/machine environment. `plannotator daemon start|status|stop` manage that lifecycle, while normal plan/review/annotate/archive commands auto-start a compatible daemon and create session-scoped browser URLs at `/s/<sessionId>`. Browser API calls must use `/s/<sessionId>/api/...`; root `/api/...` routes are not a daemon session boundary.
104+
102105
## Installation
103106

104107
**Via plugin marketplace** (when repo is public):
@@ -216,6 +219,24 @@ During normal plan review, an Archive sidebar tab provides the same browsing via
216219

217220
## Server API
218221

222+
### Daemon Runtime (`packages/server/daemon/`)
223+
224+
The daemon is the single long-running Bun server used by normal plan/review/annotate/archive commands. It owns a session store and exposes browser sessions at `/s/<sessionId>`. Session browser APIs are scoped under `/s/<sessionId>/api/...`; root `/api/...` is not a valid daemon session API boundary.
225+
226+
| Endpoint | Method | Purpose |
227+
| --------------------- | ------ | ------------------------------------------ |
228+
| `/daemon/capabilities` | GET | Return daemon protocol/capability metadata |
229+
| `/daemon/status` | GET | Return daemon process, endpoint, and session counts |
230+
| `/daemon/sessions` | GET | List active sessions (`?clean=1` also reaps expired sessions before listing) |
231+
| `/daemon/sessions` | POST | Create a plan/review/annotate/archive session from a plugin-protocol request |
232+
| `/daemon/sessions/:id` | GET | Fetch a session summary |
233+
| `/daemon/sessions/:id/result` | GET | Wait for a session decision/result |
234+
| `/daemon/sessions/:id/cancel` | POST | Cancel a session and dispose its resources |
235+
| `/daemon/sessions/:id` | DELETE | Delete a session record |
236+
| `/daemon/shutdown` | POST | Ask the daemon to stop |
237+
| `/s/:id` | GET | Serve the browser HTML for a session |
238+
| `/s/:id/api/...` | Any | Route browser API requests to that session's plan/review/annotate handler |
239+
219240
### Plan Server (`packages/server/index.ts`)
220241

221242
| Endpoint | Method | Purpose |

apps/hook/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ curl -fsSL https://plannotator.ai/install.cmd -o install.cmd && install.cmd && d
2323

2424
Released binaries ship with SHA256 sidecars and [SLSA build provenance](https://slsa.dev/) attestations from v0.17.2 onwards. See the [installation docs](https://plannotator.ai/docs/getting-started/installation/#verifying-your-install) for version pinning and verification commands.
2525

26-
The released binary owns Plannotator's browser server runtime for Claude Code, OpenCode, and Pi. See [Single Binary Runtime](../../docs/single-binary-runtime.md) for the plugin client boundary and daemon-next design.
26+
The released binary owns Plannotator's browser server runtime for Claude Code, OpenCode, and Pi. See [Single Binary Runtime](../../docs/single-binary-runtime.md) for the plugin client boundary and daemon runtime design.
2727

2828
---
2929

@@ -84,6 +84,19 @@ When Claude Code calls `ExitPlanMode`, this hook intercepts and:
8484
| `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
8585
| `PLANNOTATOR_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. |
8686

87+
## Daemon Runtime
88+
89+
Plan, review, annotate, and archive sessions are created through one long-running `plannotator` daemon. Normal commands auto-start a compatible daemon when needed.
90+
91+
```bash
92+
plannotator daemon status
93+
plannotator daemon stop
94+
plannotator daemon start
95+
plannotator sessions
96+
```
97+
98+
`daemon status` reports the daemon PID, endpoint, protocol version, and active session count. If the running daemon was started with different remote/port settings, stop it and retry with the desired `PLANNOTATOR_REMOTE` / `PLANNOTATOR_PORT` values.
99+
87100
## Remote / Devcontainer Usage
88101

89102
When running Claude Code in a remote environment (SSH, devcontainer, WSL), set `PLANNOTATOR_REMOTE=1` (or `true`) and these environment variables:

apps/hook/server/cli.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("CLI top-level help", () => {
2323
expect(output).toContain("plannotator [--browser <name>]");
2424
expect(output).toContain("plannotator review [--git] [PR_URL]");
2525
expect(output).toContain("plannotator annotate <file.md | file.html | https://... | folder/>");
26+
expect(output).toContain("plannotator daemon start|status|stop");
2627
expect(output).toContain("plannotator plugin capabilities");
2728
expect(output).toContain("running 'plannotator' without arguments is for hook integration");
2829
});
@@ -57,6 +58,7 @@ describe("interactive no-arg invocation", () => {
5758
expect(output).toContain("It expects hook JSON on stdin.");
5859
expect(output).toContain("plannotator review");
5960
expect(output).toContain("plannotator sessions");
61+
expect(output).toContain("plannotator daemon status");
6062
expect(output).toContain("plannotator plugin capabilities");
6163
expect(output).toContain("Run 'plannotator --help' for top-level usage.");
6264
});

apps/hook/server/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function formatTopLevelHelp(): string {
3030
" plannotator last",
3131
" plannotator archive",
3232
" plannotator sessions",
33+
" plannotator daemon start|status|stop",
3334
" plannotator improve-context",
3435
" plannotator plugin capabilities",
3536
"",
@@ -49,6 +50,7 @@ export function formatInteractiveNoArgClarification(): string {
4950
" plannotator last",
5051
" plannotator archive",
5152
" plannotator sessions",
53+
" plannotator daemon status",
5254
" plannotator plugin capabilities",
5355
"",
5456
"Run 'plannotator --help' for top-level usage.",

0 commit comments

Comments
 (0)