## Context
The Dispatch-to-Agent button in the right-panel (previous commit) calls
two endpoints that did not yet exist server-side:
- `POST /api/agent-dispatch` — resolve the bead via the same Dolt pool
the existing `/api/beads/read` route uses, build a prompt from it, and
forward to `claude-agent-service`'s `/execute` endpoint with a bearer
token. This is the server-side trust boundary: the bearer token lives
in env (never in the browser), and the bead is re-read from Dolt
(never trusted from the client payload) so a malicious client cannot
inject a prompt.
- `GET /api/agent-status` — proxy `claude-agent-service`'s `/health`
endpoint (which already returns `{status, busy}`), with a 2 s in-
memory cache so 5 s UI polls across multiple open tabs don't hammer
the service.
The claude-agent-service serialises jobs behind an `asyncio.Lock` — a
second `/execute` while one is running returns HTTP 409 "Agent is busy".
We surface that 409 through to the browser unchanged so the UI can show
the right toast/status line without re-mapping status codes.
```
client ──► /api/agent-dispatch ──► claude-agent-service /execute
│ (asyncio.Lock guards)
└─► reads bead from Dolt pool (same path as /api/beads/read)
└─► buildDispatchPrompt(bead)
└─► POST {prompt, agent, budget, timeout} + Bearer token
client ──► /api/agent-status (2s cache) ──► /health ──► {busy: bool}
```
## This change
### `src/app/api/agent-dispatch/route.ts`
- `POST {taskId: string}` handler.
- Validates JSON body and non-empty taskId (→ 400).
- Early 500 if `CLAUDE_AGENT_SERVICE_URL` or `CLAUDE_AGENT_BEARER_TOKEN`
is missing — fail fast, fail loud.
- Resolves the bead via `readIssuesFromDisk({preferBd: true})`, which
uses the existing Dolt client (and falls back to `issues.jsonl`).
Filtering by id after is acceptable at BeadBoard's scale (~hundreds
of beads) and avoids introducing a new single-bead query path.
- 400 when the bead is missing, or when `acceptance_criteria?.trim()` is
empty — defense in depth alongside the UI disable. The button should
already hide in these cases, but a curl'd POST must still be rejected.
- Forwards to `${CLAUDE_AGENT_SERVICE_URL}/execute` with agent
`beads-task-runner`, max_budget_usd 5, timeout_seconds 900 (matches
the values in the task spec).
- Passes through 409 verbatim. Other upstream errors collapse to 502.
### `src/app/api/agent-status/route.ts`
- `GET` handler, module-level snapshot cache with 2 s TTL to avoid
hammering `/health`.
- In-flight de-dup: a single pending `fetchRemoteStatus()` is shared
across concurrent requests so we only hit the upstream once per
window even under bursty load.
- When `CLAUDE_AGENT_SERVICE_URL` is unset, returns `{busy: false}` and
skips the fetch entirely — this is how the dev server boots before
the service env is configured.
- HTTP 503 from upstream is interpreted as `busy: true` (future-proofing
in case the service swaps 409 for 503 on overload).
- Any network error degrades gracefully to `{busy: false}` — the 409
path on `/api/agent-dispatch` is the authoritative gate.
### Test coverage
- `tests/api/agent-dispatch-route.test.ts` (3 cases): invalid JSON body,
missing taskId, missing env returns 500.
- `tests/api/agent-status-route.test.ts` (3 cases): unset service URL
returns `{busy: false}` without a fetch, `/health busy:true` proxies
through, HTTP 503 maps to busy=true. Uses a `globalThis.fetch` stub
and cache-busting query params on dynamic import so each case starts
from a fresh module snapshot.
## What is NOT in this change
- End-to-end happy-path coverage (bead loads, fetch returns 200, route
yields job_id) would require mocking `readIssuesFromDisk` — that's a
bigger refactor than this commit warrants. Live integration happens
once the pipeline is wired.
- No retry/backoff on upstream failures. 502 passes through; the client
decides whether to retry.
- No auth on the routes themselves — they inherit the Next.js app's
session model (same as all other `/api/*` routes today).
## Test Plan
### Automated
```
$ node --import tsx --test tests/api/agent-dispatch-route.test.ts \
tests/api/agent-status-route.test.ts
# tests 6 pass 6 fail 0 duration_ms 1560
```
All six route cases pass. Typecheck output shows only the pre-existing
`OrchestratorChatMessage` gap in `left-panel.tsx`. Lint output is
unchanged from `main` (1 pre-existing `no-require-imports` error in
`src/lib/bb-pi-bootstrap.ts`).
### Manual Verification
1. Export env:
```
export CLAUDE_AGENT_SERVICE_URL=http://claude-agent-service.claude-agent.svc.cluster.local:8080
export CLAUDE_AGENT_BEARER_TOKEN=$(vault kv get -field=api_bearer_token secret/claude-agent-service)
```
2. `npm run dev`
3. `curl -s http://localhost:3000/api/agent-status` → `{"busy":false}`
(if the service is reachable; `{"busy":true}` if a job is running).
4. `curl -X POST http://localhost:3000/api/agent-dispatch \
-H 'content-type: application/json' \
-d '{"taskId":"beadboard-xyz"}'`
- 400 if bead does not exist or lacks acceptance criteria.
- 200 `{"job_id":"..."}` on success.
- 409 `{"error":"Agent is busy"}` when an agent is already running.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Context
We want a one-click path from the right-panel task detail into the
claude-agent-service runner, without the user copy-pasting the bead id
into a CLI or another tab. The runner expects a self-contained prompt
that restates the bead id, title, description, acceptance criteria, and
the guard rails the agent must operate under (no push, no file edits,
no terraform/kubectl/helm). The prompt template lives in `src/lib/` so
it can be tested and reused from the server-side dispatch route.
The right-panel button needs to:
- Only appear when the bead is actionable (`open` or `in_progress`).
- Disable itself while the claude-agent-service is already busy (the
service has a global `asyncio.Lock` — parallel dispatches 409).
- Disable itself when the bead lacks acceptance criteria. An agent that
doesn't know what "done" looks like burns budget and closes nothing.
- Surface the resulting `job_id` or any 409/error back to the user.
The project has no toast library (no `sonner`, no `react-hot-toast`), so
we render status inline under the button rather than pulling in a new
dependency for this single surface.
## This change
- `src/lib/dispatch-prompt.ts` exports `buildDispatchPrompt(bead)` which
produces the exact prompt the agent runner expects. Bead id, priority
(`P<n>`), issue type, description, and acceptance criteria are
interpolated; `<job_id>` stays a literal placeholder because the agent
only learns its own id at runtime (env var).
- `src/components/shared/dispatch-button.tsx` is a focused client
component with three responsibilities:
1. Poll `GET /api/agent-status` every 5 s while the panel is open
(plus an initial fetch on mount), mirror `busy` into local state.
2. On click, `POST /api/agent-dispatch` with `{taskId}`; branch on
200 / 409 / other.
3. Render an inline status line under the button (`text-xs`, tone
driven by `ok | info | error`) — no toast dep required.
The poll interval self-clears on unmount so closing the panel stops
network traffic.
- `src/components/shared/thread-drawer.tsx` renders `<DispatchButton>`
alongside the existing "Edit task" button in the summary section,
wrapped in a `flex-wrap` so the two controls reflow on narrow panes.
- Registers two new tests in `package.json`'s enumerated test script.
## What is NOT in this change
- The `/api/agent-dispatch` and `/api/agent-status` routes themselves —
those land in the next commit. The button calls them but the server
side is intentionally a separate step so each commit can be reviewed
in isolation.
- No real toast system is introduced; inline status is sufficient.
- No change to how task state transitions on dispatch. The agent itself
is expected to run `bd update --claim` / `bd close` via the prompt's
operating rules.
## Test Plan
### Automated
```
$ node --import tsx --test tests/lib/dispatch-prompt.test.ts \
tests/components/shared/dispatch-button.test.tsx
# tests 7 pass 7 fail 0
```
Covers:
- Bead id appears in opening paragraph and in both `bd note` / `bd close`
commands.
- Priority rendered as `P<n>`, issue type echoed.
- Description and acceptance criteria quoted verbatim when present.
- `(no description)` / `(no acceptance criteria)` fallbacks when null.
- Guard rails block present (no terraform/kubectl/helm, workspace bd
path, `bd update … --status blocked` fallback).
- DispatchButton module loads and exports both named and default.
`npm run typecheck` shows only the pre-existing `OrchestratorChatMessage`
type gap in `left-panel.tsx` that reproduces on untouched `main`.
### Manual Verification
1. `npm install`
2. `npm run dev`
3. Open `http://localhost:3000/?task=<some-open-bead-id>`
4. Expected: "Dispatch to Agent" button next to "Edit task" in the
right-panel summary section.
5. Button disabled on beads with `status in {closed, blocked, deferred}`
(they don't render the button at all).
6. Button disabled on beads missing acceptance criteria, with tooltip
"Task is missing acceptance criteria — cannot dispatch.".
7. Click: UI flips to "Dispatching…"; once the next commit is merged,
the agent-dispatch route will surface a `job_id` (today it returns
404 which renders as "Dispatch failed (HTTP 404)").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Context
The left-panel task list surfaces every bead regardless of whether the task
has been specified thoroughly enough for an agent to pick it up. Tasks with
empty `acceptance_criteria` or very short descriptions are low-signal noise
that muddles the navigation spine — especially now that we plan to dispatch
tasks to Claude agents (which need concrete instructions to succeed).
Epics are deliberately exempt: their role is grouping, not execution, so
requiring acceptance criteria / long descriptions on epics would hide the
entire backbone of the navigation.
## This change
- Plumbs `acceptance_criteria` end-to-end: adds the optional field on
`BeadIssue`, and reads it both from `.beads/issues.jsonl` (parser) and
Dolt SQL (`read-issues-dolt.ts` row shape + normalizer).
- Extends `LeftPanelFilters` with `hideNoAcceptance` and
`hideShortDescription` (both default `true`) in the source-of-truth
`hooks/use-url-state.ts` and in the re-exported shadow type on
`components/shared/left-panel.tsx`.
- Updates `isTaskMatch` in both `left-panel.tsx` and `left-panel-new.tsx`
to skip non-epic tasks lacking acceptance criteria or with a description
shorter than `SHORT_DESCRIPTION_MIN_LENGTH` (200 chars). Epics bypass
both filters via the `issue_type === 'epic'` guard.
- Exposes `isTaskMatch` from `left-panel.tsx` so the filter tests can
assert behavior directly (previously only `shouldHideEpicEntry` was
exported).
- Adds two checkboxes under the existing "Hide Closed" button in both
left-panel variants (legacy `left-panel.tsx` and the one unified-shell
currently wires up — `left-panel-new.tsx`).
- Seeds both new filter flags as `true` in the `UnifiedShell` default
state so fresh sessions see the high-signal view without toggling.
## What is NOT in this change
- No mutation of `bd` / CLI behavior. Filters are purely UI-level.
- No localStorage persistence for the two new flags — existing
`hideClosed` is also React-only, so parity is preserved. If/when we
persist any of these, all three move together.
- No change to the `metadata.acceptance` path used by `kanban.ts`'s
`hasQualitySignal` — that's a separate signal with its own callers.
## Test Plan
### Automated
Tests run from `/home/wizard/code/beadboard`:
```
$ node --import tsx --test tests/components/shared/left-panel-filtering.test.ts
# tests 15
# pass 15
# fail 0
# duration_ms 604
```
All 15 filter cases pass: 6 pre-existing `shouldHideEpicEntry` cases plus
9 new `isTaskMatch` cases covering acceptance-criteria-empty hides,
acceptance-criteria-disabled shows, epic exemption, 199/200-char
description boundary, null description, and short-description flag
disabled.
Related suites still green:
```
$ node --import tsx --test tests/components/shared/left-panel.test.tsx \
tests/components/shared/unified-shell-hide-closed-contract.test.ts
# tests 7 pass 7 fail 0
```
Pre-existing failures in `tests/hooks/url-state-integration.test.ts`
(`view=activity` cases) and one pre-existing typecheck error in
`left-panel.tsx` thread prop are unrelated — both reproduce on `main`
before this change.
### Manual Verification
1. `npm install`
2. `npm run dev`
3. Open `http://localhost:3000` with bd project loaded.
4. Expected: two new checkboxes appear under "Hide Closed" in the left
sidebar — "Hide tasks without acceptance criteria" and
"Hide tasks with short description (<200 chars)". Both checked by
default.
5. Toggle each off. Expected: additional beads appear in epic expansions
(tasks that were previously hidden because they lack quality signal).
6. Confirm epics remain visible regardless of the checkbox state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README.md:
- Remove duplicate sections, stale info, and broken markdown
- Add bb-pi orchestrator section with honest WIP status and known issues
- Clarify bd vs bb/beadboard CLI distinction
- Add cross-platform support section (Windows, macOS, Linux)
- Note Dolt as optional, document JSONL fallback
- Reference Pi SDK and community tools listing
orchestrator-panel.tsx:
- Add amber "Under construction" banner with link to track progress
- Visible on every orchestrator panel render
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
readIssuesFromDisk was throwing when Dolt was unreachable instead of
falling back to .beads/issues.jsonl on disk. This broke the app for
users without Dolt installed.
Restores the JSONL fallback path: try Dolt first, then read from the
git-tracked JSONL file, return empty array if neither is available.
Fixes#22
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move leftSidebarMode from URL state to local useState in unified-shell,
avoiding force-dynamic router round-trip that made the button appear broken - Replace fileURLToPath(new URL(..., import.meta.url)) with process.cwd()
in bb-pi-bootstrap.ts — import.meta.url is a webpack:// URL in Next.js,
causing cross-realm TypeError when passed to Node.js fileURLToPath()
- pathing.ts: use path.resolve() on POSIX instead of win32.normalize
- registry.ts: replace ensureWindowsAbsolutePath with path.isAbsolute()
- Tests: platform-conditional assertions for both Windows and POSIX
- Windows behavior preserved unchanged via os.platform() guard
All 17 tests pass on macOS. Windows tests guarded behind IS_WINDOWS.
Critical Security Fixes:
- Fix command injection vulnerability in Windows shims (beadboard.cmd, bb.cmd)
- Added path validation to block traversal (.. and root-relative paths)
- Added quotes around env var to prevent command injection
Reliability Fixes:
- Fix agent cache null safety bug
- Fixed callBdAgentShow() to check for cache misses (null check, expiration)
- Fixed getCachedAgent to properly return entry.data or null
- Fix null body crashes in mail ack route
- Added null check before casting body to object
- Returns 400 error instead of 500 for invalid requests
BD Compliance Fixes:
- Fix read-issues to use BD audit record path
- Ensures all writes go through bd audit record
- Maintains watcher/SSE parity and Dolt commit tracking
Code Quality Fixes:
- Fix path canonicalization violations
- Use canonicalizeWindowsPath() and windowsPathKey() from pathing module
- Prevents Windows edge cases and ensures machine-reproducible paths
- Fix typo: mobile-fronted → mobile-frontend
- Pin GitHub Actions tags
- softprops/action-gh-release@v1 → specific commit hash
- Register pr14 test in package.json (already registered)
Testing:
- Refactor broad exception handlers in Python scripts
- Replace except Exception: with specific exceptions
- Allows KeyboardInterrupt and SystemExit to propagate correctly
- All tests passing
- Added memory-anchor filter to left-panel.tsx
- Removed issues.jsonl fallback in read-issues.ts (Dolt-only)
- Frontend still shows stale data despite these changes
- Root cause NOT identified - see NEXT_SESSION_PROMPT.md for details
- Add import for BlockedTriageModal in unified-shell.tsx
- Add blockedTriageOpen state and handlers
- Pass onOpenBlockedTriage prop to TopBar
- Render BlockedTriageModal at end of UnifiedShell
- Add onOpenBlockedTriage prop to TopBarProps interface
- Update blocked items button onClick to use onOpenBlockedTriage
- Create BlockedTriageModal component at src/components/shared/blocked-triage-modal.tsx
- Implements modal with blocked task triage functionality
- Uses deriveBlockedIds and buildBlockedByTree from kanban lib
- Each row shows blocker chain and has inline archetype picker
- Modal is scrollable and closes via Escape/close button
- Add corresponding tests at tests/components/blocked-triage-modal.test.tsx
- Register test in package.json test script
TelemetryStrip now fetches from the same /api/activity endpoint and
subscribes to the same /api/events SSE stream as ActivityPanel. The
minimized dots use getEventTone() colors matching the full feed exactly
(created=green, closed=amber, reopened=blue, etc.) instead of derived
task status counts.
Co-Authored-By: Oz <oz-agent@warp.dev>
- Remove duplicate Signal (telemetry) button from DAG nodes
- Add minimize (ChevronLeft) button to Epic Command Feed view, not just global feed
- TelemetryStrip now shows 8 most recently updated tasks as status-colored dots
instead of static status counts — reflects live activity like the full feed does
- Each dot is colored by task status (blocked=red, active=amber, ready=green)
with hover tooltip showing task id, title, and status
Co-Authored-By: Oz <oz-agent@warp.dev>
- Removed broken LaunchSwarmDialog (formula-based) from TopBar/LeftPanel
- All Rocket buttons (TopBar, LeftPanel, DAG nodes, social cards) now open
AssignmentPanel (archetype-based) which actually works
- Every Rocket clears taskId first so assignMode && !taskId condition passes
- Conversation button priority: taskId always shows conversation, not assign panel
- Added TelemetryStrip: minimized right sidebar with status dots when non-telemetry
panel (conversation/assignment) is active
- Live feed has minimize button → restores last taskId or assignMode
- DAG nodes: Signal icon → restores telemetry feed
- Social button on DAG nodes: single router.push to avoid race (setView + setTaskId)
- Fixed social card message button: opens right panel with drawer:closed (no popup)
Co-Authored-By: Oz <oz-agent@warp.dev>
- Add MessageSquare icon to GraphNodeCard; prop-thread onConversationOpen
and selectedTaskId through WorkflowGraph node data (no useUrlState
inside ReactFlow nodes — avoids context/timing issues)
- Fix ContextualRightPanel: check taskId before epicId so clicking the
conversation icon always opens ThreadDrawer even when an epic filter
is active
- setEpicId now clears task from URL so selecting an epic resets any
open conversation thread
- handleGraphSelect toggles: second click on same node calls setTaskId(null)
closing the right panel
- Add onSelect to WorkflowGraph flowModel deps to prevent stale callbacks
- Fix ContextualRightPanel onClose no-ops: wired to setTaskId(null) /
setSwarmId(null) so back button works
- Right panel always visible (removed panel==='open' gate in UnifiedShell)
- SmartDag task grid: horizontal scroll, fixed-width cards, hideClosed=true
- Add <Suspense> in page.tsx for useSearchParams compatibility
- Enable dolt auto-start in .beads/config.yaml
- Add 14 static analysis tests (graph-node-conversation.test.tsx)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/lib/read-issues-dolt.ts: readIssuesViaDolt() queries issues+labels (GROUP_CONCAT)
and dependencies in 2 SQL queries; normalizes Date cols to ISO strings; returns null
on unreachable so caller can fall back gracefully
- src/lib/read-issues.ts: readIssuesFromDisk() tries Dolt first (always), falls back to
issues.jsonl with console.warn; removes dead readIssuesViaBd/normalizeBdIssue/
normalizeDependencies code now that the CLI path is superseded
- AGENTS.md: documents new Dolt read path + SSE watcher trigger; removes stale
manual issues.jsonl re-export instructions (no longer needed)
Verified: bd writes update last-touched → chokidar fires → syncActivity → Dolt query
→ snapshot diff → SSE push. 146/146 tests pass, lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/lib/dolt-client.ts: getDoltConnection(projectRoot) returns cached mysql2 Pool
- Reads host/port/database from .beads/metadata.json (no hardcoded values)
- DoltConnectionError typed error for missing metadata or unreachable server
- Tests connectivity on first connection; caches pool per resolved projectRoot
- connectionLimit: 5 for Next.js server-side concurrency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 0:
- UnifiedShell: pass blockedOnly to SocialPage; wire TopBar with live counts
- thread-drawer: show real issue.status instead of hardcoded "In Progress"
- social-page: fix onJumpToActivity to open right panel (not dead ?view=activity)
Phase 1:
- contextual-right-panel: add taskId branch (ThreadDrawer embedded) and swarmId
branch (MissionInspector via SwarmIdBranch inner component); ActivityPanel
remains the no-selection fallback
All 207 tests pass; no new typecheck errors.
Closes beadboard-r1i (Phase 1: Contextual Right Panel)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Aurora: Real northern lights colors (teal/violet on deep blue-black)
- Midnight: Deep ocean blues with violet and cyan
- Forest: Rich forest greens with golden sunlight
- Dusk: Warm sunset oranges and corals
- Contrast: Pure black with neon accents
- Light: Clean professional light mode (NEW)
All themes have:
- Consistent status colors (green/amber/red)
- Distinct surface layers for visual hierarchy
- Theme-appropriate accent glows
- Improved contrast and accessibility
- Replace hardcoded blue radial gradient with theme variable
- Update graph-view to use new token system
- Ensure status colors (red/green/amber) remain consistent across all themes
- DAG background now changes with theme while maintaining card visibility
- Create ThemeToggle component with dropdown menu
- Shows all 5 themes with descriptions
- Persists choice to localStorage
- Updates data-theme attribute instantly
- Add palette icon to top bar
- aurora: Warm charcoal with cyan accents (fixed/improved)
- midnight: Cool blue-purple with violet accents
- forest: Earthy green-brown with lime accents
- dusk: Warm purple-brown with orange/pink accents
- contrast: Pure black with neon accents (high contrast)
All themes use the same 12-category token system for consistency.
Switch themes by changing data-theme attribute on html element.