Implementation Plan

Dev Console — Phased Implementation Plan#

This document translates the product design (see DESIGN.md) into an ordered sequence of incremental milestones. Each phase ends with a working, deployable slice of the system that can be used and tested end-to-end before the next phase begins.

The guiding principle is thin vertical slices over horizontal layers: every phase ships something a real user can interact with, even if the feature set is narrow.


Phase 1 — Minimal E2E Terminal#

Goal: A user can open a browser, authenticate with GitHub, pick a workspace, and get a fully functional terminal in that workspace. Nothing else.

This is the foundational slice. Every later phase builds on top of the server, the auth middleware, and the client shell created here.

1.1 Server Scaffolding ✅#

Acceptance: go build ./... succeeds; ./dev-console --config dev-console.yaml starts and listens.

1.2 GitHub OAuth Authentication ✅#

Acceptance: Hitting GET /auth/login redirects to GitHub; after authorization the user lands back on the server with a valid session cookie. A user not on the allowlist sees a 403. A /api/whoami endpoint returns { login, id } for the authenticated user.

1.3 Auth Validation Site ✅#

A minimal server-rendered HTML site (no external JS dependencies) served directly by the Go backend to allow manual testing and validation of the auth system before the full React SPA exists.

Pages / endpoints:

Templates are Go html/template files stored in internal/templates/ and embedded in the binary via go:embed; styling uses only inline CSS so no additional build step is required. This site is intentionally replaced by the React SPA in step 1.7.

Acceptance: After completing step 1.2 configuration, an operator can:

  1. Open the server URL in a browser; when unauthenticated, be redirected to /login and see a page with a “Sign in with GitHub” link.
  2. Click the “Sign in with GitHub” link and be redirected to the GitHub OAuth flow.
  3. Complete the OAuth flow and land on the index page (/) showing their GitHub login name.
  4. Confirm that visiting / with no or an invalid cookie redirects to /login.
  5. Confirm that a GitHub login not on the allowlist receives a 403 page after completing OAuth.
  6. Click “Sign out” and confirm the session cookie is cleared and the browser returns to the login page.

1.4 Demo Login Page ✅#

The earliest deliverable with a previewable frontend: a minimal Vite + React + TypeScript SPA that renders only the LoginPage. This phase ships the entire demo-mode infrastructure and the per-PR CI preview workflow before any real backend plumbing beyond auth exists.

Deployment model for this phase: The React SPA is built as a standalone static bundle deployed as part of the existing documentation site on Cloudflare Pages — it is not embedded in the Go binary at this stage. Vite builds with --base /demo/, and the output is copied into site/static/demo/ before Hugo runs. Hugo treats it as static content and includes it verbatim in site/public/demo/. The demo is therefore accessible at the /demo/ path on the same Cloudflare Pages origin as the documentation, and no separate Cloudflare Pages project is required. The Go-embedded HTML templates from Phase 1.3 (internal/templates/) continue to serve /login and / for any running Go server instance. The SPA does not interfere with Go-served routes because it is hosted on the Cloudflare Pages documentation origin (a separate domain). The compiled SPA replaces the Go templates in Phase 1.7, when it is embedded in the binary via go:embed.

Client setup:

LoginPage component:

MSW infrastructure (created here, extended in later phases):

client/src/mocks/
  handlers.ts   # starts empty except for GET /api/whoami → { login: "demo", id: 0 }
  browser.ts    # startWorker() — MSW Service Worker bootstrap
  server.ts     # setupServer() — Vitest / Node.js mock server

client/src/main.tsx conditionally starts the worker:

if (import.meta.env.VITE_DEMO_MODE === 'true') {
  const { startWorker } = await import('./mocks/browser')
  await startWorker()
}

DemoBanner component rendered at the app root whenever VITE_DEMO_MODE === 'true': a persistent bar reading “Demo mode — no data is saved”.

Cloudflare Pages build configuration:

The demo SPA is built as part of the Cloudflare Pages build — no separate CI workflow is needed. Configure the dev-console Cloudflare Pages project with:

SettingValue
Build commandmake site-build-with-demo
Output directorysite/public
Node.js version22
Environment variableVITE_DEMO_MODE=true

Cloudflare Pages’ native GitHub integration triggers a build on every commit and PR, and posts a deployment status with a preview URL directly on the PR. The demo is accessible at <preview-url>/demo/ alongside the documentation.

When client/ does not yet exist (before Phase 1.4 is implemented), the build command degrades gracefully: the @if [ -d "client" ] && [ -f "client/package.json" ] guard in make site-build-with-demo is a no-op and only the Hugo docs are deployed.

Acceptance:

  1. VITE_DEMO_MODE=true npm run build produces a static bundle with no server dependency.
  2. npm run preview shows the login page; entering demo as the password navigates to the placeholder page; entering anything else shows an error.
  3. Cloudflare Pages posts a preview URL on the PR; the demo is accessible at <preview-url>/demo/ alongside the documentation.

1.5 SPA GitHub OAuth Login (parallel with 1.6)#

Connects the React SPA to the existing GitHub OAuth backend (Phase 1.2) and gates protected pages behind authenticated sessions. Phases 1.5 and 1.6 are independent parallel tracks: each can be merged without waiting for the other.

MSW: the existing GET /api/whoami → { login: 'demo', id: 0 } handler from Phase 1.4 already satisfies the auth check on the /projects page in demo mode; no new handlers are needed for this phase.

Acceptance:

  1. Demo mode (VITE_DEMO_MODE=true): visiting /demo/ shows the DemoBanner and the LoginPage with a “Try Demo” button; clicking it navigates to /projects.
  2. Demo mode: visiting /projects directly (e.g., via the browser’s address bar) renders the ProjectsPage without redirecting away.
  3. Production mode (real server): visiting / with no session cookie renders the LoginPage with a “Sign in with GitHub” button.
  4. Production mode: completing the GitHub OAuth flow sets a session cookie and the user is taken to /projects.
  5. Production mode: visiting /projects with no session cookie redirects the browser to /.

1.6 SPA Add Project from GitHub (parallel with 1.5)#

Connects the project-list page to the REST API so users can browse their GitHub repositories and register new projects. Phases 1.5 and 1.6 are independent parallel tracks.

Note: workspace data (GET /api/projects/:pid/workspaces) is not yet connected; workspace rows in the accordion/card show an empty list until Phase 1.8.

Acceptance:

  1. Demo mode: the project list renders with the two seeded projects from MSW; no hardcoded constant is read.
  2. Demo mode: clicking "+ New Project" opens the repo-picker dialog, which shows the four hard-coded GitHub repos.
  3. Demo mode: selecting a repo and clicking “Add Project” closes the dialog and the new project appears in the list without a page reload.
  4. Demo mode: VITE_DEMO_MODE=true npm run build succeeds with no errors.
  5. Production mode: the project list and repo picker call real API endpoints; requires Phase 1.7 backend to be running.

1.7 Project and Workspace Registration ✅#

Data models (as specified in DESIGN.md §5.1 and §6.1):

// internal/project/project.go
type Project struct {
    ID        string    `json:"id"`        // URL-safe slug (see ID generation below)
    Name      string    `json:"name"`      // Display name; defaults to the repo name portion of the URL
    RepoURL   string    `json:"repoURL"`   // HTTPS GitHub clone URL, e.g. "https://github.com/owner/repo"
    RootPath  string    `json:"-"`         // Absolute path: filepath.Join(storage.projectsDir, id); omitted from JSON
    CreatedAt time.Time `json:"createdAt"`
}

// internal/workspace/workspace.go
type Workspace struct {
    ID        string    `json:"id"`        // URL-safe slug (see ID generation below)
    ProjectID string    `json:"projectId"` // Parent project ID
    Name      string    `json:"name"`      // Display name; defaults to the branch name
    Branch    string    `json:"branch"`    // Git branch name
    PRNumber  *int      `json:"prNumber"`  // GitHub PR number; nil/JSON null means no PR is associated yet
    CreatedAt time.Time `json:"createdAt"`
}

ID generation (internal/slug package, func Generate(input string, exists func(string) bool) string):

internal/project/ package:

internal/workspace/ package:

REST endpoints (all behind RequireAuth):

Acceptance:

  1. POST /api/projects with { "repoURL": "https://github.com/owner/repo" } returns a project object with a generated id and name; a subsequent GET /api/projects response includes it; GET /api/projects/:pid returns its full metadata.
  2. DELETE /api/projects/:pid returns 204; a subsequent GET /api/projects/:pid returns 404; the directory at storage.projectsDir/<id> no longer exists.
  3. POST /api/projects with a missing or malformed repoURL returns 400; a valid URL pointing to a non-existent or inaccessible repo returns 502.
  4. GET /api/github/repos returns a JSON array of repository objects for the authenticated user.
  5. POST /api/projects/:pid/workspaces with { "branch": "main" } returns a workspace object; a subsequent GET /api/projects/:pid/workspaces includes it; GET /api/projects/:pid/workspaces/:wid returns its metadata; a git worktree exists on disk at the expected path.
  6. DELETE /api/projects/:pid/workspaces/:wid returns 204; a subsequent GET /api/projects/:pid/workspaces/:wid returns 404; the worktree directory no longer exists.
  7. DELETE /api/projects/:pid also removes all of the project’s workspaces and their worktrees.
  8. POST /api/projects/:pid/workspaces with a missing branch returns 400; an unknown :pid returns 404.

1.8 Terminal Backend ✅#

Acceptance: websocat or a small test harness can attach to the WebSocket, send resize + input, and receive shell output.

1.9 Minimal Web Client ✅#

Completes the full Phase 1 end-to-end flow by adding workspace management and the embedded terminal. The LoginPage (Phase 1.5) and project registration UI (Phase 1.6) are already complete; this phase adds:

The compiled SPA is embedded in the Go binary via go:embed and served from /, replacing the Go-embedded HTML templates from Phase 1.3 (internal/templates/). The internal/templates/ package and its routes (GET / and GET /login) are removed in this phase once the SPA takes over.

Styling is minimal — Tailwind CSS with a dark theme matching a terminal aesthetic. No design polish is required at this stage.

Demo mode is a first-class deliverable of this phase, not an afterthought. MSW handlers for every new endpoint must be committed alongside the components — not in a follow-up PR. See the Testing & Validation Strategy for the full pattern.

Demo mode for this phase (extends the handlers from Phases 1.5 and 1.6):

Per-PR Cloudflare Pages deployment is already active from Phase 1.4 — this phase’s changes will automatically trigger a preview.

Acceptance: The full flow works in Chrome and Safari on both desktop and a 375 px wide mobile viewport:

  1. VITE_DEMO_MODE=true npm run build produces a static bundle with no server dependency.
  2. npm run preview shows the demo banner and the full flow (login → project list → workspace list → open terminal → interactive echo session) without any backend.
  3. Cloudflare Pages posts a preview URL on the PR; the demo is accessible at <preview-url>/demo/ alongside the documentation.
  4. Against a real server (no VITE_DEMO_MODE): login → project list → workspace list → open terminal → interactive shell session.

Phase 1 Deliverables#

ArtifactDescription
go.modGo module definition
cmd/dev-console/Server entry point
internal/config/Config loading
internal/auth/GitHub OAuth + session middleware
internal/templates/html/template files (embedded via go:embed) for the auth validation site
internal/project/Project registry
internal/workspace/Workspace store with git worktree management (in-memory, created at runtime)
internal/slug/URL-safe slug generation helper used by project and workspace managers
internal/terminal/PTY session management
client/Vite + React + TypeScript SPA (bootstrapped in Phase 1.4)
client/src/context/AuthContext.tsxAuth context and useAuth hook (Phase 1.5)
client/src/mocks/MSW handlers and browser/server worker entry points (created in Phase 1.4, extended in 1.5 and 1.6)
docs/examples/dev-console.yaml.exampleAnnotated sample configuration
Makefilemake build, make dev, make test, make site-build-with-demo targets

Phase 2 — File Browsing#

Goal: Users can browse the workspace file tree and read file contents in the browser.

2.1 File API ✅#

Add to internal/workspace/:

Both endpoints validate that the resolved path does not escape the workspace root (path-traversal protection).

Acceptance: curl returns directory listings and file contents; requests for paths outside the root return 400.

2.2 File Browser UI ✅#

Acceptance: User can expand directories, click files, and read their contents. The terminal panel remains accessible. Demo build works end-to-end without a backend.


Code Quality & Maintainability#

This section tracks technical-debt and quality improvements that cut across multiple phases. Each item is independent of any single feature phase and can be worked on incrementally without blocking other work.

Q.1 Shared Test Utilities ✅#

Extract duplicated test helpers into internal/testutil so that the same fixtures and utilities are not copy-pasted across every package’s _test.go file.

Duplicated symbols before this change:

SymbolPackages
newLocalGitRepoproject, workspace, terminal
gitRunproject, workspace, terminal
registerProjectworkspace, terminal

Deliverables:

Acceptance: make test-race passes; no duplicate newLocalGitRepo / gitRun implementations remain in any _test.go file.

Q.2 Go API Client for Handler Tests ✅#

Replace the verbose httptest.NewRequest / httptest.NewRecorder pattern in handler tests with a typed Go client that hides request construction and response decoding behind clear method names.

Problem: Handler tests are dominated by boilerplate:

req := httptest.NewRequest(http.MethodGet, "/api/projects", nil)
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
if rr.Code != http.StatusOK { ... }
var projects []project.Project
json.NewDecoder(rr.Body).Decode(&projects)

Deliverables:

Acceptance: Refactored tests are noticeably shorter; the intent of each step is clear without reading URL strings. make test-race passes.

Q.3 Handler Project/Workspace Resolution Helpers#

The “resolve project → 404 if missing” and “resolve workspace → 404 if missing” error-handling blocks are copy-pasted into every handler that needs them.

// This block appears in every workspace and terminal handler:
if _, err := pm.Get(pid); err != nil {
    if errors.Is(err, project.ErrNotFound) {
        http.Error(w, "project not found", http.StatusNotFound)
        return
    }
    http.Error(w, "internal error", http.StatusInternalServerError)
    return
}

Deliverables:

Acceptance: No inline errors.Is(err, project.ErrNotFound) blocks remain in handler files; existing handler tests continue to pass.

Q.4 JSON Response Helper#

The w.Header().Set("Content-Type", "application/json") / json.NewEncoder(w).Encode(v) sequence is duplicated in every handler that returns a JSON body.

Deliverables:

Acceptance: No bare json.NewEncoder(w).Encode calls remain in handler files; existing handler tests continue to pass.


Phase 3 — Basic Agent Chat#

Goal: Users can open a chat session with an AI assistant that can read files and answer questions about the workspace. No file writes yet.

3.1 LLM Client ✅#

Acceptance: Unit test that stubs the HTTP response and verifies streamed chunks are reassembled correctly.

3.2 Agent Session Backend (read-only) ✅#

Tools enabled in this phase: read_file, list_files (no write_file or run_command)

Acceptance: User can ask “What files are in the root of this workspace?” and receive a correct streamed answer.

3.3 Chat UI ✅#

Acceptance: Full conversational loop visible in the browser; streaming text appears word-by-word. Demo build works end-to-end without a backend.


Phase 4 — Change Proposal & Review#

Goal: The agent can propose file edits; the user reviews diffs and accepts or rejects them before any file is written to disk.

4.1 Pending Change Backend#

Acceptance: Agent proposes a change; the in-memory store holds it; accept endpoint writes the file.

4.2 Git Status & Diff API#

Acceptance: After accepting a change, git/status reflects the modification.

4.3 Diff Review UI#

Acceptance: User can trigger a file edit via chat, review the diff, and accept or reject it. File tree updates after acceptance. Demo build works end-to-end without a backend.


Phase 5 — Full Agent Toolset & Manual Editing#

Goal: The agent has its complete tool set; users can also manually edit files.

5.1 run_command Tool#

Acceptance: Agent can run go test ./... or npm test and report results.

5.2 git_diff Tool#

5.3 Manual File Editor#

Acceptance: User can open a file, edit it, save it, and see the change reflected in git/status. Demo build works end-to-end without a backend.


Phase 6 — Polish & Production Readiness#

Goal: The system is ready for daily use by a small trusted team.

6.1 Error Handling & Resilience#

6.2 Session Persistence (optional)#

6.3 PWA & Mobile UX#

6.4 TLS & Deployment#

6.5 Observability#


Testing & Validation Strategy#

Rule: Demo mode is mandatory for all frontend functionality#

Every piece of frontend functionality must work in demo mode before it may be merged. This is not optional. A PR that adds or modifies frontend components without shipping the corresponding MSW handlers will be rejected.

Demo mode serves two purposes:

  1. Per-PR previews — reviewers can interact with any UI change via the auto-deployed Cloudflare Pages URL without running a server.
  2. Unit testing — the same MSW handlers are reused in Vitest tests, so there is one source of truth for mock behaviour.

Pattern: Mock Service Worker (MSW)#

The project uses Mock Service Worker as the seam between real backend calls and demo/test stubs. MSW intercepts fetch and WebSocket calls at the browser’s Service Worker layer, which means:

File layout:

client/src/mocks/
  handlers.ts      # All MSW request/WebSocket handlers
  browser.ts       # MSW browser worker setup (startWorker())
  server.ts        # MSW Node.js server setup for Vitest (setupServer())

Enabling demo mode:

In client/src/main.tsx:

if (import.meta.env.VITE_DEMO_MODE === 'true') {
  const { startWorker } = await import('./mocks/browser')
  await startWorker()
}

The VITE_DEMO_MODE variable is set to 'true' only in the CI demo build and local npm run demo convenience script. It is never set in the production build.

Handler conventions:

Demo-specific UI:

PR Preview Deployments#

The dev-console Cloudflare Pages project is configured to use make site-build-with-demo as its build command (site/public as the output directory). Cloudflare Pages’ native GitHub integration takes care of the rest:

  1. On every PR commit, Cloudflare Pages runs make site-build-with-demo.
  2. If client/ exists, the target builds the SPA with VITE_DEMO_MODE=true and --base /demo/, copies the output to site/static/demo/, then runs Hugo to produce site/public/ with the documentation and the demo at /demo/.
  3. If client/ does not yet exist (or has no package.json), the guard in make site-build-with-demo is a no-op and only the Hugo docs are built and deployed.
  4. Cloudflare Pages posts a deployment status with the preview URL directly on the PR. The demo is accessible at <preview-url>/demo/.

No GitHub Actions workflow or repository secrets are required for deployments.

PRs that include user-visible UI changes must include a screenshot or screen recording taken from the <preview-url>/demo/ Cloudflare Pages URL in the PR description.

Acceptance Gate (applies to every phase with frontend work)#

A phase is not done until all of the following hold:

  1. VITE_DEMO_MODE=true npm run build succeeds with no errors or warnings.
  2. npm run preview (or the Cloudflare Pages URL) shows a fully functional demo covering all UI flows introduced in the phase — no backend required.
  3. npm test (Vitest) passes; tests exercise components using the same MSW handlers as the demo.
  4. Cloudflare Pages posts a preview URL on the PR with the demo accessible at <preview-url>/demo/.

Dependency Map#

Phase 1 ──► Phase 2 ──► Phase 3 ──► Phase 4 ──► Phase 5
              │                          │
              └──────────────────────────┘
                 (file APIs reused by agent tools)
Phase 6 can be worked on incrementally alongside Phase 4 and 5.

Phase 1 is a hard prerequisite for everything else because it establishes:


Work Sizing Guidance#

PhaseEstimated EffortPrimary Risk
1~3–4 daysOAuth flow edge cases; PTY on macOS vs Linux
2~1–2 daysPath-traversal validation; large binary files
3~2–3 daysLLM streaming; tool-call parsing across providers
4~2–3 daysDiff rendering; atomic file writes
5~1–2 daysCommand sandboxing; editor UX
6~2–3 daysPWA quirks on iOS; SQLite CGo build

Estimates assume a single focused engineer. They grow if providers other than OpenAI are tested simultaneously.


Definition of Done for Each Phase#

  1. make build succeeds with no warnings.
  2. make test passes with no failures.
  3. The acceptance criteria listed in each section are manually verified.
  4. For phases with frontend work: all items in the Acceptance Gate in the Testing & Validation Strategy are satisfied — in particular, the demo build works without a backend and the demo-preview CI workflow produces a preview URL.
  5. A brief entry is added to CHANGELOG.md describing what shipped.