Skip to content

feat(idef0): Pillar C — live onboarding agent (daemon + Agent SDK + camera chat) - #169

Merged
explosivebit merged 17 commits into
developfrom
feat/idef0-onboard-agent-phase1
Jul 8, 2026
Merged

explosivebit merged 17 commits into
developfrom
feat/idef0-onboard-agent-phase1

Conversation

@explosivebit

Copy link
Copy Markdown
Contributor

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 called show_on_map{zone: z.surfaces} and narrated the zone flow. Real model turn, grounded, camera-driven.

What's in it

  • RFC-034 + ADR-010 (active) — the daemon/protocol/camera/chat architecture + the packaging decision (separate optional package + spawn-only subcommand).
  • Phase 1camera-bus seam (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 SDK query() session in a read-only profile (Read/Glob/Grep + show_on_map; deny Write/Edit/Bash), with an in-process createSdkMcpServer show_on_map tool that relays camera frames.
  • bin/commands/onboard-agent.mjs — spawn-only subcommand (rule 23: spawns the package, never imports it).
  • Tier-1 web wiringagent-client.ts (read-only WS client) + chat-store Tier-1 (stream → assistant bubble; show_on_map → camera-bus; degrades to Tier 0 when the daemon is down).

Invariants

  • Rule 22: the live path is browser↔daemon over ws://127.0.0.1; /api/* is never involved.
  • Rule 23 / ADR-010: core bin/ stays node:*+citty+siblings (spawn-only); the SDK lives only in the agent/ package; root package.json untouched.
  • Read-only: the agent cannot mutate the workspace.

Test plan

  • npx vitest run src/widgets/map-chat src/widgets/composed-map153/153.
  • npx svelte-check0 errors.
  • node agent/scripts/smoke.mjs → exit 0 (protocol + read-only profile + bind + /health + {ready}).
  • Rule-23 allow-list grep over bin/ → OK.
  • Live end-to-end turn (above) — daemon + local CC + show_on_map (EVID-096, CL3).

To try it

npx @forgeplan/web onboard-agent in 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

explosivebit and others added 17 commits July 6, 2026 17:39
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
explosivebit merged commit c703308 into develop Jul 8, 2026
3 checks passed
@explosivebit
explosivebit deleted the feat/idef0-onboard-agent-phase1 branch July 8, 2026 16:48
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)
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant