Skip to content

feat: daemon web UI - #122

Merged
pedronauck merged 23 commits into
mainfrom
daemon-web-ui
Apr 21, 2026
Merged

pedronauck merged 23 commits into
mainfrom
daemon-web-ui

Conversation

@pedronauck

@pedronauck pedronauck commented Apr 21, 2026 •

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Web UI: dashboard, workflow boards, task/review/run detail pages, workflow spec & memory views, SPA static serving, optional frontend dev-proxy and CLI flag
    • Browser protections: CSRF, host/origin validation, active-workspace header propagation
    • Run streaming: snapshot support plus canonical heartbeat/overflow/event framing
    • UI library & Storybook: new components, tokens, stories, and Vitest/Playwright tests
  • Refactor

    • Frontend verification pipeline and Makefile targets reorganized; Bun runtime pinned via .bun-version
    • New daemon query/read-model layer and transport mappers powering UI endpoints
  • Tests

    • Extensive unit/integration/e2e test additions for HTTP, middleware, query/read-models, and static serving

@pedronauck pedronauck self-assigned this Apr 21, 2026
@coderabbitai

coderabbitai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 87d87201-c7f4-4e77-8c30-a21cea84b7ed

📥 Commits

Reviewing files that changed from the base of the PR and between e183959 and bd28690.

⛔ Files ignored due to path filters (156)
  • .codex/ledger/2026-04-20-MEMORY-daemon-query-layer.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-daemon-web-ui-review-002.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-hot-reload-dev.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-web-ui-qa-exec.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-web-ui-qa.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-workspace-security-sse.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/_tasks.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/MEMORY.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_01.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_02.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_03.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_04.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_05.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_06.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_07.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_08.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_09.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_10.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_11.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_12.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_13.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_14.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_15.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_16.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/issues/BUG-001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/logs/01-contract-discovery.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/02-baseline-make-verify.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/03-smoke-workspace-bootstrap.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/04-smoke-playwright.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/05-targeted-review-fix.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/06-targeted-spec-memory.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/07-targeted-runs.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/08-targeted-route-stories.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/09-run-start-inspection.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/10-manual-browser-env.json is excluded by !**/*.json
  • .compozy/tasks/daemon-web-ui/qa/logs/10-manual-browser-harness-setup.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/11-manual-browser-flows.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/12-manual-browser-harness-teardown.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/13-run-start-regression.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/14-post-fix-playwright.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/15-run-start-playwright-only.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/16-run-start-curl-body.json is excluded by !**/*.json
  • .compozy/tasks/daemon-web-ui/qa/logs/17-rebuild-embedded-assets.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/18-run-start-playwright-rebuilt.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/19-manual-harness-setup.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-post-body.json is excluded by !**/*.json
  • .compozy/tasks/daemon-web-ui/qa/logs/21-run-start-playwright-clean-rerun.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/22-run-start-playwright-response-sync.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/23-run-start-playwright-nonconflicting-seed.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/24-manual-browser-start-run.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/25-manual-browser-start-run-ref.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/26-playwright-screenshot-start-run.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/27-manual-harness-teardown.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/28-final-make-verify.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/29-post-tracking-make-verify.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/screenshots/memory-detail.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/review-detail.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/reviews-index.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/run-detail-direct.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/run-detail.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/spec-techspec.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/task-board.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/task-detail.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/workflow-start-success.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/workflows-inventory.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-006.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-007.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-008.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-plans/daemon-web-ui-regression.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-plans/daemon-web-ui-test-plan.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/verification-report.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/_meta.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_007.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_008.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_009.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_010.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_011.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_012.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_013.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_014.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_015.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_016.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_017.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_018.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_019.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_020.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_021.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_022.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_023.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_024.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_025.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_026.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_027.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_028.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_029.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_030.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_031.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/_meta.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_007.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_008.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_009.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_010.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_011.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/_meta.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_007.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_01.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_02.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_03.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_04.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_05.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_06.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_07.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_08.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_09.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_10.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_11.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_12.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_13.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_14.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_15.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_16.md is excluded by !**/*.md
  • .github/actions/setup-bun/action.yml is excluded by !**/*.yml
  • .github/workflows/ci.yml is excluded by !**/*.yml
  • .github/workflows/claude.yml is excluded by !**/*.yml
  • CHANGELOG.md is excluded by !**/*.md
  • bun.lock is excluded by !**/*.lock
  • docs/examples/agents/repo-copilot/mcp.json is excluded by !**/*.json
  • docs/plans/2026-04-21-hot-reload-dev-design.md is excluded by !**/*.md
📒 Files selected for processing (21)
  • .bun-version
  • .compozy/tasks/daemon-web-ui/qa/issues/.gitkeep
  • .compozy/tasks/daemon-web-ui/qa/logs/16-run-start-curl-headers.txt
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-cookiejar.txt
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-get-headers.txt
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-post-headers.txt
  • .compozy/tasks/daemon-web-ui/qa/screenshots/.gitkeep
  • .gitignore
  • .prettierignore
  • Makefile
  • docs/design/daemon-mockup/colors_and_type.css
  • internal/api/client/runs.go
  • internal/api/client/runs_test.go
  • internal/api/contract/routes.go
  • internal/api/core/errors.go
  • internal/api/core/handlers.go
  • internal/api/core/handlers_contract_test.go
  • internal/api/core/handlers_error_paths_test.go
  • internal/api/core/handlers_service_errors_test.go
  • internal/api/core/handlers_smoke_test.go
  • internal/api/core/handlers_test.go

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

Adds a browser-oriented web UI and read-model layer: Bun pinning and frontend build/verify targets, embedded SPA static serving with optional dev-proxy, browser middleware (host/origin/CSRF/active-workspace), QueryService read-models and transport mappers for dashboard/workflow/task/review/run details, SSE snapshot support, UI component library/Storybook, and extensive tests.

Changes

Cohort / File(s) Summary
Build / Tooling
\.bun-version, Makefile, \.gitignore, \.prettierignore, scripts/codegen.mjs
Add .bun-version; Makefile enforces/checks Bun and adds frontend bootstrap/lint/typecheck/test/build/e2e/verify targets and dev; adjust ignore/format rules for generated/dist files; add deterministic OpenAPI TypeScript codegen script with --check.
Frontend package & Storybook
packages/ui/..., packages/ui/.storybook/*, packages/ui/vitest.config.ts, packages/ui/tests/*, web/.storybook/*
Introduce UI component library, utilities, tokens.css, Storybook configs, Vitest config and many UI stories/tests; add public exports entrypoint and token/theme files.
Static serving & dev proxy
internal/api/httpapi/static.go, internal/api/httpapi/static_test.go, internal/api/httpapi/dev_proxy.go, internal/api/httpapi/dev_proxy_test.go, internal/api/httpapi/server.go
Add embedded static FS handler with SPA fallback; add dev-proxy reverse-proxy; server now selects dev-proxy or embedded static handler at startup; include tests for static serving, deep-links, and proxy behavior.
Browser middleware & HTTP contract
internal/api/httpapi/browser_middleware.go, internal/api/httpapi/browser_middleware_test.go, internal/api/httpapi/openapi_contract_test.go, internal/api/httpapi/routes.go
Add host/origin validation, active-workspace header resolution/injection, CSRF cookie/header enforcement and helpers; extend route registration for UI/task/review endpoints; add OpenAPI contract tests.
API core — SSE / run streaming
internal/api/core/sse.go, internal/api/core/sse_test.go, internal/api/client/runs.go, internal/api/client/runs_test.go, pkg/compozy/runs/*
Introduce canonical SSE event names (run.snapshot, run.event, run.heartbeat, run.overflow), add snapshot framing/dispatching paths in client/server streams, and update tests for new frames and legacy-name compatibility.
API handlers, interfaces & middleware
internal/api/core/handlers.go, internal/api/core/routes.go, internal/api/core/interfaces.go, internal/api/core/middleware.go, internal/api/core/errors.go
Add handlers for dashboard, board/spec/memory/board/item, review issue; expand Task/Review/Run interfaces and DTOs; add workspace-context helpers, workspace-related error mapping, and default error code behavior; update handlers to use new workspace/context flows.
Query / read-model layer
internal/daemon/query_models.go, internal/daemon/query_service.go, internal/daemon/query_documents.go, internal/daemon/query_helpers_test.go, internal/daemon/query_service_test.go
Add QueryService and daemon-side DTOs for dashboard, workflow overview, task board, spec/memory, task/review/run detail; implement markdown front-matter parsing, caching, title heuristics, memory-file ID generation, directory enumeration, and related tests.
Transport mappers & services
internal/daemon/transport_mappers.go, internal/daemon/task_transport_service.go, internal/daemon/review_exec_transport_service.go, internal/daemon/transport_*_test.go
Wire QueryService into transport task/review services; add mappers converting daemon read-models to apicore payloads; add mapQueryTransportError to translate typed query errors into apicore Problems; update transport constructors to accept optional QueryService.
Daemon host / CLI integration
internal/daemon/host.go, internal/daemon/host_runtime_test.go, internal/cli/daemon.go, internal/cli/daemon_commands_test.go
Add RunOptions.WebDevProxyTarget and WithDevProxyTarget; CLI --web-dev-proxy flag/env handling and validation; propagate dev-proxy target into server startup; tests for flag/env resolution and override behavior.
Run manager & snapshot tests
internal/daemon/run_manager.go, internal/daemon/run_manager_test.go, internal/daemon/run_snapshot_test.go
Add RunDetail passthrough to query layer; adjust workflow context resolution and cancellation reason default; add comprehensive run snapshot builder lifecycle tests.
GlobalDB read APIs
internal/store/globaldb/read_queries.go, internal/store/globaldb/read_queries_test.go, internal/store/globaldb/query_coverage_test.go
Add read queries and row scanners for task items, artifact snapshots, and review rounds; introduce ErrTaskItemNotFound and row types; tests for ordering, not-found behavior, and JSON parsing.
Transport-level read-model tests & mapping validation
internal/daemon/transport_read_models_test.go, internal/daemon/transport_service_test.go, internal/daemon/transport_mappers.go
Large test suites validating transport mappings, deep-copy semantics, typed error mapping, and end-to-end read-model outputs produced from run lifecycle fixtures.
Dev infra / QA / E2E
.compozy/tasks/daemon-web-ui/qa/*, test/frontend-*.test.ts, web/e2e/*, many internal/*_test.go
Add QA artifacts (cookie jar, header logs), frontend verification and workspace config tests, Playwright e2e smoke tests for embedded UI, many Go test additions and QA placeholders.
Store & watcher updates
internal/daemon/query_documents.go, internal/daemon/watcher_error_test.go, internal/store/...
Add document reader, directory scanning, stable memory IDs, and watcher error test; various store query coverage tests added.
UI primitives & stories
packages/ui/src/components/*, packages/ui/src/index.ts, packages/ui/src/lib/utils.ts
Add reusable UI primitives (AppShell, Button, SurfaceCard, SectionHeading, StatusBadge, UIProvider), class utility cn, component stories, and package exports with tests.
Misc / Tests / Small updates
internal/cli/daemon_exec_test_helpers_test.go, internal/core/extension/*, docs/design/*, other small files
Test synchronization and HOME isolation helpers, CSS token tweaks, small test adjustments, and .gitkeep artifacts to retain QA dirs.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser (Client)
    participant Middleware as HTTP Middleware
    participant Server as HTTP Server
    participant Handler as API Handler
    participant Query as QueryService
    participant DB as GlobalDB
    Browser->>Middleware: HTTP request (Host/Origin/Workspace/CSRF)
    Middleware->>Middleware: validate Host/Origin/CSRF<br/>resolve workspace header
    Middleware->>Server: inject context & continue
    Server->>Handler: invoke GetDashboard (with workspace ctx)
    Handler->>Query: Dashboard(ctx, workspaceID)
    Query->>DB: list workflows / read docs / list runs
    DB-->>Query: rows / files / snapshots
    Query-->>Handler: WorkspaceDashboard payload
    Handler-->>Browser: 200 OK (dashboard JSON)
Loading
sequenceDiagram
    participant Client as SSE Client
    participant SSE as SSE Decoder/Proxy
    participant Items as Stream Items Channel
    participant Consumer as Consumer
    Client->>SSE: open SSE stream (Last-Event-ID / cursor)
    SSE->>SSE: receive frame event: run.snapshot
    SSE->>SSE: decode snapshot, parse next_cursor
    SSE->>Items: send RunStreamItem{Snapshot: ...}
    SSE->>SSE: receive frame event: run.heartbeat
    SSE->>Items: send RunStreamItem{Heartbeat: ...}
    SSE->>SSE: receive frame event: run.event
    SSE->>Items: send RunStreamItem{Event: ...}
    Consumer->>Items: read items
    Items-->>Consumer: yields snapshot, heartbeat, event (in order)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • refactor: daemon improvs #121: Strongly related—both introduce canonical SSE event names and snapshot handling for run streams, affecting client/server dispatching and cursor semantics.
  • stack/loop lifecycle 03 web qa #297: Overlaps SSE/run-stream and transport plumbing changes; likely touches the same streaming/event areas.
  • feat!: migrate to daemon #112: Related API/core and httpapi changes (routes, EventMessage, middleware and handler wiring) that intersect with handler and routing updates in this PR.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch daemon-web-ui

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (7)
packages/ui/src/tokens.css-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Fix current Stylelint violations in this file.

These flagged issues are straightforward and will keep lint green: unquoted single-word font family, keyword-case warnings in font stack, missing empty line, and text-rendering value case.

Proposed fix
 `@font-face` {
-  font-family: "Nippo";
+  font-family: Nippo;
   src: url("./assets/fonts/Nippo-Variable.woff2") format("woff2");
@@
-  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+  --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", "Menlo", "Monaco", "Consolas", monospace;
@@
   body {
     `@apply` bg-background font-sans text-foreground antialiased;
+
     margin: 0;
-    text-rendering: optimizeLegibility;
+    text-rendering: optimizelegibility;
     -webkit-font-smoothing: antialiased;
   }

Also applies to: 93-93, 232-233

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/tokens.css` at line 2, Remove unnecessary quotes around
single-word font-family names (change "Nippo" to Nippo) and ensure generic
family keywords in any font stacks are lowercase (e.g., sans-serif). Normalize
text-rendering values to lowercase (e.g., optimizelegibility →
optimizeLegibility in lowercase form) and add a trailing newline / empty line at
the end of the file. Update the font-family declarations and any text-rendering
properties found in this file (search for font-family and text-rendering
occurrences) so they conform to Stylelint keyword-case and quoting rules and
ensure the file ends with an empty line.
packages/ui/src/tokens.css-172-172 (1)

172-172: ⚠️ Potential issue | 🟡 Minor

Remove .stylelintrc.json or fix it for Tailwind at-rules.

The .stylelintrc.json config extends stylelint-config-standard-scss, which enables at-rule-no-unknown by default. This would flag @theme as unknown in IDEs with Stylelint extensions, despite being valid Tailwind syntax. Since Stylelint isn't installed or used in this project's CI pipeline (linting is handled by oxlint/oxfmt), either remove the unused config file or add ignoreAtRules: ["theme"] to suppress the warning for IDE users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/tokens.css` at line 172, Remove the unused Stylelint config
or update it to ignore Tailwind's `@theme` at-rule: either delete the
.stylelintrc.json file (since linting uses oxlint/oxfmt) or modify the config
that extends stylelint-config-standard-scss to add ignoreAtRules: ["theme"] so
the at-rule-no-unknown rule won't flag `@theme`; look for the .stylelintrc.json
entry and the at-rule-no-unknown configuration to apply the change.
.compozy/tasks/daemon-web-ui/qa/logs/.gitkeep-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Remove the redundant .gitkeep file.

This directory already contains 28 tracked files (log files and snapshots), making .gitkeep unnecessary. The .gitkeep convention is only needed for empty directories; since this directory is not empty, the file can be safely deleted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.compozy/tasks/daemon-web-ui/qa/logs/.gitkeep at line 1, Remove the
redundant .gitkeep file: delete the .gitkeep entry from the logs directory that
already contains tracked log files and snapshots, leaving the directory as-is
without the placeholder file.
.compozy/tasks/daemon-web-ui/qa/logs/20-run-start-cookiejar.txt-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor

Avoid committing live CSRF token values in QA log artifacts.

Please redact/sanitize the cookie value (or keep this class of logs untracked) to reduce token exposure patterns and snapshot churn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.compozy/tasks/daemon-web-ui/qa/logs/20-run-start-cookiejar.txt at line 5,
The QA artifact contains a live CSRF cookie value ("compozy_csrf"); update the
log generation/sanitization step that writes cookiejar artifacts so it redacts
or replaces cookie values for keys like "compozy_csrf" (e.g., replace the value
with "[REDACTED]" or a stable hash) before writing or commit, or stop tracking
these cookiejar artifacts entirely; ensure the change is applied where cookie
serialization occurs so entries matching the cookie name are filtered/sanitized.
pkg/compozy/runs/run.go-824-826 (1)

824-826: ⚠️ Potential issue | 🟡 Minor

Do not silently ignore malformed next_cursor in snapshot frames.

dispatchSnapshot currently drops parse errors and continues, which can mask protocol/data issues and leave cursor state stale.

Proposed fix
 func (s *daemonRunStream) dispatchSnapshot(raw []byte) error {
 	var payload daemonRunSnapshotPayload
 	if err := json.Unmarshal(raw, &payload); err != nil {
 		return fmt.Errorf("decode snapshot frame: %w", err)
 	}
 	snapshot := RemoteRunSnapshot{
 		Status: normalizeStatus(payload.Run.Status),
 	}
-	if cursor, err := parseRemoteCursor(payload.NextCursor); err == nil && cursor.Sequence > 0 {
-		snapshot.NextCursor = &cursor
-	}
+	if strings.TrimSpace(payload.NextCursor) != "" {
+		cursor, err := parseRemoteCursor(payload.NextCursor)
+		if err != nil {
+			return fmt.Errorf("decode snapshot cursor: %w", err)
+		}
+		if cursor.Sequence > 0 {
+			snapshot.NextCursor = &cursor
+		}
+	}
 	return s.sendItem(RemoteRunStreamItem{
 		Snapshot: &snapshot,
 	})
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/compozy/runs/run.go` around lines 824 - 826, The code in dispatchSnapshot
silently ignores errors from parseRemoteCursor when handling payload.NextCursor;
instead, detect parse errors for payload.NextCursor and propagate them (or at
minimum log and return an error) so malformed next_cursor doesn't get dropped
and stale state isn't used. Modify the dispatchSnapshot logic around the
parseRemoteCursor call to check the error (from
parseRemoteCursor(payload.NextCursor)) and return or bubble up that error (or
wrap it with context) rather than proceeding to set snapshot.NextCursor only on
success; reference parseRemoteCursor, payload.NextCursor, snapshot.NextCursor
and the dispatchSnapshot function when making the change.
internal/api/httpapi/openapi_contract_test.go-195-202 (1)

195-202: ⚠️ Potential issue | 🟡 Minor

Filter path-item keys to real HTTP methods.

This helper currently turns every key under a path item into a route key. If the spec starts using path-level parameters, summary, or servers, the test will invent fake routes and fail for the wrong reason. Restrict this loop to the OpenAPI operation methods only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/openapi_contract_test.go` around lines 195 - 202, The
loop over pathItem currently treats every key (including path-level keys like
"parameters", "summary", "servers") as an operation; update the iteration that
builds keys (where you do keys = append(keys, strings.ToUpper(method)+" "+path))
to only accept real OpenAPI operation names (e.g. get, put, post, delete,
options, head, patch, trace). Implement this by normalizing method to lower-case
and checking it against a small set/lookup of allowed methods before appending
the route key (use the existing variables paths, pathItem and the method loop to
locate and change the logic).
internal/api/httpapi/browser_middleware.go-297-303 (1)

297-303: ⚠️ Potential issue | 🟡 Minor

Use type assertion instead of error string matching for net.SplitHostPort.

Branching on strings.Contains(err.Error(), "missing port in address") is brittle—error messages are not documented as stable API and can change across Go versions. Instead, use a type assertion to *net.AddrError to check the error condition reliably:

Suggested approach
host, port, err := net.SplitHostPort(trimmed)
if err == nil {
	return normalizeAuthorityHost(host), strings.TrimSpace(port)
}
var addrErr *net.AddrError
if errors.As(err, &addrErr) && strings.Contains(addrErr.Err, "missing port") {
	return normalizeAuthorityHost(trimmed), ""
}
return "", ""

Alternatively, treat any unparseable input as host-only authority for more semantic robustness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/browser_middleware.go` around lines 297 - 303, Replace
the brittle error string matching after net.SplitHostPort by checking the
concrete error type: use errors.As to try to cast err to *net.AddrError (e.g.,
var addrErr *net.AddrError; if errors.As(err, &addrErr) &&
strings.Contains(addrErr.Err, "missing port") { return
normalizeAuthorityHost(trimmed), "" }) and otherwise return the empty failure
case; reference the net.SplitHostPort call and normalizeAuthorityHost to locate
the change and ensure you import "errors" if not already present.
🧹 Nitpick comments (7)
packages/ui/src/components/section-heading.tsx (1)

5-34: Consider making heading level configurable for semantic flexibility.

At Line 32, always rendering h1 can produce invalid heading hierarchy in nested views. Adding an as prop improves reuse without changing default behavior.

♻️ Suggested refactor
 export interface SectionHeadingProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
   actions?: ReactNode;
+  as?: "h1" | "h2" | "h3" | "h4";
   description?: ReactNode;
   eyebrow?: ReactNode;
   title: ReactNode;
 }

 export function SectionHeading({
   actions,
+  as = "h1",
   className,
   description,
   eyebrow,
   title,
   ...props
 }: SectionHeadingProps): ReactElement {
+  const HeadingTag = as;
   return (
     <header
       className={cn("flex flex-col gap-5 md:flex-row md:items-end md:justify-between", className)}
       {...props}
     >
       <div className="space-y-2">
         {eyebrow ? (
           <p className="font-disket text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
             {eyebrow}
           </p>
         ) : null}
         <div className="space-y-2">
-          <h1 className="font-display text-[clamp(2rem,4vw,3rem)] leading-none tracking-[-0.03em] text-foreground">
+          <HeadingTag className="font-display text-[clamp(2rem,4vw,3rem)] leading-none tracking-[-0.03em] text-foreground">
             {title}
-          </h1>
+          </HeadingTag>
           {description ? (
             <p className="max-w-3xl text-sm leading-7 text-muted-foreground">{description}</p>
           ) : null}
         </div>
       </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/section-heading.tsx` around lines 5 - 34, The
component always renders an h1 which breaks heading hierarchy; update
SectionHeadingProps to accept an optional as prop (e.g., as?: keyof
JSX.IntrinsicElements or a React element type) with default 'h1', then render
the title using that tag instead of the hard-coded <h1> inside the
SectionHeading function (keep the prop name title and existing behavior when as
is not provided); ensure types for SectionHeadingProps and the render path
(SectionHeading) are updated so className and other attributes apply correctly
to the chosen heading element.
packages/ui/src/components/button.tsx (1)

35-57: Enforce accessible naming for icon-only buttons.

At Lines 51-56, icon-only usage is currently possible without an accessible label. Tightening props prevents silent a11y regressions.

♿ Suggested type-safe API guard
-export interface ButtonProps
-  extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
-  icon?: ReactNode;
-}
+type ButtonBaseProps = ButtonHTMLAttributes<HTMLButtonElement> &
+  VariantProps<typeof buttonVariants>;
+
+type ButtonWithTextProps = {
+  children: ReactNode;
+  icon?: ReactNode;
+};
+
+type IconOnlyButtonProps = {
+  children?: never;
+  icon: ReactNode;
+  "aria-label": string;
+};
+
+export type ButtonProps = ButtonBaseProps & (ButtonWithTextProps | IconOnlyButtonProps);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/button.tsx` around lines 35 - 57, Button allows
icon-only buttons without an accessible name; update the ButtonProps/type so any
button that provides icon but no children must require an accessible label
(e.g., 'aria-label') at the type level. Concretely, refactor the exported
ButtonProps used by the Button component into a discriminated union: one branch
for regular buttons (children: ReactNode, icon optional) and another for
icon-only buttons (icon: ReactNode and no children) that requires 'aria-label':
string (or equivalent aria-labelledby) so TypeScript enforces an accessible name
when children is absent; update the Button function signature to use this new
ButtonProps shape and ensure the prop references (icon, children) remain the
same.
packages/ui/tests/package-exports.test.ts (1)

40-42: Consider tightening the leak-detection regex.

Current substring matching can produce false positives for legitimate names. Word boundaries (or a curated denylist) will make this test less brittle.

Optional refinement
-    expect(exportNames.some(name => /(route|workflow|task|review|run|page)/i.test(name))).toBe(
+    expect(exportNames.some(name => /\b(route|workflow|task|review|run|page)\b/i.test(name))).toBe(
       false
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/tests/package-exports.test.ts` around lines 40 - 42, The export
leak test uses a loose regex on exportNames which causes false positives;
tighten it by replacing the current /(route|workflow|task|review|run|page)/i
with a word-boundary or explicit denylist pattern (e.g.,
/\b(?:route|workflow|task|review|run|page)\b/i or a curated array of exact names
checked against exportNames) in the test that asserts exportNames.some(...).
Update the test handling around exportNames to use the new regex or denylist so
only whole-word matches (or exact denylisted names) trigger failure.
scripts/codegen.mjs (1)

70-78: Keep --check mode read-only.

--check currently rewrites web/src/generated/compozy-openapi.d.ts before failing. Consider making check mode non-mutating and leaving writes to normal mode only.

Suggested adjustment
   if (next !== current) {
-    writeFileSync(target, next);
     console.error(
       [
         "codegen-check: web/src/generated/compozy-openapi.d.ts is out of date.",
-        "The file was regenerated — re-run the build and commit the change.",
+        "Run codegen and commit the updated file.",
       ].join("\n")
     );
     process.exit(1);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/codegen.mjs` around lines 70 - 78, The code currently writes to
target when next !== current even in check mode; change the logic in
scripts/codegen.mjs so that the writeFileSync(target, next) call only runs when
not in read-only check mode (the flag parsed from process.argv like '--check');
when in check mode and next !== current, skip writing, emit the existing error
message about the generated file being out of date, and exit with non-zero
status. Update the branch that compares next and current (variables next,
current, target) to conditionally perform the write based on the check-mode
flag.
internal/daemon/run_snapshot_test.go (1)

14-232: Consider breaking helper assertions into subtests for clearer test output.

The test covers the snapshot builder lifecycle comprehensively, but the inline helper function tests at lines 205-231 (cloneRunJobSummary, tokenUsageRowToKinds, tokenUsageIndex, shutdownStateFromPayload) would benefit from t.Run("Should...") subtests per coding guidelines. This improves failure isolation and test output readability.

♻️ Suggested refactor for helper tests
+	t.Run("Should clone job summary with code files", func(t *testing.T) {
+		if summary := cloneRunJobSummary(
+			apicore.RunJobSummary{CodeFiles: []string{"a.go", "b.go"}},
+		); len(summary.CodeFiles) != 2 {
+			t.Fatalf("cloneRunJobSummary() = %#v, want copied code files", summary)
+		}
+	})
+
+	t.Run("Should convert token usage row to kinds", func(t *testing.T) {
+		if usage := tokenUsageRowToKinds(
+			rundb.TokenUsageRow{InputTokens: 2, OutputTokens: 3, TotalTokens: 5},
+		); usage.TotalTokens != 5 {
+			t.Fatalf("tokenUsageRowToKinds() = %#v, want total 5", usage)
+		}
+	})
+
+	t.Run("Should parse valid session index", func(t *testing.T) {
+		if index, ok := tokenUsageIndex("session-12"); !ok || index != 12 {
+			t.Fatalf("tokenUsageIndex(session-12) = %d, %v; want 12, true", index, ok)
+		}
+	})
+
+	t.Run("Should reject invalid session index", func(t *testing.T) {
+		if index, ok := tokenUsageIndex("bad"); ok || index != 0 {
+			t.Fatalf("tokenUsageIndex(bad) = %d, %v; want 0, false", index, ok)
+		}
+	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/run_snapshot_test.go` around lines 14 - 232, Break the inline
helper assertions at the end of TestRunSnapshotBuilderCoversLifecycleBranches
into separate t.Run subtests so failures are isolated and output is clearer:
create subtests for cloneRunJobSummary, tokenUsageRowToKinds, tokenUsageIndex
(good and bad cases), and shutdownStateFromPayload, moving each respective
assertion into a t.Run with a descriptive name and keep the same checks;
reference the helper functions cloneRunJobSummary, tokenUsageRowToKinds,
tokenUsageIndex, and shutdownStateFromPayload when locating the assertions to
extract.
internal/store/globaldb/read_queries_test.go (1)

14-16: Consider logging or checking db.Close() error in test cleanup.

Per coding guidelines, errors should not be ignored with _ without justification. While test cleanup errors are often tolerable, logging them helps diagnose intermittent test failures.

♻️ Suggested improvement
 	defer func() {
-		_ = db.Close()
+		if err := db.Close(); err != nil {
+			t.Logf("db.Close() error = %v", err)
+		}
 	}()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/store/globaldb/read_queries_test.go` around lines 14 - 16, The defer
currently ignores the result of db.Close(); wrap the call to check and log any
error instead of using `_`. Replace the defer with a closure like defer func() {
if err := db.Close(); err != nil { t.Logf("error closing db: %v", err) } } (or
use t.Fatalf/t.Error as appropriate) so db.Close() errors are surfaced in the
test; refer to the existing defer func and the db.Close() call to locate where
to change it.
internal/api/core/interfaces.go (1)

56-62: Prefer request structs over positional ID parameters.

Methods like WorkflowMemoryFile(ctx, workspaceRef, workflowSlug, fileID), TaskDetail(ctx, workspaceRef, workflowSlug, taskID), and ReviewDetail(ctx, workspaceRef, workflowSlug, round, issueRef) pass multiple string/primitive identifiers positionally, where a misordered argument silently compiles. Small, dedicated request structs would make these browser-facing reads self-documenting and safer to extend without future signature churn, aligning with the guideline: "Design small, focused interfaces; accept interfaces, return structs."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/core/interfaces.go` around lines 56 - 62, Replace positional
primitive parameters with small request structs for the listed API methods:
WorkflowOverview, ListItems, TaskBoard, WorkflowSpec, WorkflowMemoryIndex,
WorkflowMemoryFile, TaskDetail (and any similar methods like ReviewDetail).
Define request types (e.g., WorkflowMemoryFileRequest { ctx fields:
WorkspaceRef, WorkflowSlug, FileID } and TaskDetailRequest { WorkspaceRef,
WorkflowSlug, TaskID }) and change signatures to accept a single request struct
plus context where appropriate, update callers to construct and pass the
request, and adjust any related interface implementations and tests to use the
new request types to make calls self-documenting and extensible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/core/interfaces.go`:
- Around line 497-503: RunDetailPayload currently embeds a full Run via Snapshot
and an unbounded []events.Event timeline which can duplicate data and grow
without bound; change RunDetailPayload to avoid duplicating Run by replacing
Snapshot RunSnapshot with a lighter RunReference (or remove Snapshot) and
replace Timeline []events.Event with a paginated/pointer field (e.g.,
TimelinePageToken, TimelineCount or paginated Events slice type already exposed
by RunService) so callers must request pages; update usages of RunDetailPayload,
RunSnapshot, and consumers expecting timeline to use the RunService paginated
Events API instead to keep responses bounded.
- Around line 325-331: The exported type MarkdownDocument uses Metadata
map[string]any which prevents schema generation; change the Metadata field in
MarkdownDocument to a well-defined type (either a concrete FrontMatter struct
that models the YAML frontmatter fields used by the daemon, e.g.
author/date/tags/etc., or json.RawMessage if you need opaque flexibility) and
update any code that reads/writes Metadata to marshal/unmarshal to that new
type; ensure you add the json import if using json.RawMessage and update
references to MarkdownDocument.Metadata across the codebase so the transport
contract is explicit and OpenAPI/SPA generation will work.

In `@internal/api/core/middleware.go`:
- Around line 49-51: Remove the defensive nil-context fallback in the middleware
by deleting the ctx == nil check and the context.Background() assignment so the
middleware function (the context parameter/variable `ctx` in
internal/api/core/middleware.go) requires and uses a non-nil request-scoped
context; update any function signature or callers only if necessary (note:
browser_middleware.go uses c.Request.Context()), and ensure no code paths create
a background context inside this module.

In `@internal/api/httpapi/browser_middleware_test.go`:
- Around line 46-49: In TestBrowserMiddlewareRequestDetectionAndCSRFCookies save
the current gin mode into a variable, call gin.SetMode(...) before invoking
t.Parallel(), and register a t.Cleanup(func() { gin.SetMode(previousMode) }) to
restore the original mode; this removes the race on the process-global
gin.SetMode while keeping the test parallelizable and references the
TestBrowserMiddlewareRequestDetectionAndCSRFCookies function, gin.SetMode,
t.Parallel, and t.Cleanup to locate where to apply the change.

In `@internal/api/httpapi/browser_middleware.go`:
- Around line 74-78: The middleware currently lets requests without the
active-workspace header fall through (workspaceID :=
strings.TrimSpace(c.GetHeader(core.HeaderActiveWorkspaceID)) ... if workspaceID
== "" { c.Next(); return }), which allows handlers to accept workspace from
query/body and bypass workspace-scoped checks; change this to enforce the header
by aborting the request when workspaceID is empty (e.g., call
c.AbortWithStatusJSON or similar on the Gin context `c` with a clear error
message and status like 400/401) instead of calling c.Next(), so handlers cannot
proceed without core.HeaderActiveWorkspaceID present.

In `@internal/api/httpapi/openapi_contract_test.go`:
- Around line 208-222: registeredBrowserRouteKeys currently filters out any
registered Gin routes not present in the expected spec, hiding
extra/undocumented routes; change it to collect and return the full normalized
set of registered route keys instead of only those in expectedSet by iterating
over routes, building keys with route.Method + " " +
ginPathParamPattern.ReplaceAllString(route.Path, "{$1}") for every route,
storing them in a set/slice, sorting the result, and returning it so the test
can compare the complete registered set against the spec-derived expected set.

In `@internal/api/httpapi/static.go`:
- Around line 83-85: resolveAsset currently treats a directory hit
(info.IsDir()) as a "not found" by returning "", false, which causes
shouldServeSPAIndex to think the URL is a missing file and serve the SPA index;
instead, change resolveAsset so that when fs.Stat finds a directory it returns
the empty asset path but signals "static path exists" (for example return "",
true) so the caller will return a 404 for static directories rather than falling
back to the SPA index; apply the same change to the identical check at the other
location (the block around lines 121-125) and keep the rest of
resolveAsset/shouldServeSPAIndex logic unchanged.

In `@internal/daemon/host_runtime_test.go`:
- Around line 21-28: Replace the manual temp dir creation and cleanup (the
os.MkdirTemp call, error check, and t.Cleanup removal) with t.TempDir(): assign
homeDir := t.TempDir() and keep the existing t.Setenv("HOME", homeDir); remove
the os.MkdirTemp, error handling, and t.Cleanup related to homeDir so the test
uses t.TempDir() for automatic cleanup.

In `@internal/daemon/query_documents.go`:
- Around line 278-290: cloneMetadataMap currently performs a shallow copy so
nested maps/slices remain shared; update cloneMetadataMap to perform a deep
clone for map[string]any and []any by recursively cloning any values that are
maps (map[string]any) or slices ([]any) and copying primitives by value; ensure
nil is preserved and non-map/slice types (strings, numbers, bools) are returned
as-is. Use the same deep-clone helper from cloneMetadataMap inside
cloneMarkdownDocument so MarkdownDocument.Metadata is fully deep-copied before
caching/returning to avoid shared mutable state. Make sure the function
signature remains cloneMetadataMap(src map[string]any) map[string]any and keep
behavior of returning nil for empty/nil input.
- Around line 153-180: WalkDir currently includes symlinked *.md files which can
point outside cleanRoot; detect symlinks (use entry.Type()&fs.ModeSymlink or
filepath.EvalSymlinks on path) before appending to entries and either
skip/reject them or resolve them and verify they remain under cleanRoot: call
filepath.EvalSymlinks(path) to get the resolved path, compute its absolute/clean
path and ensure it has cleanRoot as its prefix (using filepath.Clean +
filepath.Abs or similar), then use os.Stat on the resolved path for size/mtime
and only append a markdownDirEntry if the resolved path stays within cleanRoot;
otherwise return an error or skip the entry. Ensure references to cleanRoot,
WalkDir callback, entry.Info()/os.Stat, and markdownDirEntry are updated
accordingly.

In `@internal/daemon/sync_transport_service_test.go`:
- Around line 13-103: Split the two monolithic tests into named subtests using
t.Run("Should...") and table-driven cases where appropriate: refactor
TestSyncTransportServiceResolvesTargetsAndUnavailableBranches so each scenario
(Sync by path, Sync by workspace, missing target validation, nil service
unavailable, ResolveOrRegister+resolveWorkspaceReference by id and path,
resolveWorkspaceReference nil DB) is a separate t.Run("Should ...") subtest and
call t.Parallel() inside independent subtests; keep setup calls
(newRunManagerTestEnv, newTransportSyncService, service.Sync,
resolveWorkspaceReference, env.globalDB.ResolveOrRegister) local to each subtest
or recreated in a shared setup helper to avoid races. Do the same for
TestLooksLikeWorkflowDir: make each case (empty dir, workflow dir with
task_01.md, reviews-* excluded) a t.Run("Should ...") subtest (table-driven for
file scenarios) and use t.Parallel() for independent cases; reference
looksLikeWorkflowDir, os.WriteFile, filepath.Join and t.TempDir when moving
assertions into subtests. Ensure test names follow "Should..." pattern and
preserve existing assertions and error messages.

In `@internal/daemon/transport_mappers.go`:
- Around line 409-416: cloneTransportMetadataMap currently only shallow-copies
the top-level map so nested map[string]any and []any entries remain aliased;
change it to perform a recursive deep clone: for each value in
cloneTransportMetadataMap (and helper function e.g., deepCloneValue) detect
types and recursively clone map[string]any (calling cloneTransportMetadataMap)
and []any (allocating a new slice and deep-cloning each element), preserve
primitive/immutable types by direct assignment, and ensure nil/empty inputs
return nil; update callers to use this deep copy to prevent nested mutation
leaks.

In `@internal/daemon/transport_service_test.go`:
- Around line 14-195: The tests
TestWorkspaceTransportServiceCRUDAndUnavailableBranches,
TestTaskTransportServiceWorkflowReadsStartRunAndUnavailableBranches and
TestTransportSyncResultMapsStructuredFields bundle multiple assertions/branches
in single functions; refactor each into table-driven subtests using
t.Run("Should...") for each logical case (e.g., workspace
register/list/get/resolve/update/delete and nil-service errors in
TestWorkspaceTransportServiceCRUDAndUnavailableBranches; workflow
list/get/start/run/archive/list-after-archive and nil-db/nil-runmanager errors
in TestTaskTransportServiceWorkflowReadsStartRunAndUnavailableBranches; and
structured field checks in TestTransportSyncResultMapsStructuredFields). For
each case create a table entry with name like "Should <behavior>" and execute
the specific setup/assertion inside a t.Run closure, keeping helpers like
newTransportWorkspaceService, newTransportTaskService, transportSyncResult and
env.* calls intact but moved into each subtest's body.

In `@internal/daemon/watcher_error_test.go`:
- Around line 11-35: Split the single test into table-driven t.Run subtests
named "Should..." and create a fresh workflowWatcher per subtest (set
workflowRoot to t.TempDir() and construct logger as before); for each case call
watcher.handleBackendError(...) and then assert the stopError result using
stronger assertions: use errors.As (or testing/assertion helper ErrorAs) to
verify the wrapped cause is the original backend error and ErrorContains (or
strings.Contains/assert helper) to verify the outer message contains "workflow
watcher error" context; ensure each subtest is independent and uses t.Parallel()
where appropriate.

In `@packages/ui/.storybook/preview.css`:
- Line 3: The `@source` Tailwind v4 directive is being flagged by the
scss/at-rule-no-unknown Stylelint rule; fix by adding a scoped suppression
immediately before the directive in preview.css—insert a comment like /*
stylelint-disable-next-line scss/at-rule-no-unknown */ directly above the
`@source` "../src/**/*.{ts,tsx}"; line so only that rule is disabled for this
line; alternatively, update .stylelintrc.json to add "source" to ignoreAtRules
under at-rule-no-unknown if you prefer a config change.

In `@packages/ui/src/tokens.css`:
- Around line 142-170: The .light CSS rule in tokens.css should explicitly set
the color-scheme to ensure UA controls and scrollbars render light; update the
.light selector to include "color-scheme: light;" (and optionally
"forced-color-adjust: none;" if needed for consistent OS-level forced color
handling) so that browser-native UI elements follow the light theme along with
the existing CSS variables.

In `@web/.storybook/preview.ts`:
- Around line 118-125: The conditional branch that separately renders Story
outside RouterProvider is unnecessary and breaks router context access; update
the render logic so that only RouterProvider with the router is rendered (remove
the routerOptions?.kind === "app" ? ... : ... branch and the
createElement(Fragment, null, createElement(Story), ...) path), ensuring Story
is provided/handled by the router created by createStorybookRouter so it can
access routing context via RouterProvider.

---

Minor comments:
In @.compozy/tasks/daemon-web-ui/qa/logs/.gitkeep:
- Line 1: Remove the redundant .gitkeep file: delete the .gitkeep entry from the
logs directory that already contains tracked log files and snapshots, leaving
the directory as-is without the placeholder file.

In @.compozy/tasks/daemon-web-ui/qa/logs/20-run-start-cookiejar.txt:
- Line 5: The QA artifact contains a live CSRF cookie value ("compozy_csrf");
update the log generation/sanitization step that writes cookiejar artifacts so
it redacts or replaces cookie values for keys like "compozy_csrf" (e.g., replace
the value with "[REDACTED]" or a stable hash) before writing or commit, or stop
tracking these cookiejar artifacts entirely; ensure the change is applied where
cookie serialization occurs so entries matching the cookie name are
filtered/sanitized.

In `@internal/api/httpapi/browser_middleware.go`:
- Around line 297-303: Replace the brittle error string matching after
net.SplitHostPort by checking the concrete error type: use errors.As to try to
cast err to *net.AddrError (e.g., var addrErr *net.AddrError; if errors.As(err,
&addrErr) && strings.Contains(addrErr.Err, "missing port") { return
normalizeAuthorityHost(trimmed), "" }) and otherwise return the empty failure
case; reference the net.SplitHostPort call and normalizeAuthorityHost to locate
the change and ensure you import "errors" if not already present.

In `@internal/api/httpapi/openapi_contract_test.go`:
- Around line 195-202: The loop over pathItem currently treats every key
(including path-level keys like "parameters", "summary", "servers") as an
operation; update the iteration that builds keys (where you do keys =
append(keys, strings.ToUpper(method)+" "+path)) to only accept real OpenAPI
operation names (e.g. get, put, post, delete, options, head, patch, trace).
Implement this by normalizing method to lower-case and checking it against a
small set/lookup of allowed methods before appending the route key (use the
existing variables paths, pathItem and the method loop to locate and change the
logic).

In `@packages/ui/src/tokens.css`:
- Line 2: Remove unnecessary quotes around single-word font-family names (change
"Nippo" to Nippo) and ensure generic family keywords in any font stacks are
lowercase (e.g., sans-serif). Normalize text-rendering values to lowercase
(e.g., optimizelegibility → optimizeLegibility in lowercase form) and add a
trailing newline / empty line at the end of the file. Update the font-family
declarations and any text-rendering properties found in this file (search for
font-family and text-rendering occurrences) so they conform to Stylelint
keyword-case and quoting rules and ensure the file ends with an empty line.
- Line 172: Remove the unused Stylelint config or update it to ignore Tailwind's
`@theme` at-rule: either delete the .stylelintrc.json file (since linting uses
oxlint/oxfmt) or modify the config that extends stylelint-config-standard-scss
to add ignoreAtRules: ["theme"] so the at-rule-no-unknown rule won't flag
`@theme`; look for the .stylelintrc.json entry and the at-rule-no-unknown
configuration to apply the change.

In `@pkg/compozy/runs/run.go`:
- Around line 824-826: The code in dispatchSnapshot silently ignores errors from
parseRemoteCursor when handling payload.NextCursor; instead, detect parse errors
for payload.NextCursor and propagate them (or at minimum log and return an
error) so malformed next_cursor doesn't get dropped and stale state isn't used.
Modify the dispatchSnapshot logic around the parseRemoteCursor call to check the
error (from parseRemoteCursor(payload.NextCursor)) and return or bubble up that
error (or wrap it with context) rather than proceeding to set
snapshot.NextCursor only on success; reference parseRemoteCursor,
payload.NextCursor, snapshot.NextCursor and the dispatchSnapshot function when
making the change.

---

Nitpick comments:
In `@internal/api/core/interfaces.go`:
- Around line 56-62: Replace positional primitive parameters with small request
structs for the listed API methods: WorkflowOverview, ListItems, TaskBoard,
WorkflowSpec, WorkflowMemoryIndex, WorkflowMemoryFile, TaskDetail (and any
similar methods like ReviewDetail). Define request types (e.g.,
WorkflowMemoryFileRequest { ctx fields: WorkspaceRef, WorkflowSlug, FileID } and
TaskDetailRequest { WorkspaceRef, WorkflowSlug, TaskID }) and change signatures
to accept a single request struct plus context where appropriate, update callers
to construct and pass the request, and adjust any related interface
implementations and tests to use the new request types to make calls
self-documenting and extensible.

In `@internal/daemon/run_snapshot_test.go`:
- Around line 14-232: Break the inline helper assertions at the end of
TestRunSnapshotBuilderCoversLifecycleBranches into separate t.Run subtests so
failures are isolated and output is clearer: create subtests for
cloneRunJobSummary, tokenUsageRowToKinds, tokenUsageIndex (good and bad cases),
and shutdownStateFromPayload, moving each respective assertion into a t.Run with
a descriptive name and keep the same checks; reference the helper functions
cloneRunJobSummary, tokenUsageRowToKinds, tokenUsageIndex, and
shutdownStateFromPayload when locating the assertions to extract.

In `@internal/store/globaldb/read_queries_test.go`:
- Around line 14-16: The defer currently ignores the result of db.Close(); wrap
the call to check and log any error instead of using `_`. Replace the defer with
a closure like defer func() { if err := db.Close(); err != nil { t.Logf("error
closing db: %v", err) } } (or use t.Fatalf/t.Error as appropriate) so db.Close()
errors are surfaced in the test; refer to the existing defer func and the
db.Close() call to locate where to change it.

In `@packages/ui/src/components/button.tsx`:
- Around line 35-57: Button allows icon-only buttons without an accessible name;
update the ButtonProps/type so any button that provides icon but no children
must require an accessible label (e.g., 'aria-label') at the type level.
Concretely, refactor the exported ButtonProps used by the Button component into
a discriminated union: one branch for regular buttons (children: ReactNode, icon
optional) and another for icon-only buttons (icon: ReactNode and no children)
that requires 'aria-label': string (or equivalent aria-labelledby) so TypeScript
enforces an accessible name when children is absent; update the Button function
signature to use this new ButtonProps shape and ensure the prop references
(icon, children) remain the same.

In `@packages/ui/src/components/section-heading.tsx`:
- Around line 5-34: The component always renders an h1 which breaks heading
hierarchy; update SectionHeadingProps to accept an optional as prop (e.g., as?:
keyof JSX.IntrinsicElements or a React element type) with default 'h1', then
render the title using that tag instead of the hard-coded <h1> inside the
SectionHeading function (keep the prop name title and existing behavior when as
is not provided); ensure types for SectionHeadingProps and the render path
(SectionHeading) are updated so className and other attributes apply correctly
to the chosen heading element.

In `@packages/ui/tests/package-exports.test.ts`:
- Around line 40-42: The export leak test uses a loose regex on exportNames
which causes false positives; tighten it by replacing the current
/(route|workflow|task|review|run|page)/i with a word-boundary or explicit
denylist pattern (e.g., /\b(?:route|workflow|task|review|run|page)\b/i or a
curated array of exact names checked against exportNames) in the test that
asserts exportNames.some(...). Update the test handling around exportNames to
use the new regex or denylist so only whole-word matches (or exact denylisted
names) trigger failure.

In `@scripts/codegen.mjs`:
- Around line 70-78: The code currently writes to target when next !== current
even in check mode; change the logic in scripts/codegen.mjs so that the
writeFileSync(target, next) call only runs when not in read-only check mode (the
flag parsed from process.argv like '--check'); when in check mode and next !==
current, skip writing, emit the existing error message about the generated file
being out of date, and exit with non-zero status. Update the branch that
compares next and current (variables next, current, target) to conditionally
perform the write based on the check-mode flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b7638731-e7d2-494e-8609-8d4a267dc2c6

📥 Commits

Reviewing files that changed from the base of the PR and between 83d53ae and 2757f9c.

⛔ Files ignored due to path filters (114)
  • .codex/ledger/2026-04-20-MEMORY-daemon-query-layer.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-web-ui-qa-exec.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-web-ui-qa.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-workspace-security-sse.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/_tasks.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/MEMORY.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_01.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_02.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_03.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_04.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_05.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_06.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_07.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_08.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_09.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_10.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_11.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_12.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_13.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_14.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_15.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/memory/task_16.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/issues/BUG-001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/logs/01-contract-discovery.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/02-baseline-make-verify.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/03-smoke-workspace-bootstrap.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/04-smoke-playwright.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/05-targeted-review-fix.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/06-targeted-spec-memory.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/07-targeted-runs.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/08-targeted-route-stories.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/09-run-start-inspection.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/10-manual-browser-env.json is excluded by !**/*.json
  • .compozy/tasks/daemon-web-ui/qa/logs/10-manual-browser-harness-setup.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/11-manual-browser-flows.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/12-manual-browser-harness-teardown.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/13-run-start-regression.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/14-post-fix-playwright.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/15-run-start-playwright-only.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/16-run-start-curl-body.json is excluded by !**/*.json
  • .compozy/tasks/daemon-web-ui/qa/logs/17-rebuild-embedded-assets.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/18-run-start-playwright-rebuilt.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/19-manual-harness-setup.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-post-body.json is excluded by !**/*.json
  • .compozy/tasks/daemon-web-ui/qa/logs/21-run-start-playwright-clean-rerun.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/22-run-start-playwright-response-sync.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/23-run-start-playwright-nonconflicting-seed.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/24-manual-browser-start-run.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/25-manual-browser-start-run-ref.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/26-playwright-screenshot-start-run.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/27-manual-harness-teardown.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/28-final-make-verify.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/logs/29-post-tracking-make-verify.log is excluded by !**/*.log
  • .compozy/tasks/daemon-web-ui/qa/screenshots/memory-detail.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/review-detail.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/reviews-index.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/run-detail-direct.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/run-detail.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/spec-techspec.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/task-board.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/task-detail.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/workflow-start-success.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/screenshots/workflows-inventory.png is excluded by !**/*.png
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-006.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-007.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-FUNC-008.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-cases/TC-INT-005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-plans/daemon-web-ui-regression.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/test-plans/daemon-web-ui-test-plan.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/qa/verification-report.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_01.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_02.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_03.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_04.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_05.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_06.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_07.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_08.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_09.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_10.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_11.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_12.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_13.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_14.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_15.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/task_16.md is excluded by !**/*.md
  • .github/actions/setup-bun/action.yml is excluded by !**/*.yml
  • .github/workflows/ci.yml is excluded by !**/*.yml
  • .github/workflows/claude.yml is excluded by !**/*.yml
  • CHANGELOG.md is excluded by !**/*.md
  • bun.lock is excluded by !**/*.lock
  • docs/examples/agents/repo-copilot/mcp.json is excluded by !**/*.json
  • openapi/compozy-daemon.json is excluded by !**/*.json
  • package.json is excluded by !**/*.json
  • packages/ui/package.json is excluded by !**/*.json
  • packages/ui/src/assets/fonts/Disket-Mono-Bold.ttf is excluded by !**/*.ttf
  • packages/ui/src/assets/fonts/Disket-Mono-Regular.ttf is excluded by !**/*.ttf
  • packages/ui/src/assets/fonts/Nippo-Variable.woff2 is excluded by !**/*.woff2
  • packages/ui/tsconfig.build.json is excluded by !**/*.json
  • packages/ui/tsconfig.json is excluded by !**/*.json
  • turbo.json is excluded by !**/*.json
  • web/components.json is excluded by !**/*.json
  • web/dist/.keep is excluded by !**/dist/**
  • web/package.json is excluded by !**/*.json
  • web/src/generated/compozy-openapi.d.ts is excluded by !**/generated/**
📒 Files selected for processing (186)
  • .bun-version
  • .compozy/tasks/daemon-web-ui/qa/issues/.gitkeep
  • .compozy/tasks/daemon-web-ui/qa/logs/.gitkeep
  • .compozy/tasks/daemon-web-ui/qa/logs/16-run-start-curl-headers.txt
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-cookiejar.txt
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-get-headers.txt
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-post-headers.txt
  • .compozy/tasks/daemon-web-ui/qa/screenshots/.gitkeep
  • .gitignore
  • .prettierignore
  • Makefile
  • internal/api/client/runs.go
  • internal/api/client/runs_test.go
  • internal/api/core/errors.go
  • internal/api/core/handlers.go
  • internal/api/core/handlers_error_paths_test.go
  • internal/api/core/handlers_service_errors_test.go
  • internal/api/core/handlers_smoke_test.go
  • internal/api/core/handlers_test.go
  • internal/api/core/interfaces.go
  • internal/api/core/internal_helpers_test.go
  • internal/api/core/middleware.go
  • internal/api/core/routes.go
  • internal/api/core/sse.go
  • internal/api/core/sse_test.go
  • internal/api/httpapi/browser_middleware.go
  • internal/api/httpapi/browser_middleware_test.go
  • internal/api/httpapi/openapi_contract_test.go
  • internal/api/httpapi/routes.go
  • internal/api/httpapi/server.go
  • internal/api/httpapi/static.go
  • internal/api/httpapi/static_test.go
  • internal/api/httpapi/transport_integration_test.go
  • internal/cli/daemon_exec_test_helpers_test.go
  • internal/daemon/host.go
  • internal/daemon/host_runtime_test.go
  • internal/daemon/query_documents.go
  • internal/daemon/query_helpers_test.go
  • internal/daemon/query_models.go
  • internal/daemon/query_service.go
  • internal/daemon/query_service_test.go
  • internal/daemon/review_exec_transport_service.go
  • internal/daemon/review_exec_transport_service_test.go
  • internal/daemon/run_manager.go
  • internal/daemon/run_manager_test.go
  • internal/daemon/run_snapshot_test.go
  • internal/daemon/sync_transport_service_test.go
  • internal/daemon/task_transport_service.go
  • internal/daemon/transport_mappers.go
  • internal/daemon/transport_read_models_test.go
  • internal/daemon/transport_service_test.go
  • internal/daemon/watcher_error_test.go
  • internal/store/globaldb/query_coverage_test.go
  • internal/store/globaldb/read_queries.go
  • internal/store/globaldb/read_queries_test.go
  • packages/ui/.storybook/main.ts
  • packages/ui/.storybook/preview.css
  • packages/ui/.storybook/preview.ts
  • packages/ui/src/components/app-shell.tsx
  • packages/ui/src/components/button.tsx
  • packages/ui/src/components/section-heading.tsx
  • packages/ui/src/components/status-badge.tsx
  • packages/ui/src/components/stories/app-shell.stories.tsx
  • packages/ui/src/components/stories/button.stories.tsx
  • packages/ui/src/components/stories/section-heading.stories.tsx
  • packages/ui/src/components/stories/status-badge.stories.tsx
  • packages/ui/src/components/stories/surface-card.stories.tsx
  • packages/ui/src/components/surface-card.tsx
  • packages/ui/src/components/ui-provider.tsx
  • packages/ui/src/index.ts
  • packages/ui/src/lib/utils.ts
  • packages/ui/src/tokens.css
  • packages/ui/tests/package-exports.test.ts
  • packages/ui/tests/primitives.test.tsx
  • packages/ui/tests/setup.ts
  • packages/ui/tests/storybook-config.test.ts
  • packages/ui/tests/storybook-stories.test.tsx
  • packages/ui/tests/tokens.test.ts
  • packages/ui/vitest.config.ts
  • pkg/compozy/runs/integration_test.go
  • pkg/compozy/runs/remote_watch.go
  • pkg/compozy/runs/run.go
  • pkg/compozy/runs/transport_test.go
  • scripts/codegen.mjs
  • test/frontend-verification-contract.test.ts
  • test/frontend-workspace-config.test.ts
  • web/.storybook/main.ts
  • web/.storybook/preview.ts
  • web/e2e/daemon-ui.smoke.spec.ts
  • web/e2e/global.setup.ts
  • web/e2e/global.teardown.ts
  • web/e2e/support/daemon-fixture.ts
  • web/embed.go
  • web/index.html
  • web/playwright.config.ts
  • web/public/mockServiceWorker.js
  • web/scripts/restore-dist-placeholder.mjs
  • web/scripts/tsr-generate.mjs
  • web/src/lib/api-client.test.ts
  • web/src/lib/api-client.ts
  • web/src/lib/csrf.test.ts
  • web/src/lib/csrf.ts
  • web/src/lib/query-client.ts
  • web/src/lib/session-storage.test.ts
  • web/src/lib/session-storage.ts
  • web/src/main.tsx
  • web/src/routes/-app-shell.integration.test.tsx
  • web/src/routes/-reviews-flow.integration.test.tsx
  • web/src/routes/-runs.integration.test.tsx
  • web/src/routes/-spec-memory-flow.integration.test.tsx
  • web/src/routes/-workflow-tasks.integration.test.tsx
  • web/src/routes/__root.tsx
  • web/src/routes/_app.tsx
  • web/src/routes/_app/index.tsx
  • web/src/routes/_app/memory.tsx
  • web/src/routes/_app/memory_.$slug.tsx
  • web/src/routes/_app/reviews.tsx
  • web/src/routes/_app/reviews_.$slug.$round.$issueId.tsx
  • web/src/routes/_app/runs.tsx
  • web/src/routes/_app/runs_.$runId.tsx
  • web/src/routes/_app/stories/-dashboard.stories.tsx
  • web/src/routes/_app/stories/-memory.$slug.stories.tsx
  • web/src/routes/_app/stories/-memory.stories.tsx
  • web/src/routes/_app/stories/-reviews.$slug.$round.$issueId.stories.tsx
  • web/src/routes/_app/stories/-reviews.stories.tsx
  • web/src/routes/_app/stories/-runs.$runId.stories.tsx
  • web/src/routes/_app/stories/-runs.stories.tsx
  • web/src/routes/_app/stories/-workflows.$slug.spec.stories.tsx
  • web/src/routes/_app/stories/-workflows.$slug.tasks.$taskId.stories.tsx
  • web/src/routes/_app/stories/-workflows.$slug.tasks.stories.tsx
  • web/src/routes/_app/stories/-workflows.stories.tsx
  • web/src/routes/_app/workflows.tsx
  • web/src/routes/_app/workflows_.$slug.spec.tsx
  • web/src/routes/_app/workflows_.$slug.tasks.tsx
  • web/src/routes/_app/workflows_.$slug.tasks_.$taskId.tsx
  • web/src/storybook/msw.ts
  • web/src/storybook/route-stories.test.tsx
  • web/src/storybook/route-story.tsx
  • web/src/storybook/web-storybook-config.test.tsx
  • web/src/storybook/web-storybook-msw-contract.test.ts
  • web/src/storybook/web-storybook-stories-and-fixtures.test.ts
  • web/src/styles.css
  • web/src/styles.test.ts
  • web/src/systems/app-shell/adapters/workspaces-api.test.ts
  • web/src/systems/app-shell/adapters/workspaces-api.ts
  • web/src/systems/app-shell/components/app-shell-boundary.tsx
  • web/src/systems/app-shell/components/app-shell-container.test.tsx
  • web/src/systems/app-shell/components/app-shell-container.tsx
  • web/src/systems/app-shell/components/app-shell-layout.tsx
  • web/src/systems/app-shell/components/workspace-onboarding.test.tsx
  • web/src/systems/app-shell/components/workspace-onboarding.tsx
  • web/src/systems/app-shell/components/workspace-picker.tsx
  • web/src/systems/app-shell/hooks/use-active-workspace.test.tsx
  • web/src/systems/app-shell/hooks/use-active-workspace.ts
  • web/src/systems/app-shell/hooks/use-workspaces.ts
  • web/src/systems/app-shell/index.ts
  • web/src/systems/app-shell/lib/active-workspace-context.ts
  • web/src/systems/app-shell/lib/query-keys.ts
  • web/src/systems/app-shell/lib/workspace-headers.ts
  • web/src/systems/app-shell/mocks/fixtures.ts
  • web/src/systems/app-shell/mocks/handlers.ts
  • web/src/systems/app-shell/mocks/index.ts
  • web/src/systems/app-shell/stores/active-workspace-store.ts
  • web/src/systems/app-shell/types.ts
  • web/src/systems/dashboard/adapters/dashboard-api.test.ts
  • web/src/systems/dashboard/adapters/dashboard-api.ts
  • web/src/systems/dashboard/components/dashboard-view.test.tsx
  • web/src/systems/dashboard/components/dashboard-view.tsx
  • web/src/systems/dashboard/hooks/use-dashboard.ts
  • web/src/systems/dashboard/index.ts
  • web/src/systems/dashboard/lib/query-keys.ts
  • web/src/systems/dashboard/mocks/fixtures.ts
  • web/src/systems/dashboard/mocks/handlers.ts
  • web/src/systems/dashboard/mocks/index.ts
  • web/src/systems/dashboard/types.ts
  • web/src/systems/memory/adapters/memory-api.test.ts
  • web/src/systems/memory/adapters/memory-api.ts
  • web/src/systems/memory/components/memory-index-view.test.tsx
  • web/src/systems/memory/components/memory-index-view.tsx
  • web/src/systems/memory/components/workflow-memory-view.test.tsx
  • web/src/systems/memory/components/workflow-memory-view.tsx
  • web/src/systems/memory/hooks/use-memory.ts
  • web/src/systems/memory/index.ts
  • web/src/systems/memory/lib/query-keys.ts
  • web/src/systems/memory/mocks/fixtures.ts
  • web/src/systems/memory/mocks/handlers.ts

Comment thread internal/api/core/interfaces.go Outdated
Comment thread internal/api/core/interfaces.go
Comment thread internal/api/core/middleware.go Outdated
Comment thread internal/api/httpapi/browser_middleware_test.go
Comment thread internal/api/httpapi/browser_middleware.go
Comment thread internal/daemon/transport_service_test.go Outdated
Comment thread internal/daemon/watcher_error_test.go
Comment thread packages/ui/.storybook/preview.css
Comment thread packages/ui/src/tokens.css
Comment thread web/.storybook/preview.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (8)
internal/daemon/query_helpers_test.go (3)

97-156: Consider splitting into subtests for directory reading, lane titles, and job counts.

This function tests several distinct behaviors: markdown directory walking (lines 114-130), lane/title normalization (lines 132-140), and job count aggregation (lines 142-155). Subtests would clarify what failed and allow parallel execution of independent cases.

As per coding guidelines: "MUST use t.Run("Should...") pattern for ALL test cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/query_helpers_test.go` around lines 97 - 156, Split
TestQueryHelperDirectoryAndStatusBranches into separate t.Run subtests that each
follow the "Should..." naming pattern: one subtest for readMarkdownDir directory
behavior (including missing/empty cases and ordering), one for lane/title
normalization testing laneTitle and titleCase, and one for job aggregation
testing summarizeRunJobCounts; move t.Parallel calls into each subtest as needed
so independent cases can run in parallel, keep the existing assertions and error
checks tied to the specific subtest, and ensure each subtest only references the
relevant helpers (readMarkdownDir, laneTitle, titleCase, summarizeRunJobCounts)
to make failures isolated and clearer.

234-290: Consider extracting subtests for each error branch.

This test covers four distinct scenarios: missing document read (lines 243-251), nil daemon state (lines 253-259), status error propagation (lines 261-265), and health error propagation (lines 267-274), plus the no-reviews case (lines 276-289). Subtests would improve clarity.

As per coding guidelines: "MUST use t.Run("Should...") pattern for ALL test cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/query_helpers_test.go` around lines 234 - 290, Split
TestQueryServiceReadHelpersHandleOptionalAndErrorBranches into explicit subtests
using t.Run for each scenario: one subtest for readWorkflowDocument missing
document (call service.readWorkflowDocument), one for nil daemon state, one for
status error propagation (set service.daemon =
erringDaemonStatusReader{statusErr: ...} and call service.readDaemonState), one
for health error propagation (set service.daemon with healthErr and call
readDaemonState), and one for latestReviewSummary no-reviews (call
service.latestReviewSummary). Keep the shared setup (env, service) in the parent
test and wrap each assertion block in t.Run("Should <describe case>", func(t
*testing.T) { ... }), preserving existing checks and error expectations for
readWorkflowDocument, readDaemonState, and latestReviewSummary.

36-95: Consider restructuring with t.Run("Should...") subtests for better isolation and readability.

This test function covers multiple distinct behaviors (three error types and five document title cases) in a flat structure. Breaking these into subtests would improve test isolation and make failures easier to diagnose.

Example structure:

t.Run("Should match DocumentMissingError against ErrDocumentMissing", func(t *testing.T) { ... })
t.Run("Should extract title from task body", func(t *testing.T) { ... })

As per coding guidelines: "MUST use t.Run("Should...") pattern for ALL test cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/query_helpers_test.go` around lines 36 - 95, The test
TestQueryHelperErrorsAndDocumentTitles bundles many independent checks; split it
into t.Run subtests (e.g., t.Run("Should match DocumentMissingError against
ErrDocumentMissing", ...), t.Run("Should match StaleDocumentReferenceError
against ErrStaleDocumentReference", ...), t.Run("Should match
ReviewIssueNotFoundError against ErrReviewIssueNotFound", ...), and separate
t.Run cases for each documentTitle assertion like "Should extract title from
task body", "Should default techspec title", "Should default memory title",
"Should default doc title", "Should prefer frontmatter title") and move the
corresponding assertions into each subtest; keep or move t.Parallel() to either
the parent test or individual subtests as appropriate and reference the existing
symbols DocumentMissingError, StaleDocumentReferenceError,
ReviewIssueNotFoundError, documentTitle, and daemonTaskBody so the assertions
remain unchanged.
internal/daemon/transport_mappers.go (1)

436-445: Consider logging metadata marshal failures for debugging.

When json.Marshal fails (line 441-443), the error is silently discarded. While returning nil for optional metadata is reasonable, logging the error would help diagnose unexpected marshaling failures (e.g., unsupported types in metadata).

♻️ Suggested improvement
 func marshalTransportMetadata(metadata map[string]any) json.RawMessage {
 	if len(metadata) == 0 {
 		return nil
 	}
 	body, err := json.Marshal(cloneTransportMetadataMap(metadata))
 	if err != nil {
+		// Metadata should always be JSON-serializable; log unexpected failures
+		slog.Debug("failed to marshal document metadata", "error", err)
 		return nil
 	}
 	return json.RawMessage(body)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/transport_mappers.go` around lines 436 - 445, The
marshalTransportMetadata function currently swallows json.Marshal errors and
returns nil; update it to log the marshal error for debugging while still
returning nil for empty/failed metadata. Inside marshalTransportMetadata (which
calls cloneTransportMetadataMap), capture the err from json.Marshal and send it
to the package logger (or std log) with a clear message like "failed to marshal
transport metadata" plus the error and optionally the metadata key summary, then
return nil as before.
internal/api/httpapi/openapi_contract_test.go (1)

274-276: Unnecessary wrapper function.

stringsSplitN is a trivial pass-through to strings.SplitN. Consider calling strings.SplitN directly in getOperation.

♻️ Suggested simplification
 func getOperation(t *testing.T, spec map[string]any, routeKey string) map[string]any {
 	t.Helper()

-	parts := stringsSplitN(routeKey, " ", 2)
+	parts := strings.SplitN(routeKey, " ", 2)
 	if len(parts) != 2 {
 		t.Fatalf("invalid route key %q", routeKey)
 	}
 	method := strings.ToLower(parts[0])
 	path := parts[1]

 	paths := getMap(t, spec, "paths")
 	pathItem := getMap(t, paths, path)
 	return getMap(t, pathItem, method)
 }

-func stringsSplitN(s string, sep string, n int) []string {
-	return strings.SplitN(s, sep, n)
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/openapi_contract_test.go` around lines 274 - 276, Remove
the trivial wrapper function stringsSplitN and update getOperation to call
strings.SplitN directly instead; locate the stringsSplitN definition and any
references inside getOperation and replace those calls with strings.SplitN(s,
sep, n), then delete the stringsSplitN function entirely to avoid the
unnecessary indirection.
internal/core/extension/runtime_test.go (1)

350-350: No current deadlock risk detected, but consider consolidating HOME isolation to prevent future double-acquisition.

Verification confirms no test function currently calls runtimeConfigForTest(t) multiple times or mixes it with isolateRunScopeHome(t). However, since isolateRunScopeHome(t) acquires the non-reentrant runScopeTestHomeMu (line 368), accidental double calls would deadlock until cleanup. To improve API safety and prevent future mistakes, consider either making HOME isolation idempotent per test (e.g., check if already locked) or consolidating acquisition at one explicit layer so the risk is eliminated by design.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/extension/runtime_test.go` at line 350, The call to
isolateRunScopeHome(t) can deadlock if accidentally acquired twice because it
locks the non-reentrant runScopeTestHomeMu; make HOME isolation idempotent or
consolidate the lock so tests cannot double-acquire it. Update the test helpers
so either (a) isolateRunScopeHome(t) first checks and returns if the test is
already marked isolated (introduce a per-test flag checked/updated inside
isolateRunScopeHome and cleaned via t.Cleanup), or (b) move the
runScopeTestHomeMu acquisition out of isolateRunScopeHome and into
runtimeConfigForTest(t) so only one code path ever grabs the mutex; reference
the functions isolateRunScopeHome, runtimeConfigForTest and the mutex symbol
runScopeTestHomeMu when making the change.
internal/daemon/host_runtime_test.go (1)

20-149: Adopt t.Run("Should...") subtests for these test cases.

These tests are currently single-case top-level functions; the repository test standard requires t.Run("Should...") as the default structure.

As per coding guidelines, **/*_test.go: "Table-driven tests with subtests (t.Run) as the default pattern" and "MUST use t.Run("Should...") pattern for ALL test cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/host_runtime_test.go` around lines 20 - 149, Convert each
top-level test body into a single t.Run("Should ...", func(t *testing.T) { ...
}) subtest: wrap the existing contents of
TestPrepareHostRuntimeStartsTransportsAndProbeReady, TestRunRejectsNilContext,
TestDaemonHealthProblemUsesDetailMessage, and
TestBuildHostHandlersWiresStopCallback inside a t.Run call whose name follows
the "Should ..." style; preserve existing t.Parallel() calls by moving them
inside the subtest where appropriate, and keep all referenced symbols and helper
calls (Start, prepareHostRuntime, Run, daemonHealthProblem, buildHostHandlers,
etc.) unchanged so the logic and assertions remain exactly as before.
internal/api/httpapi/dev_proxy_test.go (1)

60-63: The race condition concern is not applicable with Gin v1.12.0, which uses atomic operations (atomic.StoreInt32 and sync/atomic.Value) to make gin.SetMode goroutine-safe. The -race detector will not flag these tests as problematic.

However, the codebase already demonstrates a cleaner pattern in other test files: save the previous mode and restore it in a cleanup function (as seen in handlers_error_paths_test.go and browser_middleware_test.go). Apply this same pattern for consistency:

func TestDevProxyRoutesServeFrontendRequests(t *testing.T) {
	t.Parallel()

	previousMode := gin.Mode()
	gin.SetMode(gin.TestMode)
	t.Cleanup(func() { gin.SetMode(previousMode) })

Also applies to: 143-146, 199-202, 214-217

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/dev_proxy_test.go` around lines 60 - 63, Save and
restore Gin's mode in the test to match the project's cleanup pattern: in
TestDevProxyRoutesServeFrontendRequests call previousMode := gin.Mode() before
gin.SetMode(gin.TestMode) and register t.Cleanup(func() {
gin.SetMode(previousMode) }) so the mode is restored after the test; apply the
same change (use gin.Mode(), gin.SetMode(...), and t.Cleanup to restore) to the
other test functions in this file that set the mode (the other occurrences
around the later test cases).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/httpapi/dev_proxy.go`:
- Around line 26-38: The dev proxy currently forwards browser credentials;
update the ReverseProxy Rewrite function (the anonymous func on proxy:
&httputil.ReverseProxy { Rewrite: func(request *httputil.ProxyRequest) { ... }
}) to strip sensitive daemon/session headers before forwarding by deleting
Authorization, Cookie, and CSRF-related headers (e.g., "Authorization",
"Cookie", "X-CSRF-Token", and any session cookies) from request.Out.Header (and
request.Out.Host if needed), then call request.SetURL(target) /
request.SetXForwarded() as before; ensure ErrorHandler remains unchanged.

In `@Makefile`:
- Around line 63-76: The check-bun-version target currently only detects the
installed Bun but never compares it to the pinned version; update the target
(check-bun-version) to read the pinned version from .bun-version (similar to
check-go-version), parse the installed Bun version from $(BUNCMD) --version into
a comparable string/semver, and perform a numeric/semantic comparison against
the pinned value, failing with a clear error if they differ; ensure you
reference BUNCMD, the .bun-version file, and the check-go-version pattern for
how to perform the comparison and error handling so the Makefile fails fast on
mismatched Bun versions.

---

Nitpick comments:
In `@internal/api/httpapi/dev_proxy_test.go`:
- Around line 60-63: Save and restore Gin's mode in the test to match the
project's cleanup pattern: in TestDevProxyRoutesServeFrontendRequests call
previousMode := gin.Mode() before gin.SetMode(gin.TestMode) and register
t.Cleanup(func() { gin.SetMode(previousMode) }) so the mode is restored after
the test; apply the same change (use gin.Mode(), gin.SetMode(...), and t.Cleanup
to restore) to the other test functions in this file that set the mode (the
other occurrences around the later test cases).

In `@internal/api/httpapi/openapi_contract_test.go`:
- Around line 274-276: Remove the trivial wrapper function stringsSplitN and
update getOperation to call strings.SplitN directly instead; locate the
stringsSplitN definition and any references inside getOperation and replace
those calls with strings.SplitN(s, sep, n), then delete the stringsSplitN
function entirely to avoid the unnecessary indirection.

In `@internal/core/extension/runtime_test.go`:
- Line 350: The call to isolateRunScopeHome(t) can deadlock if accidentally
acquired twice because it locks the non-reentrant runScopeTestHomeMu; make HOME
isolation idempotent or consolidate the lock so tests cannot double-acquire it.
Update the test helpers so either (a) isolateRunScopeHome(t) first checks and
returns if the test is already marked isolated (introduce a per-test flag
checked/updated inside isolateRunScopeHome and cleaned via t.Cleanup), or (b)
move the runScopeTestHomeMu acquisition out of isolateRunScopeHome and into
runtimeConfigForTest(t) so only one code path ever grabs the mutex; reference
the functions isolateRunScopeHome, runtimeConfigForTest and the mutex symbol
runScopeTestHomeMu when making the change.

In `@internal/daemon/host_runtime_test.go`:
- Around line 20-149: Convert each top-level test body into a single
t.Run("Should ...", func(t *testing.T) { ... }) subtest: wrap the existing
contents of TestPrepareHostRuntimeStartsTransportsAndProbeReady,
TestRunRejectsNilContext, TestDaemonHealthProblemUsesDetailMessage, and
TestBuildHostHandlersWiresStopCallback inside a t.Run call whose name follows
the "Should ..." style; preserve existing t.Parallel() calls by moving them
inside the subtest where appropriate, and keep all referenced symbols and helper
calls (Start, prepareHostRuntime, Run, daemonHealthProblem, buildHostHandlers,
etc.) unchanged so the logic and assertions remain exactly as before.

In `@internal/daemon/query_helpers_test.go`:
- Around line 97-156: Split TestQueryHelperDirectoryAndStatusBranches into
separate t.Run subtests that each follow the "Should..." naming pattern: one
subtest for readMarkdownDir directory behavior (including missing/empty cases
and ordering), one for lane/title normalization testing laneTitle and titleCase,
and one for job aggregation testing summarizeRunJobCounts; move t.Parallel calls
into each subtest as needed so independent cases can run in parallel, keep the
existing assertions and error checks tied to the specific subtest, and ensure
each subtest only references the relevant helpers (readMarkdownDir, laneTitle,
titleCase, summarizeRunJobCounts) to make failures isolated and clearer.
- Around line 234-290: Split
TestQueryServiceReadHelpersHandleOptionalAndErrorBranches into explicit subtests
using t.Run for each scenario: one subtest for readWorkflowDocument missing
document (call service.readWorkflowDocument), one for nil daemon state, one for
status error propagation (set service.daemon =
erringDaemonStatusReader{statusErr: ...} and call service.readDaemonState), one
for health error propagation (set service.daemon with healthErr and call
readDaemonState), and one for latestReviewSummary no-reviews (call
service.latestReviewSummary). Keep the shared setup (env, service) in the parent
test and wrap each assertion block in t.Run("Should <describe case>", func(t
*testing.T) { ... }), preserving existing checks and error expectations for
readWorkflowDocument, readDaemonState, and latestReviewSummary.
- Around line 36-95: The test TestQueryHelperErrorsAndDocumentTitles bundles
many independent checks; split it into t.Run subtests (e.g., t.Run("Should match
DocumentMissingError against ErrDocumentMissing", ...), t.Run("Should match
StaleDocumentReferenceError against ErrStaleDocumentReference", ...),
t.Run("Should match ReviewIssueNotFoundError against ErrReviewIssueNotFound",
...), and separate t.Run cases for each documentTitle assertion like "Should
extract title from task body", "Should default techspec title", "Should default
memory title", "Should default doc title", "Should prefer frontmatter title")
and move the corresponding assertions into each subtest; keep or move
t.Parallel() to either the parent test or individual subtests as appropriate and
reference the existing symbols DocumentMissingError,
StaleDocumentReferenceError, ReviewIssueNotFoundError, documentTitle, and
daemonTaskBody so the assertions remain unchanged.

In `@internal/daemon/transport_mappers.go`:
- Around line 436-445: The marshalTransportMetadata function currently swallows
json.Marshal errors and returns nil; update it to log the marshal error for
debugging while still returning nil for empty/failed metadata. Inside
marshalTransportMetadata (which calls cloneTransportMetadataMap), capture the
err from json.Marshal and send it to the package logger (or std log) with a
clear message like "failed to marshal transport metadata" plus the error and
optionally the metadata key summary, then return nil as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a64f3bf-fd58-4ad5-a78e-19e041377aef

📥 Commits

Reviewing files that changed from the base of the PR and between 2757f9c and 9ba8760.

⛔ Files ignored due to path filters (40)
  • .codex/ledger/2026-04-21-MEMORY-hot-reload-dev.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/_meta.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_007.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_008.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_009.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_010.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_011.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_012.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_013.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_014.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_015.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_016.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_017.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_018.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_019.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_020.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_021.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_022.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_023.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_024.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_025.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_026.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_027.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_028.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_029.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_030.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-001/issue_031.md is excluded by !**/*.md
  • bun.lock is excluded by !**/*.lock
  • docs/plans/2026-04-21-hot-reload-dev-design.md is excluded by !**/*.md
  • openapi/compozy-daemon.json is excluded by !**/*.json
  • package.json is excluded by !**/*.json
  • packages/ui/package.json is excluded by !**/*.json
  • web/package.json is excluded by !**/*.json
  • web/src/generated/compozy-openapi.d.ts is excluded by !**/generated/**
📒 Files selected for processing (55)
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-cookiejar.txt
  • Makefile
  • docs/design/daemon-mockup/colors_and_type.css
  • internal/api/core/interfaces.go
  • internal/api/core/middleware.go
  • internal/api/httpapi/browser_middleware.go
  • internal/api/httpapi/browser_middleware_test.go
  • internal/api/httpapi/dev_proxy.go
  • internal/api/httpapi/dev_proxy_test.go
  • internal/api/httpapi/openapi_contract_test.go
  • internal/api/httpapi/server.go
  • internal/api/httpapi/static.go
  • internal/api/httpapi/static_test.go
  • internal/api/httpapi/transport_integration_test.go
  • internal/cli/daemon.go
  • internal/cli/daemon_commands_test.go
  • internal/core/extension/hooks_integration_test.go
  • internal/core/extension/runtime_test.go
  • internal/daemon/host.go
  • internal/daemon/host_runtime_test.go
  • internal/daemon/query_documents.go
  • internal/daemon/query_helpers_test.go
  • internal/daemon/query_service.go
  • internal/daemon/run_manager_test.go
  • internal/daemon/run_snapshot_test.go
  • internal/daemon/sync_transport_service_test.go
  • internal/daemon/transport_mappers.go
  • internal/daemon/transport_read_models_test.go
  • internal/daemon/transport_service_test.go
  • internal/daemon/watcher_error_test.go
  • internal/store/globaldb/read_queries_test.go
  • packages/ui/src/components/app-shell.tsx
  • packages/ui/src/components/button.tsx
  • packages/ui/src/components/section-heading.tsx
  • packages/ui/src/components/status-badge.tsx
  • packages/ui/src/components/stories/button.stories.tsx
  • packages/ui/src/components/stories/surface-card.stories.tsx
  • packages/ui/src/components/surface-card.tsx
  • packages/ui/src/tokens.css
  • packages/ui/tests/package-exports.test.ts
  • packages/ui/tests/primitives.test.tsx
  • packages/ui/tests/tokens.test.ts
  • pkg/compozy/runs/run.go
  • pkg/compozy/runs/transport_test.go
  • scripts/codegen.mjs
  • scripts/dev-web-proxy.sh
  • test/codegen-script.test.ts
  • test/frontend-workspace-config.test.ts
  • web/src/routes/_app/memory_.$slug.tsx
  • web/src/routes/_app/reviews_.$slug.$round.$issueId.tsx
  • web/src/routes/_app/runs_.$runId.tsx
  • web/src/routes/_app/workflows_.$slug.spec.tsx
  • web/src/routes/_app/workflows_.$slug.tasks.tsx
  • web/src/routes/_app/workflows_.$slug.tasks_.$taskId.tsx
  • web/src/styles.css
✅ Files skipped from review due to trivial changes (8)
  • .compozy/tasks/daemon-web-ui/qa/logs/20-run-start-cookiejar.txt
  • internal/api/httpapi/static.go
  • internal/store/globaldb/read_queries_test.go
  • internal/api/httpapi/browser_middleware_test.go
  • internal/api/httpapi/static_test.go
  • internal/daemon/sync_transport_service_test.go
  • internal/daemon/transport_service_test.go
  • internal/daemon/query_documents.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • internal/daemon/run_manager_test.go
  • internal/daemon/host.go
  • internal/api/core/middleware.go
  • internal/api/httpapi/server.go
  • internal/daemon/watcher_error_test.go
  • internal/daemon/run_snapshot_test.go
  • internal/daemon/transport_read_models_test.go
  • internal/api/core/interfaces.go
  • internal/daemon/query_service.go

Comment thread internal/api/httpapi/dev_proxy.go
Comment thread internal/cli/daemon.go Outdated
Comment thread Makefile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
internal/daemon/host_runtime_test.go (1)

22-28: 🛠️ Refactor suggestion | 🟠 Major

Use t.TempDir() instead of manual temp directory management.

The test creates a temporary directory with os.MkdirTemp and manually cleans it up. Per coding guidelines, use t.TempDir() for filesystem isolation.

♻️ Suggested fix
-	homeDir, err := os.MkdirTemp("", "cmp-home-")
-	if err != nil {
-		t.Fatalf("MkdirTemp() error = %v", err)
-	}
-	t.Cleanup(func() {
-		_ = os.RemoveAll(homeDir)
-	})
+	homeDir := t.TempDir()
 	t.Setenv("HOME", homeDir)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/host_runtime_test.go` around lines 22 - 28, Replace the
manual temp dir creation and cleanup (homeDir, err := os.MkdirTemp(...) and
t.Cleanup(os.RemoveAll(homeDir))) with the testing helper t.TempDir(): call
t.TempDir() to get the directory and assign it to homeDir (e.g., homeDir :=
t.TempDir()), remove the error check and explicit Cleanup; update any code using
the previous homeDir variable to use the new one.
internal/api/httpapi/openapi_contract_test.go (1)

31-43: ⚠️ Potential issue | 🟠 Major

The browser contract is still masking two browser-scoped routes.

GET /api/reviews/{slug}/rounds/{round} and GET /api/tasks/{slug}/items are registered in internal/api/core/routes.go, and internal/api/httpapi/browser_middleware.go:206-226 treats them as active-workspace browser routes. Excluding/omitting them here means those endpoints can fall out of the OpenAPI browser contract without this test failing.

Also applies to: 49-75, 108-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/openapi_contract_test.go` around lines 31 - 43, The
test's browserRouteExclusions map is incorrectly masking two active
browser-scoped routes; remove the entries "GET
/api/reviews/{slug}/rounds/{round}" and "GET /api/tasks/{slug}/items" from the
browserRouteExclusions variable in openapi_contract_test.go (and likewise from
any other exclusion maps in that file), so the OpenAPI browser contract test
covers those endpoints which are actually registered as browser routes in the
router (see the route names in routes.go and browser middleware that treat them
as active-workspace browser routes).
🧹 Nitpick comments (3)
internal/daemon/host_runtime_test.go (1)

70-96: Consider adding a brief comment explaining the SPA marker assertion.

The test verifies the embedded frontend serves correctly by checking for <div id="app"></div>. A brief comment noting this is the SPA mount point from web/index.html would improve maintainability.

📝 Suggested addition
+		// Verify embedded SPA shell is served at root (mount point from web/index.html)
 		if !bytes.Contains(rootBody, []byte(`<div id="app"></div>`)) {
 			t.Fatalf("GET / body = %q, want SPA shell", rootBody)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/host_runtime_test.go` around lines 70 - 96, Add a brief
inline comment above the SPA assertion that explains why we check for `<div
id="app"></div>` (it's the single-page-app mount point in web/index.html) so
future readers understand the purpose of the bytes.Contains(rootBody,
[]byte(`<div id="app"></div>`)) check in the GET / response validation; update
the test near the rootBody assertion (around the bytes.Contains call) to include
this note.
internal/cli/daemon.go (1)

320-340: Error messages misleadingly reference env var name when validating flag values.

When normalizeDaemonWebDevProxyTarget is called from resolveDaemonWebDevProxyTarget with a flag value, error messages like %s=%q must use http or https still use daemonWebDevProxyEnv constant, confusing users who passed --web-dev-proxy ws://....

Consider passing a source label parameter or restructuring to provide accurate error context.

♻️ Suggested approach
-func normalizeDaemonWebDevProxyTarget(rawValue string) (string, error) {
+func normalizeDaemonWebDevProxyTarget(rawValue string, source string) (string, error) {
 	value := strings.TrimSpace(rawValue)
 	if value == "" {
 		return "", nil
 	}
 	if !strings.Contains(value, "://") {
-		return "", fmt.Errorf("%s=%q must use http or https", daemonWebDevProxyEnv, rawValue)
+		return "", fmt.Errorf("%s=%q must use http or https", source, rawValue)
 	}
 
 	target, err := url.Parse(value)
 	if err != nil {
-		return "", fmt.Errorf("parse %s=%q: %w", daemonWebDevProxyEnv, rawValue, err)
+		return "", fmt.Errorf("parse %s=%q: %w", source, rawValue, err)
 	}
 	if target.Scheme != "http" && target.Scheme != "https" {
-		return "", fmt.Errorf("%s=%q must use http or https", daemonWebDevProxyEnv, rawValue)
+		return "", fmt.Errorf("%s=%q must use http or https", source, rawValue)
 	}
 	if strings.TrimSpace(target.Host) == "" {
-		return "", fmt.Errorf("%s=%q must include a host", daemonWebDevProxyEnv, rawValue)
+		return "", fmt.Errorf("%s=%q must include a host", source, rawValue)
 	}
 	return target.String(), nil
 }

Then update callers:

// In resolveDaemonWebDevProxyTarget:
return normalizeDaemonWebDevProxyTarget(flagValue, "--web-dev-proxy")

// In cliDaemonWebDevProxyFromEnv:
return normalizeDaemonWebDevProxyTarget(rawValue, daemonWebDevProxyEnv)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/daemon.go` around lines 320 - 340,
normalizeDaemonWebDevProxyTarget currently hardcodes daemonWebDevProxyEnv in its
error messages, which is misleading when called with a flag; change the function
signature to accept a source label (e.g., source string) and use that label in
all formatted error messages instead of daemonWebDevProxyEnv, update callers
(resolveDaemonWebDevProxyTarget -> call
normalizeDaemonWebDevProxyTarget(flagValue, "--web-dev-proxy") and
cliDaemonWebDevProxyFromEnv -> call normalizeDaemonWebDevProxyTarget(rawValue,
daemonWebDevProxyEnv)), and ensure the returned errors and final return still
use target.String() and wrap parse errors the same way.
Makefile (1)

161-162: Reuse $(BINARY_DIR) and $(BINARY_NAME) in dev.

Hard-coding ./bin/compozy duplicates the build config and can drift from go-build.

Proposed fix
 dev: go-build
-	./bin/compozy daemon start --foreground --web-dev-proxy http://127.0.0.1:3000
+	./$(BINARY_DIR)/$(BINARY_NAME) daemon start --foreground --web-dev-proxy http://127.0.0.1:3000
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 161 - 162, The dev target currently hard-codes
./bin/compozy; change it to reuse the Makefile variables by invoking
$(BINARY_DIR)/$(BINARY_NAME) instead of ./bin/compozy so it stays in sync with
go-build; leave the dependency on go-build intact and ensure the command line
flags (--foreground --web-dev-proxy ...) are preserved when replacing the binary
path in the dev recipe.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/httpapi/openapi_contract_test.go`:
- Around line 125-136: The loop over workspaceScopedRoutes in
openapi_contract_test.go causes failures to abort the whole loop; change each
iteration into an isolated subtest by wrapping the body for each routeKey in
t.Run("Should ...", func(t *testing.T){ t.Parallel(); ... }) so each assertion
(calls to getOperation, hasParameterRef checks for
"#/components/parameters/ActiveWorkspaceHeader" and negative check for
"WorkspaceQuery", and hasResponse("412")) runs as its own parallel subtest;
apply the same t.Run/t.Parallel conversion pattern to the other loops mentioned
(the blocks around lines 145-150 and 177-196) so all cases are independent and
report failures individually.

In `@internal/cli/daemon.go`:
- Around line 342-355: The helper overrideDaemonWebDevProxyEnv currently ignores
errors from os.Setenv/os.Unsetenv; update it to check returned errors and log a
warning when any Setenv/Unsetenv fails rather than assigning to `_`.
Specifically, in overrideDaemonWebDevProxyEnv (and in the returned restoration
closure) replace the ignored errors for os.Setenv(daemonWebDevProxyEnv, value),
os.Setenv(daemonWebDevProxyEnv, previousValue) and
os.Unsetenv(daemonWebDevProxyEnv) with error checks and call the project's
logger (or fmt.Fprintf(os.Stderr,...)) to emit a warning including the
daemonWebDevProxyEnv name and the error; keep the existing control flow (respect
hadPrevious) but ensure all env syscall failures are surfaced via warnings.

In `@internal/daemon/query_helpers_test.go`:
- Around line 117-122: The test case in query_helpers_test.go has a mismatched
label vs expectation: the test case struct with name "Should fall back to a
title-cased filename" expects want "design notes" (lowercase). Update either the
test name or the expected value so they match intent — e.g., change the name to
"Should fall back to a lowercased filename" or change want to "Design Notes" —
by editing the test case object (the struct containing name/path/kind/body/want)
so the name and want are consistent.
- Around line 185-190: Test currently only checks that readMarkdownDir(" ")
returns a non-nil error which can mask unrelated failures; update the subtest
("Should reject empty markdown directory paths") to assert a specific error
value or message using errors.Is or t.ErrorContains (e.g., compare against a
sentinel ErrEmptyPath if defined, or assert the error string contains "empty" or
"path" so the failure is explicit). Locate the call to readMarkdownDir in the
test and replace the generic err==nil check with a concrete assertion
(errors.Is(err, ErrEmptyPath) or t.ErrorContains(err, "empty") depending on
which sentinel/message exists) to ensure the test fails only for unexpected
errors.

---

Duplicate comments:
In `@internal/api/httpapi/openapi_contract_test.go`:
- Around line 31-43: The test's browserRouteExclusions map is incorrectly
masking two active browser-scoped routes; remove the entries "GET
/api/reviews/{slug}/rounds/{round}" and "GET /api/tasks/{slug}/items" from the
browserRouteExclusions variable in openapi_contract_test.go (and likewise from
any other exclusion maps in that file), so the OpenAPI browser contract test
covers those endpoints which are actually registered as browser routes in the
router (see the route names in routes.go and browser middleware that treat them
as active-workspace browser routes).

In `@internal/daemon/host_runtime_test.go`:
- Around line 22-28: Replace the manual temp dir creation and cleanup (homeDir,
err := os.MkdirTemp(...) and t.Cleanup(os.RemoveAll(homeDir))) with the testing
helper t.TempDir(): call t.TempDir() to get the directory and assign it to
homeDir (e.g., homeDir := t.TempDir()), remove the error check and explicit
Cleanup; update any code using the previous homeDir variable to use the new one.

---

Nitpick comments:
In `@internal/cli/daemon.go`:
- Around line 320-340: normalizeDaemonWebDevProxyTarget currently hardcodes
daemonWebDevProxyEnv in its error messages, which is misleading when called with
a flag; change the function signature to accept a source label (e.g., source
string) and use that label in all formatted error messages instead of
daemonWebDevProxyEnv, update callers (resolveDaemonWebDevProxyTarget -> call
normalizeDaemonWebDevProxyTarget(flagValue, "--web-dev-proxy") and
cliDaemonWebDevProxyFromEnv -> call normalizeDaemonWebDevProxyTarget(rawValue,
daemonWebDevProxyEnv)), and ensure the returned errors and final return still
use target.String() and wrap parse errors the same way.

In `@internal/daemon/host_runtime_test.go`:
- Around line 70-96: Add a brief inline comment above the SPA assertion that
explains why we check for `<div id="app"></div>` (it's the single-page-app mount
point in web/index.html) so future readers understand the purpose of the
bytes.Contains(rootBody, []byte(`<div id="app"></div>`)) check in the GET /
response validation; update the test near the rootBody assertion (around the
bytes.Contains call) to include this note.

In `@Makefile`:
- Around line 161-162: The dev target currently hard-codes ./bin/compozy; change
it to reuse the Makefile variables by invoking $(BINARY_DIR)/$(BINARY_NAME)
instead of ./bin/compozy so it stays in sync with go-build; leave the dependency
on go-build intact and ensure the command line flags (--foreground
--web-dev-proxy ...) are preserved when replacing the binary path in the dev
recipe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2dc8fb48-3534-424b-9b4b-a1f3bf3b6308

📥 Commits

Reviewing files that changed from the base of the PR and between 9ba8760 and e2199bb.

⛔ Files ignored due to path filters (16)
  • .codex/ledger/2026-04-21-MEMORY-daemon-web-ui-review-002.md is excluded by !**/*.md
  • .codex/ledger/2026-04-21-MEMORY-hot-reload-dev.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/_meta.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_007.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_008.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_009.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_010.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-002/issue_011.md is excluded by !**/*.md
  • docs/plans/2026-04-21-hot-reload-dev-design.md is excluded by !**/*.md
  • package.json is excluded by !**/*.json
📒 Files selected for processing (9)
  • Makefile
  • internal/api/httpapi/dev_proxy.go
  • internal/api/httpapi/dev_proxy_test.go
  • internal/api/httpapi/openapi_contract_test.go
  • internal/cli/daemon.go
  • internal/cli/daemon_commands_test.go
  • internal/daemon/host_runtime_test.go
  • internal/daemon/query_helpers_test.go
  • test/frontend-workspace-config.test.ts
✅ Files skipped from review due to trivial changes (2)
  • internal/api/httpapi/dev_proxy_test.go
  • internal/api/httpapi/dev_proxy.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/cli/daemon_commands_test.go

Comment thread internal/api/httpapi/openapi_contract_test.go
Comment thread internal/cli/daemon.go Outdated
Comment thread internal/daemon/query_helpers_test.go Outdated
Comment thread internal/daemon/query_helpers_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/api/httpapi/openapi_contract_test.go (1)

163-186: Wrap schema validation assertions in named subtests for consistency.

These schema requirement checks are standalone assertions that should use the t.Run("Should...") pattern for consistency with the rest of this test and per coding guidelines.

♻️ Suggested refactor
-	taskRunSchema := getSchema(t, spec, "TaskRunRequest")
-	if schemaRequires(taskRunSchema, "workspace") {
-		t.Fatal("TaskRunRequest must not require workspace")
-	}
-	reviewRunSchema := getSchema(t, spec, "ReviewRunRequest")
-	if schemaRequires(reviewRunSchema, "workspace") {
-		t.Fatal("ReviewRunRequest must not require workspace")
-	}
-	workflowRefSchema := getSchema(t, spec, "WorkflowRefRequest")
-	if schemaRequires(workflowRefSchema, "workspace") {
-		t.Fatal("WorkflowRefRequest must not require workspace")
-	}
-
-	runSnapshot := getSchema(t, spec, "RunSnapshotPayload")
-	if !schemaRequires(runSnapshot, "run") {
-		t.Fatal("RunSnapshotPayload must require run")
-	}
-	properties := getMap(t, runSnapshot, "properties")
-	if _, ok := properties["snapshot"]; ok {
-		t.Fatal("RunSnapshotPayload must not wrap the payload under snapshot")
-	}
-	if _, ok := properties["jobs"]; !ok {
-		t.Fatal("RunSnapshotPayload must expose jobs")
-	}
+	for _, tc := range []struct {
+		name   string
+		schema string
+		field  string
+		want   bool
+	}{
+		{"Should not require workspace in TaskRunRequest", "TaskRunRequest", "workspace", false},
+		{"Should not require workspace in ReviewRunRequest", "ReviewRunRequest", "workspace", false},
+		{"Should not require workspace in WorkflowRefRequest", "WorkflowRefRequest", "workspace", false},
+		{"Should require run in RunSnapshotPayload", "RunSnapshotPayload", "run", true},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			schema := getSchema(t, spec, tc.schema)
+			if got := schemaRequires(schema, tc.field); got != tc.want {
+				t.Fatalf("%s.%s required = %v, want %v", tc.schema, tc.field, got, tc.want)
+			}
+		})
+	}
+
+	t.Run("Should not wrap RunSnapshotPayload under snapshot", func(t *testing.T) {
+		t.Parallel()
+		runSnapshot := getSchema(t, spec, "RunSnapshotPayload")
+		properties := getMap(t, runSnapshot, "properties")
+		if _, ok := properties["snapshot"]; ok {
+			t.Fatal("RunSnapshotPayload must not wrap the payload under snapshot")
+		}
+	})
+
+	t.Run("Should expose jobs in RunSnapshotPayload", func(t *testing.T) {
+		t.Parallel()
+		runSnapshot := getSchema(t, spec, "RunSnapshotPayload")
+		properties := getMap(t, runSnapshot, "properties")
+		if _, ok := properties["jobs"]; !ok {
+			t.Fatal("RunSnapshotPayload must expose jobs")
+		}
+	})

As per coding guidelines: MUST use t.Run("Should...") pattern for ALL test cases and Use t.Parallel() for independent subtests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/openapi_contract_test.go` around lines 163 - 186, Wrap
each standalone schema assertion block into its own named subtest using
t.Run("Should ...", func(t *testing.T) { t.Parallel(); ... }) so the checks for
TaskRunRequest, ReviewRunRequest, WorkflowRefRequest and RunSnapshotPayload (the
calls using getSchema, schemaRequires, getMap and the properties checks) run as
independent subtests; ensure each t.Run has a descriptive name like "Should not
require workspace for TaskRunRequest" or "Should expose jobs for
RunSnapshotPayload" and move the existing assertions inside those closures.
internal/cli/daemon.go (1)

288-291: Wrap propagated env-resolution errors with call-site context.

These return paths pass raw errors upward, which makes troubleshooting less precise once wrapped further up the stack.

♻️ Suggested improvement
 func cliDaemonRunOptionsFromEnv() (daemon.RunOptions, error) {
 	httpPort, err := cliDaemonHTTPPortFromEnv()
 	if err != nil {
-		return daemon.RunOptions{}, err
+		return daemon.RunOptions{}, fmt.Errorf("resolve %s: %w", daemonHTTPPortEnv, err)
 	}
 	webDevProxyTarget, err := cliDaemonWebDevProxyFromEnv()
 	if err != nil {
-		return daemon.RunOptions{}, err
+		return daemon.RunOptions{}, fmt.Errorf("resolve %s: %w", daemonWebDevProxyEnv, err)
 	}
 	return daemon.RunOptions{
 		Version:           version.String(),
 		HTTPPort:          httpPort,
 		WebDevProxyTarget: webDevProxyTarget,
 	}, nil
 }

 func resolveDaemonWebDevProxyTarget(flagValue string) (string, error) {
 	if strings.TrimSpace(flagValue) != "" {
 		return normalizeDaemonWebDevProxyTarget(flagValue, daemonWebDevProxyFlag)
 	}
-	return cliDaemonWebDevProxyFromEnv()
+	target, err := cliDaemonWebDevProxyFromEnv()
+	if err != nil {
+		return "", fmt.Errorf("resolve web dev proxy target: %w", err)
+	}
+	return target, nil
 }

As per coding guidelines, "Prefer explicit error returns with wrapped context using fmt.Errorf(\"context: %w\", err)."

Also applies to: 299-304

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/daemon.go` around lines 288 - 291, The env-resolution errors
returned from calls like cliDaemonWebDevProxyFromEnv() (and the similar call
around the 299-304 region) should be wrapped with call-site context before being
returned; replace raw returns like "return daemon.RunOptions{}, err" with
wrapped errors using fmt.Errorf("failed to resolve web dev proxy from env: %w",
err) (or an appropriate contextual message for the other call), so callers get
clear context while preserving the original error via %w.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/cli/daemon_commands_test.go`:
- Around line 1146-1153: The test currently only verifies
overrideDaemonWebDevProxyEnv returned a non-nil error; strengthen it by
asserting the error message contains a specific substring indicating why Setenv
failed (e.g., mentions "NUL" or "null byte"); update the assertion after calling
overrideDaemonWebDevProxyEnv (used in the t.Run) to use a specific error check
like testing/require's ErrorContains or Error to validate the error text from
overrideDaemonWebDevProxyEnv and keep the existing restore == nil assertion.

---

Nitpick comments:
In `@internal/api/httpapi/openapi_contract_test.go`:
- Around line 163-186: Wrap each standalone schema assertion block into its own
named subtest using t.Run("Should ...", func(t *testing.T) { t.Parallel(); ...
}) so the checks for TaskRunRequest, ReviewRunRequest, WorkflowRefRequest and
RunSnapshotPayload (the calls using getSchema, schemaRequires, getMap and the
properties checks) run as independent subtests; ensure each t.Run has a
descriptive name like "Should not require workspace for TaskRunRequest" or
"Should expose jobs for RunSnapshotPayload" and move the existing assertions
inside those closures.

In `@internal/cli/daemon.go`:
- Around line 288-291: The env-resolution errors returned from calls like
cliDaemonWebDevProxyFromEnv() (and the similar call around the 299-304 region)
should be wrapped with call-site context before being returned; replace raw
returns like "return daemon.RunOptions{}, err" with wrapped errors using
fmt.Errorf("failed to resolve web dev proxy from env: %w", err) (or an
appropriate contextual message for the other call), so callers get clear context
while preserving the original error via %w.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f4562962-e3d8-454a-97b8-b4ef36621f4b

📥 Commits

Reviewing files that changed from the base of the PR and between e2199bb and e183959.

⛔ Files ignored due to path filters (9)
  • .compozy/tasks/daemon-web-ui/reviews-003/_meta.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/daemon-web-ui/reviews-003/issue_007.md is excluded by !**/*.md
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • Makefile
  • internal/api/httpapi/openapi_contract_test.go
  • internal/cli/daemon.go
  • internal/cli/daemon_commands_test.go
  • internal/daemon/query_helpers_test.go
  • test/frontend-workspace-config.test.ts
✅ Files skipped from review due to trivial changes (1)
  • internal/daemon/query_helpers_test.go

Comment on lines +1146 to +1153
t.Run("Should reject values os.Setenv cannot store", func(t *testing.T) {
restore, err := overrideDaemonWebDevProxyEnv("http://127.0.0.1:3100\x00")
if err == nil {
t.Fatal("overrideDaemonWebDevProxyEnv() error = nil, want non-nil")
}
if restore != nil {
t.Fatal("overrideDaemonWebDevProxyEnv() restore should be nil on failure")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Strengthen the invalid-Setenv assertion with explicit error content.

This case only checks that an error exists; it should also validate the error message/context so the failure mode stays specific.

✅ Suggested assertion hardening
 	t.Run("Should reject values os.Setenv cannot store", func(t *testing.T) {
 		restore, err := overrideDaemonWebDevProxyEnv("http://127.0.0.1:3100\x00")
 		if err == nil {
 			t.Fatal("overrideDaemonWebDevProxyEnv() error = nil, want non-nil")
 		}
+		if !strings.Contains(err.Error(), "set "+daemonWebDevProxyEnv) {
+			t.Fatalf("overrideDaemonWebDevProxyEnv() error = %v, want set %s context", err, daemonWebDevProxyEnv)
+		}
 		if restore != nil {
 			t.Fatal("overrideDaemonWebDevProxyEnv() restore should be nil on failure")
 		}
 	})

As per coding guidelines, "MUST have specific error assertions (ErrorContains, ErrorAs)."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
t.Run("Should reject values os.Setenv cannot store", func(t *testing.T) {
restore, err := overrideDaemonWebDevProxyEnv("http://127.0.0.1:3100\x00")
if err == nil {
t.Fatal("overrideDaemonWebDevProxyEnv() error = nil, want non-nil")
}
if restore != nil {
t.Fatal("overrideDaemonWebDevProxyEnv() restore should be nil on failure")
}
t.Run("Should reject values os.Setenv cannot store", func(t *testing.T) {
restore, err := overrideDaemonWebDevProxyEnv("http://127.0.0.1:3100\x00")
if err == nil {
t.Fatal("overrideDaemonWebDevProxyEnv() error = nil, want non-nil")
}
if !strings.Contains(err.Error(), "set "+daemonWebDevProxyEnv) {
t.Fatalf("overrideDaemonWebDevProxyEnv() error = %v, want set %s context", err, daemonWebDevProxyEnv)
}
if restore != nil {
t.Fatal("overrideDaemonWebDevProxyEnv() restore should be nil on failure")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/daemon_commands_test.go` around lines 1146 - 1153, The test
currently only verifies overrideDaemonWebDevProxyEnv returned a non-nil error;
strengthen it by asserting the error message contains a specific substring
indicating why Setenv failed (e.g., mentions "NUL" or "null byte"); update the
assertion after calling overrideDaemonWebDevProxyEnv (used in the t.Run) to use
a specific error check like testing/require's ErrorContains or Error to validate
the error text from overrideDaemonWebDevProxyEnv and keep the existing restore
== nil assertion.

pedronauck and others added 21 commits April 21, 2026 14:01
Adds the TanStack Router shell (`__root.tsx`, `_app.tsx`), dashboard
(`/`), and workflow inventory (`/workflows`) routes for the daemon web
UI. The shell implements tab-scoped single-workspace bootstrap — empty
onboarding via `POST /api/workspaces/resolve`, auto-select for one,
explicit selection for many, and stale-workspace recovery via
`sessionStorage` + a zustand store. Dashboard and inventory render
through the typed `openapi-fetch` client and expose the shell-level
operational actions (workflow sync, archive).

Backfills the missing task_02 frontend contract: installs the TanStack
Router/Query, openapi-fetch, zustand, and testing-library deps; wires
`bun run codegen[-check]` + a per-web `tsr-generate.mjs` step; emits
the typed browser contract at `web/src/generated/compozy-openapi.d.ts`;
and adds a double-submit CSRF middleware that reads `compozy_csrf` and
sets `X-Compozy-CSRF-Token` on mutating requests.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Ships the operator-facing runs surfaces for the daemon web UI: the
cross-workflow run inventory at `/runs` and the live run detail at
`/runs/$runId`. Both consume the typed daemon REST contract (`listRuns`,
`getRunSnapshot`, `cancelRun`, `startWorkflowRun`) and render live run
state through a reusable SSE stream hook that honors the explicit
`run.event` / `run.heartbeat` / `run.overflow` contract, resumes via
`Last-Event-ID` / cursor, and auto-reconnects with a bounded retry
schedule. Overflow refreshes the snapshot cache; manual reconnect and
cancel actions are exposed in the detail view with operator feedback.

Introduces the `@/systems/runs` domain barrel (types, query keys,
adapter, hooks, view components, stream factory + test override), adds
a "Runs" nav entry to the app shell, and registers the detail route as
a flat sibling (`runs_.$runId.tsx`) so it does not inherit the list
layout. Covers the slice with 34 runs-system unit tests plus a 6-case
route integration suite that exercises reconnect, overflow refresh,
filter re-query, and the cancel round-trip against the typed contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds the read-only workflow task surfaces the operator console needs
(ADR-004): board at /workflows/$slug/tasks and detail at
/workflows/$slug/tasks/$taskId, backed by the additive
/api/tasks/{slug}/board and /api/tasks/{slug}/items/{task_id} daemon
contracts via the typed openapi-fetch client.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Delivers task_12: /reviews index with per-workflow round + inline issues,
review issue detail with daemon-backed dispatch-fix, workflow spec read
with PRD/TechSpec/ADR tabs, and workflow memory read that drives the
document panel from opaque file_id values. Shell nav exposes Reviews +
Memory and workflow inventory cards deep-link into Spec and Memory.
@pedronauck
pedronauck merged commit 9c359f1 into main Apr 21, 2026
4 of 6 checks passed
@pedronauck
pedronauck deleted the daemon-web-ui branch April 21, 2026 17:52
This was referenced Apr 28, 2026
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