@chanlerdev/scorel 0.0.1

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 (80) hide show
  1. package/README.md +110 -0
  2. package/dist/index.js +6675 -0
  3. package/dist/index.js.map +7 -0
  4. package/docs/CHANGELOG.md +12 -0
  5. package/docs/README.md +116 -0
  6. package/docs/ROADMAP.md +669 -0
  7. package/docs/SHIP.md +242 -0
  8. package/docs/spec/channels.md +156 -0
  9. package/docs/spec/client.md +326 -0
  10. package/docs/spec/daemon.md +408 -0
  11. package/docs/spec/events.md +423 -0
  12. package/docs/spec/extensions.md +255 -0
  13. package/docs/spec/relay.md +391 -0
  14. package/docs/spec/runtime.md +251 -0
  15. package/docs/spec/session.md +380 -0
  16. package/docs/spec/ship/S0001-docs-baseline.md +41 -0
  17. package/docs/spec/ship/S0002-package-skeleton.md +56 -0
  18. package/docs/spec/ship/S0003-protocol-contracts.md +49 -0
  19. package/docs/spec/ship/S0004-session-core.md +50 -0
  20. package/docs/spec/ship/S0005-runtime-loop.md +48 -0
  21. package/docs/spec/ship/S0006-embedded-daemon-client.md +51 -0
  22. package/docs/spec/ship/S0007-cli-alpha.md +49 -0
  23. package/docs/spec/ship/S0008-coding-tools.md +107 -0
  24. package/docs/spec/ship/S0009-code-discovery-tools.md +82 -0
  25. package/docs/spec/ship/S0010-todo-tool-and-cli.md +81 -0
  26. package/docs/spec/ship/S0011-coding-agent-alpha-smoke.md +110 -0
  27. package/docs/spec/ship/S0012-coding-tools-maturity.md +143 -0
  28. package/docs/spec/ship/S0013-local-daemon-protocol.md +57 -0
  29. package/docs/spec/ship/S0014-local-daemon-lifecycle.md +64 -0
  30. package/docs/spec/ship/S0015-local-attach-and-broadcast.md +58 -0
  31. package/docs/spec/ship/S0016-local-daemon-resync-smoke.md +60 -0
  32. package/docs/spec/ship/S0017-grep-files-output-mode.md +49 -0
  33. package/docs/spec/ship/S0018-daemon-entrypoint-smoke.md +48 -0
  34. package/docs/spec/ship/S0019-remote-transport-contract.md +59 -0
  35. package/docs/spec/ship/S0020-remote-websocket-server.md +56 -0
  36. package/docs/spec/ship/S0021-remote-websocket-client-transport.md +55 -0
  37. package/docs/spec/ship/S0022-remote-daemon-cli-lifecycle.md +60 -0
  38. package/docs/spec/ship/S0023-remote-control-e2e-validation.md +66 -0
  39. package/docs/spec/ship/S0024-remote-attach-interactive-stream.md +49 -0
  40. package/docs/spec/ship/S0025-remote-attach-session-event-view.md +57 -0
  41. package/docs/spec/ship/S0026-attach-project-cache-and-dual-seq-reconnect.md +87 -0
  42. package/docs/spec/ship/S0027-session-diagnostics-log.md +77 -0
  43. package/docs/spec/ship/S0028-client-attach-diagnostics-log.md +70 -0
  44. package/docs/spec/ship/S0029-project-index-for-session-lookup.md +119 -0
  45. package/docs/spec/ship/S0030-webui-product-intent.md +73 -0
  46. package/docs/spec/ship/S0031-daemon-projectslug-rule.md +72 -0
  47. package/docs/spec/ship/S0032-daemon-protocol-completion.md +123 -0
  48. package/docs/spec/ship/S0033-webui-skeleton-routing.md +92 -0
  49. package/docs/spec/ship/S0034-webui-device-settings.md +121 -0
  50. package/docs/spec/ship/S0035-webui-device-handshake.md +83 -0
  51. package/docs/spec/ship/S0036-webui-project-session-sync.md +70 -0
  52. package/docs/spec/ship/S0037-webui-chatbox-v1.md +97 -0
  53. package/docs/spec/ship/S0038-webui-cancel-multiclient.md +65 -0
  54. package/docs/spec/ship/S0039-webui-e2e-newchat.md +74 -0
  55. package/docs/spec/ship/S0040-webui-codex-visual-tokens.md +227 -0
  56. package/docs/spec/ship/S0041-webui-markdown-and-tool-block.md +248 -0
  57. package/docs/spec/ship/S0042-webui-streaming-ux-autoscroll.md +130 -0
  58. package/docs/spec/ship/S0043-startup-ergonomics.md +278 -0
  59. package/docs/spec/ship/S0044-webui-chatbox-rebuild.md +556 -0
  60. package/docs/spec/ship/S0045-webui-card-sidebar-and-session-fixes.md +469 -0
  61. package/docs/spec/ship/S0046-webui-empty-composer-and-lazy-session.md +428 -0
  62. package/docs/spec/ship/S0047-webui-project-hover-newchat-and-dynamic-greeting.md +176 -0
  63. package/docs/spec/ship/S0048-device-level-host-project-registry.md +253 -0
  64. package/docs/spec/ship/S0049-webui-add-project-directory-browser.md +217 -0
  65. package/docs/spec/ship/S0050-instruction-snapshot-and-agents-assembly.md +338 -0
  66. package/docs/spec/ship/S0051-harness-item-and-system-reminder.md +190 -0
  67. package/docs/spec/ship/S0052-follow-up-queue-and-dual-loop.md +195 -0
  68. package/docs/spec/ship/S0053-skill-index-and-skill-tool.md +252 -0
  69. package/docs/spec/ship/S0054-webui-running-message-behavior.md +72 -0
  70. package/docs/spec/ship/S0055-webui-composer-acceptance-and-queue-strip.md +68 -0
  71. package/docs/spec/ship/S0056-relay-and-hosted-webui-contract.md +106 -0
  72. package/docs/spec/ship/S0057-relay-service-protocol-skeleton.md +161 -0
  73. package/docs/spec/ship/S0058-host-outbound-relay-and-pair-command.md +138 -0
  74. package/docs/spec/ship/S0059-relay-transport-and-hosted-webui-connector.md +140 -0
  75. package/docs/spec/ship/S0060-relay-hosted-webui-e2e-validation.md +132 -0
  76. package/docs/spec/ship/S0060-relay-hosted-webui-e2e-validation.verification.md +90 -0
  77. package/docs/spec/ship/S0061-hosted-defaults-and-cli-command-surface.md +208 -0
  78. package/docs/spec/ship/S0062-npm-package-and-release-workflow.md +166 -0
  79. package/docs/spec/tools.md +173 -0
  80. package/package.json +51 -0
@@ -0,0 +1,130 @@
1
+ # S0042: WebUI Streaming UX And Autoscroll
2
+
3
+ ## Goal
4
+
5
+ Polish the streaming chat experience: animated cursor, rAF-batched text-delta integration, IntersectionObserver-driven autoscroll with "Jump to bottom" floating button. Lock the streaming polish without expanding scope to keyboard shortcuts, history, or rewind UI. Builds on S0040 (design tokens) and S0041 (markdown rendering); does not change any rendering content.
6
+
7
+ Final spec of the M5-polish chain.
8
+
9
+ ## Scope
10
+
11
+ ### Animated streaming cursor
12
+
13
+ Replace the v1 single faint `▋` character with a CSS-animated caret.
14
+
15
+ - New file `apps/webui/components/chatbox/streaming-cursor.tsx` (client component) exporting `<StreamingCursor />`.
16
+ - Renders a 1ch wide × 1.1em tall element using `bg-text-muted` (semantic token from S0040), `inline-block`, `align-text-bottom`.
17
+ - CSS keyframes defined in `apps/webui/app/globals.css`:
18
+ ```css
19
+ @keyframes scorel-caret-blink {
20
+ 0%, 50% { opacity: 1; }
21
+ 51%, 100% { opacity: 0; }
22
+ }
23
+ .scorel-caret { animation: scorel-caret-blink 1s steps(2) infinite; }
24
+ ```
25
+ - Reduced-motion respect: `@media (prefers-reduced-motion: reduce)` overrides the animation to `opacity: 1` (cursor still visible, but static).
26
+ - Mounted by `turn-assistant.tsx` as a sibling element when `streaming === true`, **outside** the `MarkdownView` so the parser does not see it.
27
+
28
+ ### rAF-batched text-delta integration
29
+
30
+ Today the session attach controller emits a state snapshot every event; in pathological streams (100+ tokens/sec) that triggers excessive re-parses through markdown.
31
+
32
+ - Add `apps/webui/lib/events/delta-batch.ts` (new):
33
+ ```ts
34
+ export type FlushFn = () => void;
35
+
36
+ export function createRafBatcher(flush: FlushFn): {
37
+ schedule(): void;
38
+ cancel(): void;
39
+ };
40
+ ```
41
+ - Implementation: tracks a pending `requestAnimationFrame` handle; `schedule()` is a no-op if already pending. `cancel()` removes the handle.
42
+ - Integrate into `apps/webui/lib/connection/session.ts`:
43
+ - When event is a `text_delta`, project into projector state but **defer the snapshot emission** to the next animation frame via the batcher.
44
+ - Non-delta events (turn_start, turn_end, message_end, persistent message, error) flush synchronously and cancel any pending batch (so the final state is not delayed by a stale rAF).
45
+ - Browsers that lack `requestAnimationFrame` (none in supported targets) fall back to `setTimeout(fn, 16)` — implement defensively.
46
+ - The batcher must respect document visibility: when the tab is hidden, `requestAnimationFrame` pauses, but `setTimeout` does not. Acceptable v1: deltas accumulate until the tab is visible again. Document.
47
+
48
+ ### Autoscroll + Jump-to-bottom button
49
+
50
+ - Add `apps/webui/components/chatbox/autoscroll-region.tsx` (client component):
51
+ - Wraps `Transcript` content inside a scrollable `<div>` with overflow-y-auto.
52
+ - Renders an invisible 1px-tall sentinel `<div ref={sentinelRef}>` at the very bottom.
53
+ - Uses `IntersectionObserver` watching the sentinel against the scroll container.
54
+ - State: `isAtBottom: boolean` derived from the latest IntersectionObserver entry.
55
+ - Effect: when new turns are appended (track turn count), if `isAtBottom`, programmatically scroll the sentinel into view; if not, do nothing.
56
+ - Renders `<JumpToBottomButton onClick={...} visible={!isAtBottom && transcriptHasContent} />` — fixed position bottom-right of the scroll region (not viewport), with a 12px margin.
57
+ - `JumpToBottomButton` (in same file or sibling): pill-shaped, `bg-surface-raised border border-subtle shadow-md text-muted`, click scrolls sentinel into view smoothly. Show count of unseen new turns since user scrolled away (optional; if implementing, track lastSeenTurnIndex).
58
+ - Replace `apps/webui/components/chatbox/transcript.tsx` autoscroll logic with delegation to `<AutoscrollRegion>`. Transcript becomes a pure presenter that takes `turns` and renders them into the region's children.
59
+
60
+ ### Reduced motion + smooth scroll
61
+
62
+ - Use `scrollIntoView({ behavior: prefers-reduced-motion ? "auto" : "smooth", block: "end" })` for jump.
63
+ - Animated cursor and scroll both check `window.matchMedia("(prefers-reduced-motion: reduce)").matches`.
64
+
65
+ ## Not In Scope
66
+
67
+ - Keyboard shortcuts (Cmd+K, Cmd+B, Cmd+Enter, etc.) — backlog.
68
+ - Composer history (`↑` recall) — backlog.
69
+ - Sidebar collapse persistence — backlog.
70
+ - Tool block specialization — backlog.
71
+ - Dark mode — backlog.
72
+ - Mid-token markdown segmentation / streamdown swap — only triggered if S0041 flicker turns out unacceptable; not in this spec.
73
+ - Search-in-transcript — backlog.
74
+
75
+ ## Acceptance Criteria
76
+
77
+ - Streaming cursor visibly blinks at 1Hz next to the streaming assistant text. Static under reduced-motion preference.
78
+ - During heavy delta load (≥ 50 tokens/sec simulated), markdown re-parses cap at one per animation frame, observable via `console.time` instrumentation in dev. (Not asserted automatically; PR description includes a profile screenshot.)
79
+ - Final `assistant_message` arrival flushes any pending rAF batch synchronously; the final transcript matches the streamed transcript without a render gap.
80
+ - Scrolling up while streaming pauses autoscroll; new tokens still append but viewport stays put.
81
+ - "Jump to bottom" pill appears when user scrolls away from the bottom and disappears when bottom is reached again.
82
+ - Smooth scroll engaged unless `prefers-reduced-motion: reduce`.
83
+ - No regression in S0041 markdown rendering, S0040 design tokens, or S0038 cancel UX.
84
+ - `apps/webui/src/package-boundaries.test.ts` unchanged (no new externals).
85
+ - `pnpm --filter @scorel/app-webui typecheck && pnpm --filter @scorel/app-webui test` passes.
86
+ - `pnpm --filter @scorel/app-webui build` succeeds.
87
+ - Repo-level `pnpm typecheck && pnpm test` passes.
88
+ - Manual: stream a long assistant reply; verify cursor blink, smooth append, scroll-up pauses follow, jump button appears, click jumps back.
89
+ - M5 polish phase status flipped to **Done** in `docs/ROADMAP.md` (this is the closing spec of the polish chain).
90
+
91
+ ## Tests
92
+
93
+ - `apps/webui/lib/events/delta-batch.test.ts` (new): schedule coalesces; cancel clears handle; flush executes once per frame; setTimeout fallback path.
94
+ - `apps/webui/lib/connection/session.test.ts` (extend):
95
+ - Burst of three `text_delta` events results in one snapshot emission per frame, not three.
96
+ - Subsequent `turn_end` flushes synchronously.
97
+ - `apps/webui/components/chatbox/streaming-cursor.test.tsx` (new): renders with class; `prefers-reduced-motion` query disables animation class.
98
+ - `apps/webui/components/chatbox/autoscroll-region.test.tsx` (new):
99
+ - Mock IntersectionObserver; assert `isAtBottom` toggles.
100
+ - Adding a new child while `isAtBottom` triggers `scrollIntoView` on sentinel; while not at bottom, no scroll call.
101
+ - JumpToBottomButton visibility binding.
102
+ - `apps/webui/components/chatbox/transcript.test.tsx` (extend): delegation works (existing tests should still pass with the wrapper).
103
+ - Manual smoke: stream 200-turn fake transcript; profile parse cadence; verify scroll behavior.
104
+
105
+ ## Affected Paths
106
+
107
+ - `apps/webui/components/chatbox/streaming-cursor.tsx` (new)
108
+ - `apps/webui/components/chatbox/streaming-cursor.test.tsx` (new)
109
+ - `apps/webui/components/chatbox/autoscroll-region.tsx` (new)
110
+ - `apps/webui/components/chatbox/autoscroll-region.test.tsx` (new)
111
+ - `apps/webui/components/chatbox/transcript.tsx`
112
+ - `apps/webui/components/chatbox/transcript.test.tsx`
113
+ - `apps/webui/components/chatbox/turn-assistant.tsx` (mount cursor when streaming)
114
+ - `apps/webui/components/chatbox/turn-assistant.test.tsx`
115
+ - `apps/webui/lib/events/delta-batch.ts` (new)
116
+ - `apps/webui/lib/events/delta-batch.test.ts` (new)
117
+ - `apps/webui/lib/connection/session.ts`
118
+ - `apps/webui/lib/connection/session.test.ts`
119
+ - `apps/webui/app/globals.css` (cursor keyframes + reduced-motion)
120
+ - `docs/ROADMAP.md` — flip S0042 row + M5 polish stage to Done
121
+
122
+ ## Risks And Boundaries
123
+
124
+ - **rAF in hidden tabs**: `requestAnimationFrame` pauses; deltas accumulate until visibility returns. If this causes "snap" on resume, follow-up spec can fall back to `setTimeout` when `document.hidden`. Document.
125
+ - **IntersectionObserver in jsdom**: not natively supported. Tests must polyfill or mock. Use `vi.stubGlobal("IntersectionObserver", FakeObserver)` pattern.
126
+ - **Smooth scroll + virtual list**: long transcripts may benefit from windowing later; v1 keeps full transcript mounted. Acceptable for typical chat lengths (< 200 turns).
127
+ - **Cursor placement**: if cursor renders inside `MarkdownView` it would be parsed as text. Keep it strictly outside the markdown wrapper. Document.
128
+ - **Jump button overlap with composer**: position bottom-right of scroll region with margin; the composer sits below the region, not overlapping.
129
+ - **Reduced-motion not asserted in tests**: difficult under jsdom; manual check sufficient.
130
+ - **Final flush race**: if a `turn_end` arrives in the same tick as a pending rAF, ordering matters. Tests cover the flush-then-cancel path explicitly.
@@ -0,0 +1,278 @@
1
+ # S0043: Startup Ergonomics — Single `scorel` Entry, Auto Token, WebUI Auto-Detect, `scorel up`
2
+
3
+ ## Goal
4
+
5
+ Collapse the multi-binary, multi-flag, multi-terminal startup flow into a single `scorel` entrypoint with sensible defaults, persistent token, and a one-command `scorel up` that brings up daemon + WebUI together. Eliminate every step in the current "open two terminals, type four required flags, copy a token into a browser form" loop.
6
+
7
+ Locked discussion: `self/discussions/2026-05-31-webui-polish-brainstorm.md` follow-up turn (this session). Decisions:
8
+
9
+ - Single `scorel` binary; retire `scorel-daemon` (apps/daemon bin removed).
10
+ - No backward compatibility — project still pre-1.0.
11
+ - Default host `127.0.0.1` (loopback).
12
+ - Token persisted in `~/.scorel/daemon.json`, generated once, reused across runs.
13
+ - daemon.json never auto-deleted; reused across restarts via pid liveness check.
14
+
15
+ This spec replaces the M3-era `scorel-daemon start` (local unix socket) entirely. Everything that connected over `daemon.sock` either moves to embedded transport (CLI `scorel chat`) or to the WS daemon (`scorel daemon serve`).
16
+
17
+ ## Scope
18
+
19
+ ### CLI command surface (after this spec)
20
+
21
+ ```
22
+ scorel chat [--session <id>] [--cwd <dir>]
23
+ scorel attach [--session <id>] [--remote <ws-url> --token <token>]
24
+ scorel daemon serve [--host <h>] [--port <p>] [--token <t>] [--cwd <d>]
25
+ scorel daemon status
26
+ scorel daemon stop
27
+ scorel daemon reset
28
+ scorel webui [--port <p>] [--host <h>]
29
+ scorel up [--daemon-port <p>] [--webui-port <p>] [--cwd <d>]
30
+ scorel logs [--attach] --session <id> [--remote <ws-url>] [--tail <n>]
31
+ ```
32
+
33
+ Removed: every `scorel-daemon ...` invocation, every `scorel daemon start|status|stop` form bound to `daemon.sock`. The local unix socket path (`~/.scorel/daemon.sock`) is no longer used; only the WS daemon (`serve`) remains.
34
+
35
+ ### `scorel daemon serve` defaults
36
+
37
+ | Flag | Default |
38
+ |---|---|
39
+ | `--host` | `127.0.0.1` |
40
+ | `--port` | `7777` |
41
+ | `--token` | `daemon.json.token` if file exists; else `crypto.randomUUID()`, persisted |
42
+ | `--cwd` | `process.cwd()` |
43
+
44
+ Any explicit `--token <new>` rotates and overwrites the stored token. Any explicit `--host` / `--port` overwrites the stored fields too (so `daemon status` reflects current state).
45
+
46
+ ### `~/.scorel/daemon.json` schema (replaces M3 shape)
47
+
48
+ ```ts
49
+ type DaemonStateFile = {
50
+ host: string;
51
+ port: number;
52
+ wsUrl: string; // derived: ws://host:port (host literal as written)
53
+ token: string; // persistent, regenerated only via reset or explicit --token
54
+ cwd: string;
55
+ pid: number; // process holding the lock; may be stale
56
+ startedAt: number; // Date.now()
57
+ stoppedAt: number | null; // null while running; populated on graceful exit
58
+ };
59
+ ```
60
+
61
+ Lifecycle:
62
+
63
+ - `serve`:
64
+ 1. Read existing file (if any). Reuse `token` unless `--token` overrides.
65
+ 2. If file present and `stoppedAt === null`, run `process.kill(pid, 0)` to liveness-check.
66
+ - Alive **and** mode is the same → exit 1 with `scorel daemon already running pid=<pid> url=<wsUrl>`.
67
+ - Dead → fall through (treat as orphan, overwrite).
68
+ 3. Write file with new `pid`, `startedAt`, `stoppedAt: null`, current host/port/cwd, reused or fresh token.
69
+ 4. Boot `startEmbeddedDaemonWebSocketServer`. On graceful shutdown (SIGINT/SIGTERM/`server.close()`) update `stoppedAt = Date.now()`. Do **not** delete the file.
70
+ 5. Crash path leaves `stoppedAt: null` with a dead pid; next `serve` cleans it up.
71
+ - `status`:
72
+ - File missing → exit 1, `scorel daemon not configured`.
73
+ - File present, pid alive, `stoppedAt === null` → exit 0, print `running url=<wsUrl> pid=<pid> token=<token>` (token printable only for local-loopback config; if `host !== 127.0.0.1` and stdout is a TTY, prompt before print or require `--show-token`).
74
+ - File present, pid dead or `stoppedAt` set → exit 0, print `stopped url=<wsUrl> last-pid=<pid> stoppedAt=<ts>`.
75
+ - `stop`:
76
+ - File present + alive pid → `process.kill(pid, "SIGTERM")`; wait up to 5s for `stoppedAt` to populate; force `SIGKILL` if not.
77
+ - File missing or pid dead → exit 0 with note.
78
+ - `reset`:
79
+ - Delete `daemon.json`. Print `daemon state reset; next serve will generate a new token`.
80
+
81
+ ### `scorel webui` subcommand
82
+
83
+ - Spawns `next dev` from `apps/webui` workspace via the workspace's own `pnpm dev` (resolved relative to the package using `pnpm --filter @scorel/app-webui dev`, or by spawning `node_modules/next/dist/bin/next` directly to avoid a recursive pnpm hop).
84
+ - Flags: `--port` (default 3000), `--host` (default `127.0.0.1`).
85
+ - Forward env: `PORT`, `HOST` to next.
86
+ - Stream stdout/stderr with `[webui]` prefix.
87
+
88
+ ### `scorel up` subcommand
89
+
90
+ - Reads `~/.scorel/daemon.json` if present, otherwise generates on the fly via the same `serve` path.
91
+ - Spawns daemon serve in-process (subprocess) with current defaults; waits until it logs `scorel daemon serving url=<wsUrl>` before continuing (parse stdout line).
92
+ - Spawns `scorel webui --port <webui-port>` as a separate child process.
93
+ - Prints unified header:
94
+ ```
95
+ scorel up
96
+ daemon ws://127.0.0.1:7777 token=<token>
97
+ webui http://127.0.0.1:3000
98
+ ```
99
+ - SIGINT (Ctrl+C) propagates SIGTERM to both children; awaits both exits; final line `scorel up stopped`.
100
+ - If either child dies unexpectedly, kill the other and exit 1.
101
+
102
+ ### WebUI auto-detect
103
+
104
+ New file `apps/webui/app/api/local-daemon/route.ts` (Next App Router server route):
105
+
106
+ ```ts
107
+ export async function GET(): Promise<Response> {
108
+ // Reads ~/.scorel/daemon.json server-side.
109
+ // Returns 404 if missing.
110
+ // Returns 200 { wsUrl, token, cwd, host, port } if present.
111
+ // Important: this route runs on the WebUI server, which is on the user's machine.
112
+ // It must NOT be exposed when WebUI is hosted somewhere else.
113
+ // For v1 we only support local dev (next dev on the user's box), so reading the file is OK.
114
+ // Document the invariant in apps/webui/README.md.
115
+ }
116
+ ```
117
+
118
+ Constraint: this route only fires when both daemon and webui run on the same machine (default flow). If a user hosts webui on another host, the route returns 404 because `~/.scorel/daemon.json` does not exist on that host. Acceptable v1.
119
+
120
+ WebUI Settings page changes:
121
+
122
+ - On Settings mount, call `fetch("/api/local-daemon")` once.
123
+ - If 200 and no existing device matches `wsUrl + token`, render a banner card above the device list:
124
+ ```
125
+ Detected local daemon
126
+ ws://127.0.0.1:7777 cwd=/Users/.../Scorel
127
+ [Use this device]
128
+ ```
129
+ - Click → BrowserStore.upsertDevice with `{ name: "Local", link: wsUrl, token }`. Navigate to `/devices/<id>`.
130
+ - If a matching device already exists, no banner (to avoid duplicates).
131
+
132
+ ### Retire `apps/daemon`
133
+
134
+ - Delete `apps/daemon/src/index.ts` bin entry (entire `apps/daemon/` directory removed).
135
+ - Move daemon-app logic that doesn't already live in `packages/daemon` into `apps/cli/src/daemon-cli.ts` (new file) imported by `apps/cli/src/index.ts`.
136
+ - Remove `pnpm-workspace.yaml` reference if it lists the package explicitly.
137
+ - Remove root `package.json` `scorel-daemon` script.
138
+ - Update root `dev` script (new): `"dev": "node --import tsx apps/cli/src/index.ts up"`.
139
+
140
+ ### `packages/daemon` shape change
141
+
142
+ `LocalDaemonState` type updated:
143
+
144
+ ```ts
145
+ export type LocalDaemonState = {
146
+ host: string;
147
+ port: number;
148
+ wsUrl: string;
149
+ token: string;
150
+ cwd: string;
151
+ pid: number;
152
+ startedAt: number;
153
+ stoppedAt: number | null;
154
+ };
155
+ ```
156
+
157
+ `socketPath` removed. `startLocalDaemonSocketServer` and friends removed (not used anywhere after this spec). Search-and-destroy passes:
158
+
159
+ ```bash
160
+ rg "socketPath|daemon\.sock|startLocalDaemonSocketServer|NodeSocketTransport" -- packages/ apps/
161
+ ```
162
+
163
+ Each remaining hit must be either deleted or migrated.
164
+
165
+ `createLocalDaemonState` / `readLocalDaemonState` keep their function names; payload schema follows the new shape. New helpers:
166
+
167
+ - `daemonStateLiveness(state): "running" | "stopped" | "orphan"` — encapsulates the pid liveness + `stoppedAt` logic.
168
+ - `markDaemonStopped(stateDir, stoppedAt)` — partial update used by graceful shutdown.
169
+
170
+ ### Tests
171
+
172
+ CLI (`apps/cli/src/index.test.ts`):
173
+
174
+ - `daemon serve` honors defaults (mock `startEmbeddedDaemonWebSocketServer`, capture call args).
175
+ - `daemon serve` reuses token across two invocations via a tmp `stateDir`.
176
+ - `daemon serve` rejects when prior pid is alive (mock `process.kill(pid, 0)`).
177
+ - `daemon serve` overwrites stale orphan state (`stoppedAt: null` + dead pid).
178
+ - `daemon status` prints running/stopped lines.
179
+ - `daemon stop` issues SIGTERM and waits for `stoppedAt`.
180
+ - `daemon reset` deletes the state file.
181
+ - `webui` subcommand spawns next with correct env (mock `child_process.spawn`).
182
+ - `up` orchestrates daemon ready-detection then webui spawn (mock spawn + scripted stdout).
183
+ - `up` SIGINT propagates SIGTERM to both children.
184
+
185
+ Daemon package (`packages/daemon/src/protocol.test.ts`):
186
+
187
+ - Update existing tests for new `LocalDaemonState` shape.
188
+ - New test for `daemonStateLiveness` covering running / stopped / orphan / missing-file.
189
+ - Delete tests touching `daemon.sock` and `startLocalDaemonSocketServer`.
190
+
191
+ WebUI (`apps/webui/src/api-local-daemon.test.ts` new):
192
+
193
+ - 200 path: file present, parses, returns wsUrl+token+cwd.
194
+ - 404 path: file missing.
195
+ - 500-ish defensive path: malformed JSON → 404 + console.warn (do not crash dev server).
196
+ - Token present in the response body but never logged. Asserted by spying console.
197
+
198
+ Settings UI test (`apps/webui/components/settings/device-list.test.tsx` extend):
199
+
200
+ - Mocks `fetch("/api/local-daemon")` returning 200; renders banner; click adds device; banner disappears.
201
+ - Mocks 404; banner not rendered.
202
+ - Existing matching device → banner not rendered.
203
+
204
+ ### Docs
205
+
206
+ - `docs/SHIP.md`: add Quickstart section near the top:
207
+ ```
208
+ ## Quickstart
209
+ pnpm install
210
+ pnpm dev # = scorel up; daemon + WebUI
211
+ open http://127.0.0.1:3000
212
+ ```
213
+ - `apps/webui/README.md`: document `/api/local-daemon` invariant (server-side route, only useful when WebUI runs on same host as daemon).
214
+ - `docs/ROADMAP.md`:
215
+ - Add new milestone `M5.6: Startup Ergonomics` with goal + Done table.
216
+ - Mark S0043 row `Done` after this spec ships.
217
+
218
+ ## Not In Scope
219
+
220
+ - Custom `cwd` per WebUI device (still daemon-side at startup).
221
+ - Skills / Plugins / OAuth / TLS / public tunnel.
222
+ - Multi-daemon side-by-side (one daemon.json per `~/.scorel`).
223
+ - Auto-restart / supervisor (still manual `scorel daemon stop && scorel daemon serve`).
224
+ - Token rotation API; only `--token` flag overrides on next serve.
225
+ - Browser-side detect-on-every-page-load (only Settings mount).
226
+ - Windows-specific path/PID semantics (development on macOS/Linux primary).
227
+
228
+ ## Acceptance Criteria
229
+
230
+ - A clean machine flow takes the user from `git clone` to a working WebUI + first prompt in **two commands**:
231
+ ```
232
+ pnpm install
233
+ pnpm dev
234
+ open http://127.0.0.1:3000 # Settings shows "Detected local daemon"
235
+ ```
236
+ - `apps/daemon/` is gone from the workspace.
237
+ - `scorel-daemon` script removed from root `package.json`.
238
+ - `scorel daemon serve` runs without any required flags; subsequent invocations reuse the token.
239
+ - `~/.scorel/daemon.json` survives daemon restart; `token` value identical across runs.
240
+ - `scorel daemon status` correctly distinguishes running / stopped / orphan based on pid liveness + `stoppedAt`.
241
+ - `scorel up` brings up both children, prints unified header, terminates both on Ctrl+C.
242
+ - WebUI `/api/local-daemon` returns the JSON only when the file exists; the response is consumed by the Settings page banner.
243
+ - Settings banner renders exactly once and adds the device on click; it disappears after add.
244
+ - All `daemon.sock` / `socketPath` / `NodeSocketTransport` code paths removed; `rg "daemon\\.sock"` returns no production hits.
245
+ - `pnpm typecheck && pnpm test` green.
246
+ - `pnpm --filter @scorel/app-webui build` green.
247
+ - Manual smoke: with real provider in `~/.scorel/config.toml`, run `pnpm dev` from a fresh `~/.scorel`; click "Use this device"; send a real prompt; receive a streamed reply.
248
+
249
+ ## Affected Paths
250
+
251
+ - `apps/cli/src/index.ts` — add `daemon serve|status|stop|reset`, `webui`, `up` subcommands; route `daemon` argv through new `daemon-cli.ts`.
252
+ - `apps/cli/src/daemon-cli.ts` (new) — daemon serve/status/stop/reset implementations.
253
+ - `apps/cli/src/up-cli.ts` (new) — `scorel up` orchestrator.
254
+ - `apps/cli/src/webui-cli.ts` (new) — `scorel webui` spawner.
255
+ - `apps/cli/src/index.test.ts` — extend.
256
+ - `apps/daemon/` — **deleted** (package + bin retired).
257
+ - `pnpm-workspace.yaml` — drop `apps/daemon` if listed.
258
+ - `packages/daemon/src/index.ts` — new `LocalDaemonState` schema, drop socket helpers, add `daemonStateLiveness` + `markDaemonStopped`.
259
+ - `packages/daemon/src/protocol.test.ts` — schema + helper tests; drop socket tests.
260
+ - `apps/webui/app/api/local-daemon/route.ts` (new).
261
+ - `apps/webui/src/api-local-daemon.test.ts` (new).
262
+ - `apps/webui/components/settings/device-list.tsx` — render detected-daemon banner.
263
+ - `apps/webui/components/settings/device-list.test.tsx` — extend for fetch mock.
264
+ - `apps/webui/README.md` — document auto-detect route.
265
+ - `docs/SHIP.md` — Quickstart section.
266
+ - `docs/ROADMAP.md` — add M5.6 + S0043 entry, flip Done after ship.
267
+ - `package.json` (root) — replace `scorel-daemon` script removal; add `dev` = `scorel up`.
268
+
269
+ ## Risks And Boundaries
270
+
271
+ - **Token leak via /api/local-daemon**: only same-origin fetch from a same-host browser tab; route is a server route, never serialized into client bundles. Adversary scenarios all require local filesystem access already (game over). Acceptable; document.
272
+ - **`scorel up` orphaning children on crash**: if the parent dies between spawn-daemon and spawn-webui, daemon keeps running. That's actually desirable (idempotent next `up` will detect it and only spawn webui). Test path covers it.
273
+ - **PID reuse**: a long-stopped daemon pid could collide with a newly assigned pid post-reboot. `process.kill(pid, 0)` returns true even if it's some unrelated process. Mitigated by checking `startedAt` proximity? No — too fragile. Accept the rare false-positive; user can `scorel daemon reset`.
274
+ - **Unix-only `process.kill(pid, 0)`**: on Windows the semantics differ; project is macOS/Linux primary, document and skip.
275
+ - **WebUI banner double-trigger**: race between fetch and existing-device list load. Implementation must wait until BrowserStore hydrated before rendering banner; otherwise we'd flash "detected" then hide. Test the order.
276
+ - **Single daemon assumption**: project-wide `~/.scorel` assumes one daemon per user. Multi-daemon requires `--state-dir <custom>` flag, which exists in code paths but is not surfaced as part of this spec; documented as a power-user override.
277
+ - **Removed mode `start` (unix socket)**: any spec that depended on `daemon.sock` (S0014/S0015 era) is now historical; the WS daemon is the only daemon in production paths.
278
+ - **PR scope**: large blast radius (touches 4 packages). Single commit per SHIP.md convention: `S0043: feat: collapse startup to a single scorel entry with auto token`.