feat(idef0): Pillar C — live onboarding agent (daemon + Agent SDK + camera chat) - #169
Merged
Merged
Conversation
The web-only foundation for the live onboarding agent (RFC-034), shipping
value with NO daemon: a chat that answers from map.json and drives the map.
- camera-bus.svelte.ts (rune store): the ONE seam a chat (Tier 0 or Tier 1)
uses to move the existing RFC-033 tour camera. showOnMap({kind,id}) bumps a
monotonic seq so re-asking about the same zone/node/flow still recentres;
ComposedMapView consumes it via a seq-keyed $effect → fitToRect (zone) /
select (node) / activeFlow (flow). No camera redesign.
- widgets/map-chat/ — the chat shell + Tier 0 (client-grounded, model-free,
offline): tier0.ts answers purely from the loaded MapDocument (zone/node/
flow match → grounded text + a CameraTarget), honest (no fabrication when
description_ru absent); chat-store.svelte.ts drives it + camera-bus;
MapChat.svelte composes shared/ui (rule 24). Tier 1 is a Phase-3 stub.
- ComposedMapView mounts an "Ask" toggle → the chat overlay.
Pure client (rule 22: no WebSocket, no /api/* — those are Phase 2/3). This is
the container the live agent (Tier 1) plugs into by swapping the answer source
from map.json to a live local Claude Code session.
vitest 123/123 on the composed-map + map-chat surface; svelte-check 0 errors.
Refs: RFC-034, PRD-038
…4/ADR-010)
The live agent: talk to your project via your LOCAL Claude Code, and the map
moves as it explains. Phases 2-3 of RFC-034, on top of the Phase-1 shell.
agent/ — NEW separate optional package @forgeplan/web-agent (ADR-010: its own
deps @anthropic-ai/claude-agent-sdk + ws + zod, never in the core):
- bin/agent.mjs: localhost (127.0.0.1) WebSocket daemon. Boots a persistent
Agent SDK query() session in a READ-ONLY profile (allowedTools Read/Glob/Grep
+ the show_on_map tool; disallowedTools Write/Edit/Bash), cwd = project root.
Registers one in-process createSdkMcpServer tool show_on_map(kind,id) whose
handler relays a {show_on_map} frame to the browser and returns a text ack.
Streams assistant text as {token} frames; {ready}/{done}/{error}; GET /health
for the probe. realpathSync main-module guard so the npx symlink still boots.
- lib/protocol.mjs (versioned WS schema), lib/profile.mjs (read-only options +
onboarding-guide systemPrompt), scripts/smoke.mjs (protocol + read-only +
bind + /health + ready, no live-model turn).
bin/commands/onboard-agent.mjs — NEW spawn-only subcommand (rule 23: node:* +
citty + siblings only; child_process.spawn the agent package, NEVER imports it;
actionable install hint when absent) + cli.mjs registration.
template/src/widgets/map-chat/ — Tier-1 wiring: agent-client.ts (read-only WS
client: probe → connect → stream tokens → dispatch show_on_map to camera-bus);
chat-store Tier-1 send (streams into the assistant message, degrades to Tier 0
when the daemon is down); MapChat "● live — <model>" vs "offline (Tier 0)".
Rule 22 intact (the live path is browser↔daemon, never /api/*). Rule 23 intact
(bin spawn-only; root package.json untouched). vitest 153/153, svelte-check 0,
daemon smoke exit 0, rule-23 grep OK.
Refs: RFC-034, ADR-010, PRD-038
…gent proven) Rule-11 gate for the live onboarding agent. EVID-096 records the end-to-end proof: the daemon booted, a real local Claude Code session answered a project question grounded in the actual repo, and the model called show_on_map to drive the camera (target zone z.surfaces) — plus 153 web tests, svelte-check 0, daemon smoke exit 0, rule-23 grep OK. verdict supports / CL3 / test. RFC-034 (daemon/protocol/camera/chat) and ADR-010 (separate optional package + spawn-only subcommand) both draft -> active (R_eff > 0 via EVID-096). This consolidates the Pillar C shape + build + evidence onto one branch; the shape-only PR #168 is superseded by this branch's PR. Refs: RFC-034, ADR-010, EVID-096, PRD-038
Live-testing the browser chat found a bug the mock-ws unit tests missed: connectAgent() returns immediately (doesn't wait for `open`), and chat-store's Tier-1 send calls `conn.send(question)` synchronously right after — so the socket was still CONNECTING and dispatch() silently dropped the user_message (`readyState !== OPEN`). The daemon's session then waited forever → the chat bubble stayed empty even though the connection was live. Fix: connectAgent now buffers any send/cancel issued before `open` in a `pending` queue and flushes it (in order) on the socket's `open` event. Every existing guarantee preserved (never throws; no-op without WebSocket; close/cancel unchanged). +2 tests (buffered-then-flushed on open; immediate when already open). Verified LIVE end-to-end in the browser: the daemon-backed Tier-1 chat now streams the agent's grounded answer into the bubble and the camera moves via show_on_map. vitest map-chat 60/60, svelte-check 0. Refs: RFC-034, EVID-096
Upgrade the onboarding chat from a minimal panel into a proper assistant chat that looks right in the app design. - ChatMarkdown.svelte + MdLink.svelte — assistant answers now render as REAL markdown (svelte-exmarkdown + gfmPlugin + rehype-highlight for code), no more raw ** and ##. Reactive so it streams live; denylist blocks script/iframe; custom renderers style links/code/headings in our tokens (dual-theme). - shared/ui/scroll-area/ — a new bits-ui ScrollArea primitive (rule 24: owned by shared/ui, showcased on /playground), re-exported from shared/ui. - chat-store: sessions — ChatSession + localStorage history (capped), newChat() archives the current + starts fresh, viewSession()/viewCurrentSession() revisit past transcripts read-only (live-continue is a // TODO graduation). - MapChat.svelte rebuilt: wider right drawer; header with the tier badge (● live — <model> / offline · Tier 0), New chat, and a sessions menu; a ScrollArea message list (assistant via ChatMarkdown, auto-stick to bottom only when near it, "↓ jump to latest" when scrolled up); a growing textarea input (Enter=send, Shift+Enter=newline); a "● thinking…" / streaming state; and a show_on_map chip in the message. Strictly our tokens, shared/ui + ScrollArea, dual-theme, a11y, reduced-motion. Runtime deps svelte-exmarkdown ^5 + rehype-highlight ^7 in template/package.json (rule 21). vitest map-chat 63/63 (+3), svelte-check 0. Refs: RFC-034, PRD-038
…oken
Two Pillar-C fixes from live use:
1. AI-ONLY chat. The model-free Tier-0 keyword matcher gave dumb/wrong
answers when the daemon wasn't running ("it shouldn't give anything at
all without AI"). Removed it: deleted tier0.ts + its test; send() only
answers via the live agent (no-op unless tier1). When the daemon is not
detected the chat shows a calm call-to-action — "The live assistant isn't
running" + a copyable `npx @forgeplan/web onboard-agent` (the shared/ui
Code primitive, rule 24) + "connects automatically once running" — with
the input/Send unrendered (no fake answers). The probe keeps polling and
the panel flips to live chat when the agent comes up.
2. TOKEN STREAMING. The daemon sent each assistant text block as one big
{token} frame (answers appeared in chunks, not a live typewriter). Now
options.includePartialMessages: true and the loop forwards
stream_event content_block_delta/text_delta as incremental {token}
frames; the complete assistant message no longer re-sends text. show_on_map
(tool relay) and the read-only profile are unchanged.
vitest map-chat 51/51 (tier0 tests removed), svelte-check 0, daemon smoke
exit 0.
Refs: RFC-034, PRD-038
…e polish Finish RFC-034 Pillar C Phase-4 hardening. - CANCEL/STOP: while a turn streams, Send becomes a Stop button that calls the connection's cancel() → the daemon interrupts the in-flight SDK query (activeQuery.interrupt()) for that connection and closes the turn; the partial answer already streamed is kept, input re-enables, the session stays open. Cancel with no in-flight turn is a no-op. - LIVE-CONTINUE: the daemon reports its SDK session_id (on ready) and accepts a `?resume=<sessionId>` on the WS upgrade → passes options.resume so a reconnect continues a prior conversation with its context. chat-store persists the session_id per ChatSession; a "Continue" action on a viewed past session reconnects with resume and makes it live again (graceful fresh-start fallback when no id / unsupported). - POLISH: the offline daemon probe now uses fetch(/health) + AbortController instead of opening a probe WebSocket (quieter — no red WS-refused console spam every poll); the stale "answers come straight from the loaded map" placeholder is replaced with AI-accurate copy. Read-only profile + rule 22 (browser↔daemon) intact. vitest map-chat 60/60, svelte-check 0, daemon smoke exit 0. Refs: RFC-034, PRD-038
…CORS-blocked
The Phase-4 "console-noise" polish changed probeDaemon to
fetch("http://127.0.0.1:7431/health"). The browser origin (:5179) and the
daemon (:7431) are different origins, so the cross-origin fetch is blocked by
CORS (the daemon serves no Access-Control-Allow-Origin header) — the probe
always rejected and the chat stayed stuck "offline" even with the daemon up.
Revert probeDaemon to the WebSocket implementation (ws:// does not enforce
CORS): open a probe socket, resolve {up:true, model} on the `ready` frame,
{up:false} on error/close/timeout. All other Phase-4 work (Stop/cancel,
session resume, onSession) is unchanged. Live-verified: badge flips to
"● live", token streaming grows, Stop keeps partial text.
Refs: RFC-034
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The map-chat upgrade added shared/ui/scroll-area (bits-ui ScrollArea wrapper) for the transcript list, but the /playground catalogue was never updated. Rule 24 requires every new shared/ui primitive to be showcased before any upper-layer caller uses it. Add the ScrollArea section (24 scrollable rows, token-styled thumb) so the catalogue reflects what widgets/map-chat consumes. Refs: RFC-034 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…per connection P0 — the daemon melted the host: kernel_task thermally throttled to ~230% because a `claude` CLI subprocess leaked on every WS connection, and the web liveness probe opened a fresh WS every 15s. Root cause (agent/bin/agent.mjs): startQuery() ran unconditionally on connection-open (spawning a query()/subprocess before any user_message), and socket close only set a passive `closed` flag that the drain loop checks only when the SDK emits another message — which never happens for a probe, so the subprocess parked forever. Compounded by the earlier CORS revert making the probe a WebSocket, so every 15s poll spawned+leaked one subprocess. Fix (defense in depth): - A lazy start: query() is now created on the FIRST user_message (queryStarted guard), so a probe / idle chat spawns zero subprocesses. `ready` still sent on connect so liveness is unaffected. - B active teardown: createMessageQueue gained close(); socket "close" now ends the input generator and calls activeQuery.interrupt()/.return() (Query extends AsyncGenerator — .return() disposes the child), swallowing rejections. Closing the drawer / navigating away kills the subprocess. - C cheap probe: /health now sends `access-control-allow-origin: *` (daemon is 127.0.0.1-only) and probeDaemon reverts to a fetch(/health) with an AbortController timeout — no WS, no connection, no subprocess. connectAgent (the real chat) stays WebSocket. Live-verified: 0 subprocs over 40s of probing; exactly 1 on a question; 0 within ~2s of navigating away; daemon survives; badge shows ● live via fetch. vitest 59/59, svelte-check 0, smoke exit 0 (11 checks incl. lazy-start proof). Refs: RFC-034 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…at|Info tabs RFC-035 Wave 1 (web-only). Turns the fixed right-drawer onboarding chat into a proper window and surfaces what the agent IS. - New shared/ui primitive `FloatingWindow` (rule 24) built on @neodrag/svelte (reuse-first — no hand-rolled pointer math): docked (resize left edge) and floating (drag header to move, resize any edge) modes, right-edge snap to re-dock, versioned localStorage geometry clamped to the viewport on restore (never off-screen), min-size, Pointer-Events + keyboard-nudge resize, role="toolbar" header, Esc-to-close, reduced-motion. Showcased on /playground. - MapChat renders inside FloatingWindow; verbose "● live — <model>" badge replaced by a compact 🟢 online / 🔴 offline dot (--ok/--danger tokens). - Chat | Info tabs via shared/ui Tabs (compose, not re-skin). Chat subtree stays mounted across switches (streaming survives). Info tab surfaces model, the read-only profile (allowed Read/Glob/Grep/show_on_map · disallowed Write/Edit/Bash), and Wave-2 placeholders for token usage + instance count. - chat-store gains activeTab state. Live-verified: status dot, tab switch (fixed a CSS-cascade bug where an author-origin display:flex defeated [hidden] — now scoped :not([hidden])), Info diagnostics, dock↔float toggle. vitest 77/77, svelte-check 0 errors. Wave 2 (daemon: native SDK usage frames + registry instance discovery) fills the two Info placeholders. EVID-097 recorded the pre-fix BLOCKER review. Refs: RFC-035 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y) + corner resize
RFC-035 Wave 2 (daemon/protocol) + a FloatingWindow resize refinement.
FR-5 token usage (reuse-first — forwards the SDK's NATIVE result.usage):
- protocol.mjs: new `usage` frame {inputTokens,outputTokens,costUsd}; ready
frame extended with capabilities[] + otherInstances[]; PROTOCOL_VERSION 1→2
(additive — old browsers ignore unknown frames, default the new arrays []).
- agent.mjs: on the SDK `result` message, forward message.usage.input_tokens /
output_tokens / total_cost_usd as a usage frame before done.
- web: agent-client parses usage + ready meta; chat-store accumulates session +
cumulative tokens/cost; Info tab TOKEN USAGE row shows "input N · output N ·
$cost". Live-verified: input 105,932 · output 702 · $3.0390.
FR-6 instance discovery (reuse-first — reuses the instances.json FORMAT, not
the core bin/lib/registry.mjs, since @forgeplan/web-agent is a separate package):
- new agent/lib/registry.mjs: own atomic writer for ~/.forgeplan-web/
instances.json with kind:"agent" rows + 30s heartbeat + stale sweep +
readOtherLiveInstances(self). Never crashes the daemon on fs error.
- agent.mjs registers on listen, heartbeats, deregisters on exit; reports
readOtherLiveInstances on the ready frame. web Info tab OTHER PROJECTS row
shows "sees N other · project:port". Live-verified with a 2nd daemon:
"sees 1 other · Work:7432". No /api change, rule 22 intact (browser↔daemon).
FloatingWindow resize (user ask — not just the left edge):
- resize grips on all 4 edges + 4 corners in floating mode (corner = 2-axis,
standard window affordance) with nwse/nesw/ew/ns cursors; docked keeps the
left width grip. min-size clamped, viewport-bounded, keyboard nudge kept.
Known follow-up: the Info rows populate on WS connect (first message) — a
connect-on-chat-open polish will surface them immediately (the connection is
lazy by the Wave-1 CPU fix; ready arrives with zero subprocess).
smoke exit 0, vitest 101/101, svelte-check 0 errors.
Refs: RFC-035
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/health probe RFC-035 follow-up. The Info tab's "Other projects" row only appeared after the first message, because otherInstances rode only on the WebSocket `ready` frame (opened lazily on first send). Now the cheap `fetch /health` liveness probe — which already polls every 15s — carries the same data, so the row (and model) populate the moment the chat opens, with no WebSocket and no subprocess. - agent.mjs: GET /health JSON gains `capabilities` + `otherInstances` (readOtherLiveInstances(self), try/catch → [] on any registry error), mirroring the ready frame. CORS header + shape otherwise unchanged. - agent-client.ts: ProbeResult carries optional capabilities/otherInstances; probeDaemon parses them from the /health JSON (malformed → []). - chat-store.ts: checkDaemon sets otherInstances from the probe result on tier1 — same state the ready frame feeds, so the 15s probe keeps it live even with no open connection. Tokens deliberately stay "—" until a real WS usage frame (correct — no usage before a turn). Deliberately does NOT touch the WS connection lifecycle (ensureConnection/connectAgent/send) — that path carried the earlier "stuck offline" bugs; the low-risk probe extension avoids it. Live-verified with two daemons: Info shows "sees 1 other · Work:7432" on chat open with zero messages sent. smoke 0, vitest 83/83, svelte-check 0. Note: a simultaneous-startup registry write race can briefly drop a row (self-heals via the 30s heartbeat) — pre-existing registry contention (RFC-035 I5), not introduced here. Refs: RFC-035 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chat launcher floated over the map (bottom-right), which was in the way, and looked like an ordinary secondary button. Two changes: - shared/ui Button gains a `magic` variant (rule 24 — the look lives in the primitive): an animated iridescent rainbow gradient (hue-sliding shimmer), a soft pulsing glow, and two twinkling ✦ sparkle accents (pure CSS pseudo- elements, no dep). White label + text-shadow carry contrast in both themes; `prefers-reduced-motion` freezes to a static gradient. Showcased on /playground + documented in shared/ui/README.md. (Also fixed a self-inflicted compile break: a CSS comment containing `*/` closed the block early.) - The launcher moves OUT of the map overlay into the /onboard header, next to "Exit to standard view →", as a `variant="magic"` "✨ Ask" / "Close chat" button. ComposedMapView gains `showChatLauncher` (default true) + a bindable `chatOpen`, so the onboard host drives the chat from its header while the dashboard host keeps its own (now also magic) launcher — neither breaks. - Removed the `.map-chat-pos` absolute wrapper: its stacking context was trapping the RFC-035 FloatingWindow's position:fixed root inside a local paint order instead of letting it escape to the viewport. Live-verified on /onboard: magic Ask sits in the header off the map, opens the chat, toggles to Close chat. Full suite 58 files / 771 tests, svelte-check 0. Refs: RFC-035 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…boost gradient) Refines the launcher per user direction (supersedes the filled "✨ Ask" header button from 60ae14a): - New shared/ui `MagicStar` primitive: the ✨ sparkles motif (one main four-pointed sparkle + two small accent sparkles) drawn as a gradient-stroked CONTOUR (fill:none, stroke=animated linear-gradient) in extraboost.ai's signature palette (#5B8DEF → #9D7BEA → #FB7185 → #FBBF24 → #34D399, looping). The colors visibly cycle — a 3s SMIL gradient rotation + a 4s CSS hue-rotate sweep — and freeze to a static gradient under prefers-reduced-motion. Unique gradient id per instance; showcased on /playground. - The launcher moves from the bottom-right over-the-map corner into the TOP chips toolbar, LEFT of the "All" chip: FlowChips gains a `leading` snippet (guarded so it still shows on zero-flow maps) and ComposedMapView passes the compact `<Button variant="ghost" size="icon"><MagicStar/></Button>` there. - Retired the just-added filled `magic` Button variant (superseded by MagicStar) and removed the onboard-header launcher + the now-unused showChatLauncher / bindable chatOpen plumbing — the launcher is common to both hosts again, mounted identically. Live-verified on /onboard: the ✨ sits left of "All" in the chips row with a visibly cycling gradient and opens the chat. vitest 211 pass, svelte-check 0. Refs: RFC-035 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "What's inside" zone-detail panel popped up and re-rendered on every pointer move across a zone (handleCanvasPointerMove set detailZoneId immediately), and sat top-right where it now collides with the chips toolbar + the ✨ launcher. - Dwell: detailZoneId is now set behind a 350ms timer (ZONE_DWELL_MS) that only fires if the cursor is still resting on the same zone — a quick pass no longer flashes the card. The hover ring (hoveredZoneId) stays immediate. The timer is cleared on zone change, on closeZoneDetail, on descend/level change, and on teardown. Sticky behavior kept: once shown it stays until a different zone is dwelt on or × dismisses it. - Moved ZoneDetailCard from top-right (top:52 right:16) to bottom-left (bottom:16 left:16), clear of the chips row and the bottom-center tour card; the "What's inside" list still scrolls. Live-verified: fast pass shows nothing; resting ~350ms on a zone shows the card in the bottom-left corner. vitest composed-map 101 pass, svelte-check 0. Refs: RFC-035 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An adversarial 3-dimension web review (verified each with a concrete trigger) found 9 real bugs the green suite + a manual click-through missed. All fixed + regression-tested. HIGH: - Cross-turn frame race: cancel+immediate-resend on the one persistent WS could land a cancelled turn's token/usage/done on the next answer. Fix: per-turn `turnId` threaded client↔daemon on user_message + every server frame; the client drops frames whose turnId != current; cancel advances the turn. - Close-chat-mid-stream left `pending` stuck + an orphaned "thinking…" bubble (newChat became a no-op). Fix: stopAgentProbe now calls fallBackToTier0. - Escape closed the chat AND navigated the map (one press, two actions). Fix: FloatingWindow.handleKeydown stopPropagation on Escape. - FloatingWindow docked width seeded/left unclamped → panel off-screen on a narrow first-visit or live-shrink. Fix: clamp dockedWidth on resize + init. - handleError appended the error then fell to tier0 in the same tick → the error text never rendered (blank CTA). Fix: keep the transcript visible whenever messages exist; CTA only for the empty case. MEDIUM: - Error frame had no fatal/non-fatal discriminant → any per-turn error forced full tier0 fallback. Fix: `fatal:boolean` on the error frame; tier0 only when fatal. - maxWidth() floored at minWidth below a 352px viewport → window spilled off-screen. Fix: cap geometry against raw viewport dimensions. - hoveredZoneId not reset on descend/ascend/climbTo → stale hover-ring flash on re-entry (deterministic zone ids). Fix: clear it alongside detailZoneId. - checkDaemon probe writes unguarded → a fast close→reopen let an older probe overwrite a newer result. Fix: monotonic probe generation guard. Also fixed a type-cast bug in the error-frame parse surfaced by the fatal change (runtime type check, no unsafe cast). PROTOCOL_VERSION bumped; turnId/fatal are additive (missing → tolerated, backward-compatible). smoke exit 0, vitest 60 files / 782 tests, svelte-check 0 errors. Refs: RFC-035 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
explosivebit
added a commit
that referenced
this pull request
Jul 9, 2026
…ee (#171) ## Summary Pre-release housekeeping on `develop` — no source or runtime behaviour change. - **Fix markdown↔index desync (RED LINE #4).** `PRD-038` + `RFC-035` were activated in the Lance index during the Pillar C (#169) close-out, but their `status: active` never landed in the committed markdown. A `forgeplan scan-import` on develop would have silently reverted them to `draft` and "unactivated" the Pillar C chain. Restored `status: active` (+ the session's score/link updates on the Pillar-B/C evidence chain). Verified: after `scan-import`, both stay `active`. - **Commit `EVID-098`** (RFC-035 chat-panel-v2 verification) — it existed only in the working tree, captured by no prior commit. - **Add three reference docs**: `docs/CLAUDE-PLUGINS.md`, `docs/MAP-PACK-v0.2.0-FINDINGS.md`, `docs/hints-rules.md`. - **gitignore** transient forgeplan/local paths: `.forgeplan/anomalies-journal.jsonl`, `.forgeplan/map/.work/`, `.local/`. - **Remove stray scratch**: `pnpm-lock.yaml` (project is npm — `package-lock.json` is the lockfile) + dev-run `*.yml` / `*.md` droppings. ## Why Bring `develop` to a clean, durable state before cutting the `v0.3.0` release branch. The markdown is the source of truth (ADR-003); it must match the derived index so a fresh clone + `scan-import` reproduces the activated Pillar C chain. ## Test plan - `forgeplan scan-import` → `PRD-038` + `RFC-035` remain **active** (desync cured). - No `template/` source touched → existing CI (svelte-check + vitest + build matrix) unaffected; this is a docs/metadata-only change. - `git diff --cached` verified: no scratch, no gitignored paths, no screenshots staged. Refs: PRD-038 🤖 Generated with [Claude Code](https://claude.com/claude-code)
6 tasks
explosivebit
added a commit
that referenced
this pull request
Jul 9, 2026
…oarding, 3D-iso) (#172) ## Summary Release **v0.3.0** (MINOR) — cuts the entire IDEF0 composed-map program from `develop` to `main`. 152 commits since `v0.2.4`, no breaking changes. Headline features landed since v0.2.4: - **Composed-map (T4)** — the 9th "Map" graph view: curated zoned composition with render-proof against the `forgeplan.map/v1` contract (PRD-036 / SPEC-006 / RFC-030). - **Recursive drill-down** — descend/climb/breadcrumb into zones, prefer map-pack-emitted per-zone layers with client-derived fallback (PRD-037 / RFC-031). - **Onboarding tour (Pillar B)** — `/onboard` route + deterministic zone-walk camera tour (#167). - **Live local-agent guide (Pillar C)** — daemon + Agent SDK + two-tier chat (Tier-0 map-grounded, Tier-1 live), floatable/dockable chat panel v2 (#169, PRD-038 / RFC-034 / RFC-035 / ADR-010). - **3D isometric Map-corner minimap** — Threlte v8 lazy chunk, bidirectional 3D↔2D drill sync, dist cap raised 3→3.5 MiB (#170, PRD-039 / RFC-036 / ADR-011). ## Version bump - `package.json` + `template/package.json`: `0.2.4` → `0.3.0`. ## Release checklist - [x] Version bumped in both package manifests - [ ] CI matrix (ubuntu / macos / windows) green on this PR - [ ] Merge to `main` (merge commit) - [ ] Annotated tag `v0.3.0` on `main` + push - [ ] GitHub Release published (fires `release.yml` → npm with provenance) - [ ] Back-merge `release/v0.3.0` → `develop` ## Test plan Full CI matrix on this PR must pass. No new code beyond the version bump; all feature work was already CI-verified on `develop` (each of #167/#169/#170/#171 merged green). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The live onboarding agent (PRD-038 Pillar C). Talk to your project in the web chat and a real local Claude Code session answers — grounded in the actual repo — while the map moves as it explains. Two-tier + graceful: Tier 0 answers offline from
map.json; Tier 1 upgrades to the live agent when the daemon is running.This PR consolidates the Pillar C shape + build + evidence (supersedes the shape-only #168).
Proven end-to-end (EVID-096)
Spawned the daemon, connected a WS client, asked "what is this project and what is it for?" — the local CC answered verbatim: "@forgeplan/web is a tiny zero-install npm CLI that scaffolds a pre-built SvelteKit app into
.forgeplan-web/, serves a read-only force-directed map of Forgeplan artifacts…npx @forgeplan/web start…" — then calledshow_on_map→{zone: z.surfaces}and narrated the zone flow. Real model turn, grounded, camera-driven.What's in it
camera-busseam (the one primitive a chat uses to move the RFC-033 tour camera) + Tier-0 chat (map-chat: client-grounded, model-free, offline).agent/— NEW separate package@forgeplan/web-agent(ADR-010: own deps@anthropic-ai/claude-agent-sdk+ws+zod, never in core): a 127.0.0.1 WebSocket daemon booting a persistent Agent SDKquery()session in a read-only profile (Read/Glob/Grep +show_on_map; deny Write/Edit/Bash), with an in-processcreateSdkMcpServershow_on_maptool that relays camera frames.bin/commands/onboard-agent.mjs— spawn-only subcommand (rule 23:spawns the package, never imports it).agent-client.ts(read-only WS client) +chat-storeTier-1 (stream → assistant bubble;show_on_map→ camera-bus; degrades to Tier 0 when the daemon is down).Invariants
ws://127.0.0.1;/api/*is never involved.bin/staysnode:*+citty+siblings (spawn-only); the SDK lives only in theagent/package; rootpackage.jsonuntouched.Test plan
npx vitest run src/widgets/map-chat src/widgets/composed-map→ 153/153.npx svelte-check→ 0 errors.node agent/scripts/smoke.mjs→ exit 0 (protocol + read-only profile + bind +/health+{ready}).bin/→ OK.show_on_map(EVID-096, CL3).To try it
npx @forgeplan/web onboard-agentin your project → the web chat detects it (● live) → ask away.Refs: PRD-038, RFC-034, ADR-010, EVID-096, RFC-033
🤖 Generated with Claude Code