diff --git a/.forgeplan/adrs/ADR-010-agent-sdk-onboarding-daemon-in-a-separate-optional-npm-package-launched-by-a-spawn-only-bin-subcommand.md b/.forgeplan/adrs/ADR-010-agent-sdk-onboarding-daemon-in-a-separate-optional-npm-package-launched-by-a-spawn-only-bin-subcommand.md new file mode 100644 index 0000000..a816288 --- /dev/null +++ b/.forgeplan/adrs/ADR-010-agent-sdk-onboarding-daemon-in-a-separate-optional-npm-package-launched-by-a-spawn-only-bin-subcommand.md @@ -0,0 +1,112 @@ +--- +depth: standard +id: ADR-010 +kind: adr +links: +- target: PRD-038 + relation: based_on +- target: ADR-003 + relation: informs +status: active +title: Agent SDK + onboarding daemon in a separate optional npm package launched by a spawn-only bin/ subcommand +--- + +## Context + +Pillar C of the composed-map onboarding program (PRD-038) adds a **live local +onboarding agent**: the user asks questions in the web chat and a real Claude +Code session answers, driving the map camera. The fixed design (FD-1..FD-7): use +the user's LOCAL Claude Code via the **Claude Agent SDK** +(`@anthropic-ai/claude-agent-sdk`), running in a **localhost daemon-bridge** the +user launches (the SvelteKit server structurally cannot spawn `claude` — rule +22 keeps it a read-only mirror). + +The trap (PRD-038 **Q1**): **ADR-003 / rule 23** pin `bin/` to a named +allow-list of exactly `node:*` + `citty`. The Agent SDK is a heavy third-party +dependency with a large transitive tree. It cannot enter `bin/` without a +decision, because `bin/` is what `npx @forgeplan/web` runs **before the user has +installed anything** — the whole point of ADR-003 is that no third-party +resolution happens at `npx` time. + +## Decision + +**Selected**: Ship the Agent SDK + onboarding daemon as a **separate, optional +npm package** (working name `@forgeplan/web-agent`), launched by a **spawn-only** +`bin/` subcommand. + +`bin/forgeplan-web.mjs` gains an `onboard-agent` subcommand that does exactly one +new thing: `child_process.spawn` the separate package's binary (resolved from the +user's environment / `npx @forgeplan/web-agent`) and stream its output. **`bin/` +imports nothing from the agent package** — no `import`, no `require`, only a +`spawn` of an external process. ADR-003's allow-list (`node:*` + `citty` + +relative siblings) is therefore **untouched**: the core `@forgeplan/web` stays +lean and `npx`-fast for the 99% of users who only view the map; the agent is +opt-in and its heavy dependency tree is resolved **only** when a user +deliberately runs the agent. + +**Why Selected**: it is the only option that keeps ADR-003's `npx`-latency +guarantee intact while still letting Pillar C "use what already exists" (the +Agent SDK). The spawn-only boundary is the same trust seam rule 22 uses for the +`forgeplan` CLI — a process boundary, not an import. + +## Alternatives Considered + +| Option | Verdict | Why | +|--------|---------|-----| +| **A — separate optional npm package + spawn-only `bin/` subcommand** | **Chosen** | Core stays lean + `npx`-fast (ADR-003 intact); SDK resolved only on deliberate agent use; process boundary mirrors rule 22's `forgeplan`/`git` spawn seam. | +| B — bundled `dist-agent/` image (esbuild-inline, PRD-030/ADR-005 shape) | Rejected | The image discipline (PRD-030) is for **viewer variants** copied by `init`; it would bloat every install with the SDK + its transitive tree even for users who never run the agent, and an esbuild single-file bundle of the Agent SDK (which itself spawns `claude`) is fragile. | +| C — extend the ADR-003 allow-list to admit the SDK into `bin/` | Rejected | Reintroduces the exact `npx`-time third-party resolution ADR-003 removed — every `init`/`start`/`update` would pay to resolve the SDK before doing its job, for a feature most users never touch. Directly violates ADR-003's invariant I3. | + +## Consequences + +### Positive +- Core `@forgeplan/web` unchanged in weight + `npx` latency; ADR-003 / rule 23 + hold verbatim (verified by the existing bin allow-list grep). +- The agent is strictly **opt-in**: no SDK, no `claude`, no API key for the + view-only user. +- Security is a natural consequence of the process boundary: the daemon is a + separate, user-launched, 127.0.0.1-bound process with a read-only agent + profile — the web never gains a code-execution surface. + +### Negative (trade-offs) +- A **second package** to publish + version (`@forgeplan/web-agent`), plus a + documented spawn contract between the `onboard-agent` subcommand and that + package's binary. +- The user must install / `npx` the agent package on first use (mitigated by a + guided prompt from the `onboard-agent` subcommand when the package is absent). + +### Risks +- **Version skew** between `@forgeplan/web` and `@forgeplan/web-agent` (mitigate: + the daemon advertises a protocol version in its WebSocket probe; the web + tolerates a missing/older daemon by staying in chat **Tier 0**). +- The spawn-only subcommand must **validate the agent package's presence** and + fail with an actionable install hint, never a raw ENOENT. + +## Invariants + +- `bin/` imports only `node:*`, `citty`, and relative `bin/` siblings — the + `onboard-agent` subcommand adds **only** a `child_process.spawn`, never an + `import`/`require` of the agent package (rule 23 grep must still pass). +- The daemon binds **127.0.0.1 only** and is launched explicitly by the user. +- The agent runs a **read-only** profile: Read/Glob/Grep + read-only forgeplan + MCP; no Write/Edit/Bash. +- The SvelteKit server (`/api/*`) is never involved in the agent path — the + browser talks to the daemon directly (rule 22 intact). + +## Evidence Requirements + +- A spawn smoke: `bin onboard-agent` spawns the agent package binary (or emits + the install hint when absent) — exit-code asserted. +- Rule-23 verification grep over `bin/` still reports OK (no new bare-specifier + imports). +- The agent package's SDK options object denies Write/Edit/Bash and binds + localhost only (asserted in the agent package's own tests). + +## Related Artifacts + +| Artifact | Type | Relation | +|----------|------|----------| +| PRD-038 | PRD | based_on (Pillar C, Q1) | +| ADR-003 | ADR | informs (the bin/ allow-list this preserves) | +| RFC (Pillar C daemon, pending) | RFC | based_on (the RFC that presumes this packaging) | + diff --git a/.forgeplan/evidence/EVID-096-pillar-c-live-agent-works-end-to-end-daemon-local-cc-answered-grounded-drove-show-on-map-camera-153-tests-smoke-pass.md b/.forgeplan/evidence/EVID-096-pillar-c-live-agent-works-end-to-end-daemon-local-cc-answered-grounded-drove-show-on-map-camera-153-tests-smoke-pass.md new file mode 100644 index 0000000..298b5e6 --- /dev/null +++ b/.forgeplan/evidence/EVID-096-pillar-c-live-agent-works-end-to-end-daemon-local-cc-answered-grounded-drove-show-on-map-camera-153-tests-smoke-pass.md @@ -0,0 +1,95 @@ +--- +depth: standard +id: EVID-096 +kind: evidence +last_modified_at: 2026-07-06T15:34:34.875409+00:00 +last_modified_by: claude-code/2.1.201 +links: +- target: RFC-034 + relation: informs +status: active +title: 'Pillar C live agent works end-to-end: daemon + local CC answered grounded + drove show_on_map camera; 153 tests, smoke PASS' +--- + +## Status + +draft + +## Summary + +Prove-phase checkpoint for **RFC-034** (Pillar C, the live onboarding agent) and its packaging +**ADR-010**. The live agent works **end-to-end**: the localhost daemon boots, a **real local Claude +Code session** (via the Agent SDK) answered a project question **grounded in the actual repo**, and +the model **called the `show_on_map` tool** which relayed a camera frame to the client. Plus 153 +web-side tests, `svelte-check` 0, and the daemon smoke — all green. + +## Observations (measured, 2026-07-06) + +### Live end-to-end turn (the headline — a real model turn, not a mock) + +Spawned the daemon: `node agent/bin/agent.mjs --cwd --port 7461` → printed +`onboard-agent live on ws://127.0.0.1:7461`. A WebSocket client connected, received +`{type:"ready", protocolVersion:1, model:"forgeplan-web-agent (claude-agent-sdk)"}`, and sent +*"What is this project and what is it for? … then use show_on_map to point at the most important +zone."* The daemon's persistent `query()` session streamed a **grounded** answer (verbatim): + +> "**@forgeplan/web** is a tiny zero-install npm CLI that scaffolds a pre-built SvelteKit app into a +> project's `.forgeplan-web/` folder, then serves a read-only, force-directed map of that project's +> Forgeplan artifacts… run `npx @forgeplan/web start` and *see* a project's decisions and structure +> as an interactive graph — no install, no write access to the workspace." + +— then emitted `{type:"show_on_map", target:{kind:"zone", id:"z.surfaces"}}`, then continued +narrating the zone flow (z.surfaces → z.core → z.ui, z.decisions records why) and offered to walk +deeper. Frames observed: `ready` → `token`* (streamed) → `show_on_map` → `token`* → `done`. The +answer is factually correct and sourced from the real repo — the read-only agent read the project. + +### Automated + smoke (measured) + +- `npx vitest run src/widgets/map-chat src/widgets/composed-map` → **14 files / 153 tests PASS** + (Tier-0 tier0/chat-store/MapChat + Tier-1 agent-client/chat-store + camera-bus + tour + drill). +- `npx svelte-check` → **0 errors** (2 pre-existing a11y warnings on the map ``). +- `node agent/scripts/smoke.mjs` → **exit 0, ALL CHECKS PASS**: protocol round-trip; `buildOptions` + denies Write/Edit/Bash + allows `mcp__onboard__show_on_map`; message-queue generator shape; daemon + binds 127.0.0.1, `GET /health` responds, WS sends `{ready}`. +- Rule-23 allow-list grep over `bin/` → **OK** for all `.mjs` (the `onboard-agent` subcommand is + spawn-only; no import of `@forgeplan/web-agent`; root `package.json` untouched). + +### Invariants confirmed + +- **ADR-010**: the Agent SDK + daemon live in the SEPARATE `@forgeplan/web-agent` package (its own + deps `@anthropic-ai/claude-agent-sdk` ^0.3, `ws`, `zod`; own `node_modules`, gitignored). Core + `bin/` unchanged in weight; the subcommand only `child_process.spawn`s the package. +- **Rule 22**: the live path is browser↔daemon over `ws://127.0.0.1`; the SvelteKit `/api/*` server + is never involved. +- **Read-only**: `allowedTools` = Read/Glob/Grep + `show_on_map`; `disallowedTools` = + Write/Edit/Bash; the model cannot mutate the workspace. + +## Known scope boundary + +- The daemon's `--port` default is 7431; the live turn above used 7461 (explicit). Port discovery + (fixed-port probe vs a discovery file) is RFC-034 **OQ1**. +- `cancel` (abort mid-stream) and socket-closed-mid-tool-call are marked `TODO` (Phase-4 hardening), + not blockers. +- `show_on_map(node)` precision depends on marketplace **CM-02** (stable node ids); `zone`/`flow` + targeting (demonstrated) is exact. +- `@forgeplan/web-agent` is not yet published to npm — the subcommand's `npx` fallback is verified + against the registry 404 only; re-verify post-publish. + +## Structured Fields + +verdict: supports +congruence_level: 3 +evidence_type: test + +## Related Artifacts + +- **RFC-034** (`informs`) — the daemon/protocol/camera/chat architecture this proves; activation + gated on this checkpoint (rule 11, R_eff > 0). +- **ADR-010** — the packaging decision (separate optional package + spawn-only subcommand) this + build realises and confirms. +- **RFC-033** — the tour camera the agent drives via the `camera-bus` seam (Phase 1). +- **PRD-038** — parent PRD (Pillar C, FD-1..FD-7). +- **`docs/MAP-PACK-FINDINGS-FOR-MARKETPLACE.md`** — CM-02 (stable node ids) for precise + `show_on_map(node)`. + + diff --git a/.forgeplan/evidence/EVID-097-code-review-of-rfc-035-wave-1-blocker.md b/.forgeplan/evidence/EVID-097-code-review-of-rfc-035-wave-1-blocker.md new file mode 100644 index 0000000..b7bfe61 --- /dev/null +++ b/.forgeplan/evidence/EVID-097-code-review-of-rfc-035-wave-1-blocker.md @@ -0,0 +1,108 @@ +--- +depth: standard +id: EVID-097 +kind: evidence +last_modified_at: 2026-07-07T11:58:34.941143+00:00 +last_modified_by: claude-code/2.1.202 +links: +- target: RFC-035 + relation: informs +status: draft +title: 'Code review of RFC-035 Wave 1: BLOCKER' +--- + +## Verdict + +BLOCKER + +One-line justification: the only two visible-behaviour requirements of RFC-035 Wave 1 (FR-1 float/dock/resize window, FR-2 status header, FR-3 Chat|Info tabs, FR-4 Info tab) were never wired into `MapChat.svelte` — the new `FloatingWindow` primitive and the new `chat-store` tab state are built but orphaned; `MapChat.svelte` has a **zero-line diff** and still renders the old fixed-position panel with the old verbose Badge string. + +## Scope + +- Parent: RFC-035 +- Diff range: ad-hoc — uncommitted working-tree changes on `feat/idef0-onboard-agent-phase1` vs `HEAD` (`e42040e`) +- Files reviewed: 6 files touched (2 new, 2 modified, 2 lockfile/manifest), plus full read of `MapChat.svelte` (644 lines) and `chat-store.svelte.ts` diff +- Files: `template/package.json`, `template/package-lock.json`, `template/src/shared/ui/index.ts`, `template/src/shared/ui/floating-window/FloatingWindow.svelte` (new, 655 lines), `template/src/shared/ui/floating-window/index.ts` (new), `template/src/widgets/map-chat/model/chat-store.svelte.ts` (+22 lines) + +## Tools run + +| Tool | Exit | Notes | +|---|---|---| +| `npx vitest run src/widgets/map-chat src/shared/ui` | 0 | 3 test files, 59 tests, all passed — no regressions, but zero new tests added for `FloatingWindow` | +| `npx svelte-check --threshold error` | 0 | 1280 files, 0 errors, 8 warnings, 2 files with problems | +| rule-24 grep (`.claude/rules/24-shared-ui-ownership.md` Verification block) | non-zero (FAIL) | sole match is pre-existing (`MapChat.svelte:516`, file has 0 diff lines this change) — not a new violation, see Findings #5 note | + +## Ground-truth verification + +- Base..head: `e42040e..` (source: current branch HEAD; no PR base/head SHA supplied — ad-hoc uncommitted-diff review) +- Diff probe: `git diff --stat HEAD -- template/` and `git status --short` +- Diff state: **DELTA=PRESENT** (package.json/package-lock.json/shared/ui/index.ts/chat-store.svelte.ts modified; `shared/ui/floating-window/` untracked-new) +- Expected delta token: `FloatingWindow` used as a consumer import inside `template/src/widgets/map-chat/ui/MapChat.svelte` (the token that would prove Wave 1's stated goal — "MapChat becomes floatable/dockable/resizable" — actually landed) +- Token probe: `grep -rn "FloatingWindow" template/src --include="*.svelte" | grep -v shared/ui/floating-window` → **ABSENT** +- Verdict floor from ground-truth gate: PRESENT diff + ABSENT expected token → **CONCERNS** floor at minimum; escalated to **BLOCKER** below because the missing token is not an edge case but the entire stated deliverable (see Finding #1) + +``` +$ git diff --stat HEAD -- template/ + template/package-lock.json | 18 ++++++++++++++++++ + template/package.json | 1 + + template/src/shared/ui/index.ts | 1 + + .../widgets/map-chat/model/chat-store.svelte.ts | 22 ++++++++++++++++++++++ + 4 files changed, 42 insertions(+) + +$ git diff HEAD -- template/src/widgets/map-chat/ui/MapChat.svelte +(empty — zero output, file unchanged) + +$ grep -rn "FloatingWindow" template/src --include="*.svelte" --include="*.ts" | grep -v node_modules +template/src/shared/ui/index.ts:39:export { FloatingWindow, type FloatingWindowMode } from "./floating-window"; +template/src/shared/ui/floating-window/FloatingWindow.svelte:3,50,106,120 (own definition) +template/src/widgets/map-chat/model/chat-store.svelte.ts:35 (a doc-comment reference only, not an import) +→ zero import/usage sites in MapChat.svelte, ComposedMapView.svelte, or /playground + +$ grep -rn "MapChat" template/src/widgets/composed-map/ui/ComposedMapView.svelte +87: import MapChat from "@/widgets/map-chat/ui/MapChat.svelte"; +1137: (chatOpen = false)} /> +→ MapChat is mounted exactly as before RFC-035; no FloatingWindow wrapper introduced at the call site either +``` + +## Findings + +| # | Severity | Category | Location | Description | Recommended fix | +|---|---|---|---|---|---| +| 1 | CRITICAL | 🏗 Architecture | `template/src/widgets/map-chat/ui/MapChat.svelte` (0 diff lines) | RFC-035 Wave 1's entire visible scope (FR-1 float/dock/resize, FR-2 simplified 🟢/🔴 status header, FR-3 Chat\|Info `Tabs`, FR-4 Info tab scaffold) is unimplemented in the actual chat widget. The new `FloatingWindow` primitive is exported from `shared/ui/index.ts` but has zero import sites anywhere outside its own folder; `MapChat.svelte` still renders the old `position:fixed` panel with the old verbose `Badge` (`"● live — {model}"`), no `Tabs`, no Info content. This is not a partial landing of a large feature — it is the primitive built in isolation with the integration step skipped entirely. | Wrap `MapChat`'s root in `` (or compose it at the `ComposedMapView.svelte:1137` mount site), replace the header's verbose Badge with a 🟢/🔴 dot, add a `shared/ui` `Tabs` split driven by the already-built (but unused) `getActiveTab()/setActiveTab()`, and build the Info tab body per FR-4. | +| 2 | HIGH | 🧪 Test gap | `template/src/shared/ui/floating-window/` (no test file exists) | RFC-035's own Test Strategy section requires vitest coverage of "window-state persistence (save/restore/clamp-off-screen), dock↔float transitions, min-size clamp" — none of `loadPersisted`/`persist`/`clampFloating`/`handleDrag`/`handleDragEnd`/`toggleDock` has a single unit test. | Add `FloatingWindow.test.ts` (or a headless logic-extraction test) covering: restore-clamps-off-screen-position, dock→float on header drag, float→dock on edge-snap release, min-size enforcement on resize. | +| 3 | MEDIUM | 🐛 Bug | `template/src/shared/ui/floating-window/FloatingWindow.svelte:427` | `