# Spec: Scaffold app/ Folder Structure

**Change**: 260312-jz77-scaffold-app-structure
**Created**: 2026-03-12
**Affected memory**: `docs/memory/run-kit/architecture.md`

## Non-Goals

- Functional code — no business logic, no HTTP handlers, no React components with behavior
- Porting existing code from `packages/` — that is Phases 2-4
- Deleting `packages/` or `e2e/` — that is Phase 4 (cleanup)
- Installing Go dependencies beyond what's in the current `go.mod` — the new module reuses the same dependency set

## Backend: Go Module Scaffold

### Requirement: Go module initialization

The system SHALL create a new Go module at `app/backend/` with module name `run-kit` and Go version matching the current `packages/api/go.mod` (Go 1.22.0).

The `go.mod` SHALL declare the same external dependencies as the current module:
- `github.com/go-chi/chi/v5`
- `github.com/go-chi/cors`
- `github.com/gorilla/websocket`
- `github.com/creack/pty`
- `gopkg.in/yaml.v3`

The `go.sum` SHALL be generated by running `go mod tidy` after all source files are in place.

#### Scenario: Go module compiles

- **GIVEN** the `app/backend/` directory exists with `go.mod` and all package files
- **WHEN** `go build ./...` is run from `app/backend/`
- **THEN** the build succeeds with exit code 0
- **AND** no compilation errors are reported

### Requirement: Entry point

The system SHALL create `app/backend/cmd/run-kit/main.go` with a minimal `main()` function in package `main` that prints "run-kit" to stdout and exits with code 0.

#### Scenario: Entry point runs

- **GIVEN** the Go module is initialized
- **WHEN** `go run ./cmd/run-kit` is executed from `app/backend/`
- **THEN** stdout contains "run-kit"
- **AND** the process exits with code 0

### Requirement: API handler placeholders

The system SHALL create the following files in `app/backend/api/`, each containing only a `package api` declaration and no functional code:

| File | Purpose (placeholder for) |
|------|--------------------------|
| `router.go` | Route registration + middleware |
| `sessions.go` | GET/POST session endpoints |
| `windows.go` | POST window endpoints |
| `directories.go` | GET directory autocomplete |
| `upload.go` | POST file upload |
| `sse.go` | GET SSE stream |
| `relay.go` | WS terminal relay |
| `health.go` | GET health check |
| `spa.go` | SPA static serving |

#### Scenario: API package compiles

- **GIVEN** all handler placeholder files exist in `app/backend/api/`
- **WHEN** `go build ./...` is run from `app/backend/`
- **THEN** the `api` package compiles without errors

### Requirement: Internal package placeholders

The system SHALL create the following internal packages under `app/backend/internal/`, each with:
1. A `.go` file containing a package declaration and one exported placeholder (e.g., a `// TODO` comment and a trivially exported const or type)
2. A `_test.go` file containing only a package declaration (empty test file)

| Package | Directory |
|---------|-----------|
| `tmux` | `internal/tmux/` |
| `sessions` | `internal/sessions/` |
| `fab` | `internal/fab/` |
| `config` | `internal/config/` |
| `validate` | `internal/validate/` |

The system SHALL NOT create `internal/worktree/` — this package is dead code per the architecture spec.

#### Scenario: Internal packages compile

- **GIVEN** all internal package directories exist with their `.go` and `_test.go` files
- **WHEN** `go build ./...` is run from `app/backend/`
- **THEN** all internal packages compile without errors

#### Scenario: Go tests discover zero tests

- **GIVEN** all `_test.go` files exist with package declarations only
- **WHEN** `go test ./...` is run from `app/backend/`
- **THEN** the test runner exits with code 0
- **AND** reports "ok" for each package (no tests to run)

## Frontend: Vite Project Scaffold

### Requirement: Frontend package initialization

The system SHALL create `app/frontend/package.json` with:
- Name: `run-kit-web`
- Type: `module`
- Scripts: `dev`, `build`, `preview`, `test`, `test:watch`
- Dependencies matching the current `packages/web/package.json`: React 19, TanStack Router, xterm.js 5 (@xterm/xterm, @xterm/addon-fit, @xterm/addon-web-links), react-dom
- Dev dependencies: Tailwind CSS 4, Vite 7, Vitest 4, TypeScript 5.7+, @vitejs/plugin-react, @testing-library/jest-dom, @testing-library/react, jsdom, postcss, @tailwindcss/postcss, msw (for API mocking)
- Additional dev dependencies for Playwright: @playwright/test

#### Scenario: pnpm install succeeds

- **GIVEN** `app/frontend/package.json` exists with all dependencies
- **WHEN** `pnpm install` is run from the repo root
- **THEN** installation completes without errors

### Requirement: Vite configuration

The system SHALL create `app/frontend/vite.config.ts` with:
- React plugin
- Path alias: `@` → `./src`
- Dev server proxy: `/api/*` → `http://localhost:3000`, `/relay/*` → `ws://localhost:3000` (WebSocket)

#### Scenario: Vite dev server starts

- **GIVEN** the frontend package is installed
- **WHEN** `pnpm dev` is run from `app/frontend/`
- **THEN** Vite starts on port 5173
- **AND** proxy routes are configured for `/api` and `/relay`

### Requirement: TypeScript configuration

The system SHALL create `app/frontend/tsconfig.json` matching the current configuration: ES2022 target, bundler module resolution, strict mode, React JSX, `@/*` path alias to `./src/*`.

### Requirement: Vitest configuration

The system SHALL create `app/frontend/vitest.config.ts` with jsdom environment, setup file `./src/test-setup.ts`, and path alias `@` → `./src`.

The system SHALL create `app/frontend/src/test-setup.ts` importing `@testing-library/jest-dom/vitest`.

#### Scenario: Vitest discovers zero tests

- **GIVEN** the Vitest config and setup file exist
- **WHEN** `pnpm test` is run from `app/frontend/`
- **THEN** the test runner exits with code 0
- **AND** reports no test suites found (or zero tests)

### Requirement: Minimal React entry

The system SHALL create:

- `app/frontend/src/main.tsx` — minimal React entry point that renders the router into `#root`
- `app/frontend/src/app.tsx` — single-view layout skeleton with placeholder divs for: top bar, sidebar, terminal area, bottom bar. Uses the CSS skeleton from the design spec (`h-screen flex flex-col` with `shrink-0` chrome zones and `flex-1` main area)
- `app/frontend/src/router.tsx` — TanStack Router with one route: `/:session/:window` (defaults to first session, first window). Renders `App` component.
- `app/frontend/index.html` — standard Vite HTML entry with `<div id="root">` and `<script type="module" src="/src/main.tsx">`

#### Scenario: Vite builds successfully

- **GIVEN** all React entry files exist
- **WHEN** `pnpm build` is run from `app/frontend/`
- **THEN** the build succeeds
- **AND** output goes to `app/frontend/dist/`

### Requirement: API client type stubs

The system SHALL create `app/frontend/src/api/client.ts` with typed function stubs for every endpoint in `docs/specs/api.md`:
- `getSessions(): Promise<ProjectSession[]>` — throws "not implemented"
- `createSession(name: string, cwd?: string): Promise<void>` — throws "not implemented"
- `killSession(session: string): Promise<void>` — throws "not implemented"
- `createWindow(session: string, name: string, cwd?: string): Promise<void>` — throws "not implemented"
- `killWindow(session: string, index: number): Promise<void>` — throws "not implemented"
- `renameWindow(session: string, index: number, name: string): Promise<void>` — throws "not implemented"
- `sendKeys(session: string, index: number, keys: string): Promise<void>` — throws "not implemented"
- `getDirectories(prefix: string): Promise<string[]>` — throws "not implemented"
- `uploadFile(session: string, file: File, window?: number): Promise<string>` — throws "not implemented"

All mutation functions SHALL use POST method in their signatures/comments. The URL patterns SHALL match the API spec (e.g., `/api/sessions/:session/kill`, not the old multiplexed POST).

#### Scenario: Client module type-checks

- **GIVEN** `client.ts` and `types.ts` exist
- **WHEN** `tsc --noEmit` is run from `app/frontend/`
- **THEN** no type errors are reported

### Requirement: Shared types

The system SHALL create `app/frontend/src/types.ts` with:
- `WindowInfo` type: `{ index: number; name: string; worktreePath: string; activity: "active" | "idle"; isActiveWindow: boolean; fabChange?: string; fabStage?: string }`
- `ProjectSession` type: `{ name: string; windows: WindowInfo[] }`

### Requirement: MSW handler stubs

The system SHALL create `app/frontend/tests/msw/handlers.ts` with MSW handler stubs for:
- `GET /api/sessions` — returns empty array
- `GET /api/sessions/stream` — no-op SSE stub
- `GET /api/directories` — returns `{ directories: [] }`
- `POST /api/sessions` — returns `{ ok: true }`
- `POST /api/sessions/:session/kill` — returns `{ ok: true }`

Each handler returns a minimal valid response. The file SHALL import from `msw` and export a `handlers` array.

#### Scenario: MSW handlers import cleanly

- **GIVEN** the `handlers.ts` file exists and msw is installed
- **WHEN** the module is imported
- **THEN** no import errors occur

### Requirement: Playwright configuration

The system SHALL create `app/frontend/playwright.config.ts` with:
- One project: `desktop` (Chromium)
- Base URL: `http://localhost:5173`
- Test directory: `tests/e2e/`

The system SHALL create `app/frontend/tests/e2e/smoke.spec.ts` with a single placeholder test that is marked as `test.skip` (e.g., `test.skip('smoke', () => {})`) so the test file exists but doesn't fail when no server is running.

#### Scenario: Playwright config resolves

- **GIVEN** `playwright.config.ts` exists
- **WHEN** `npx playwright test --list` is run from `app/frontend/`
- **THEN** the config is parsed without errors
- **AND** the smoke test file is discovered

## Build System: Justfile

### Requirement: Updated justfile recipes

The system SHALL replace the current justfile with recipes targeting the new `app/` paths:

| Recipe | Command |
|--------|---------|
| `just dev` | Run `go run ./cmd/run-kit` from `app/backend/` and `pnpm dev` from `app/frontend/` concurrently |
| `just build` | `go build -o ../../bin/run-kit ./cmd/run-kit` from `app/backend/` AND `pnpm build` from `app/frontend/` |
| `just test` | `just test-backend` + `just test-frontend` + `just test-e2e` |
| `just test-backend` | `go test ./...` from `app/backend/` |
| `just test-frontend` | `pnpm test` from `app/frontend/` (Vitest) |
| `just test-e2e` | `pnpm exec playwright test` from `app/frontend/` |
| `just check` | `pnpm exec tsc --noEmit` from `app/frontend/` |
| `just verify` | `just check` + `just test` + `just build` |
| `just up` | `./supervisor.sh` |
| `just bg` | Start supervisor in detached tmux session |
| `just logs` | Attach to supervisor tmux session |
| `just down` | Stop supervisor tmux session |
| `just https` | `caddy run --config Caddyfile` |
| `just trust` | `caddy trust` |

The `just dev` recipe SHALL run both processes concurrently and kill both on Ctrl+C. It MAY use a simple background process approach (`&` + `wait`) or a process group.

#### Scenario: just build succeeds

- **GIVEN** both `app/backend/` and `app/frontend/` are scaffolded
- **WHEN** `just build` is run from the repo root
- **THEN** the Go binary is built to `bin/run-kit`
- **AND** the frontend is built to `app/frontend/dist/`

#### Scenario: just test-backend succeeds with zero tests

- **GIVEN** `app/backend/` has empty test files
- **WHEN** `just test-backend` is run
- **THEN** `go test ./...` exits 0

#### Scenario: just test-frontend succeeds with zero tests

- **GIVEN** `app/frontend/` has Vitest configured with no test files
- **WHEN** `just test-frontend` is run
- **THEN** Vitest exits 0

## Build System: Supervisor

### Requirement: Updated supervisor paths

The system SHALL update `supervisor.sh` to use the new paths:
- Go build: `cd app/backend && go build -o ../../bin/run-kit ./cmd/run-kit`
- Frontend build: `cd app/frontend && pnpm build`
- SPA dist: referenced paths updated from `packages/web/dist/` to `app/frontend/dist/` where applicable

The supervisor SHALL NOT change its core logic (restart mechanism, health checks, rollback).

#### Scenario: Supervisor builds from new paths

- **GIVEN** `supervisor.sh` has updated paths
- **WHEN** the `build_all` function executes
- **THEN** it builds the Go binary from `app/backend/`
- **AND** it builds the frontend from `app/frontend/`

## Build System: Workspace

### Requirement: Updated pnpm workspace

The system SHALL update `pnpm-workspace.yaml` to reference `app/frontend` instead of `packages/web`:

```yaml
packages:
  - "app/frontend"
```

#### Scenario: pnpm recognizes new workspace

- **GIVEN** `pnpm-workspace.yaml` points to `app/frontend`
- **WHEN** `pnpm install` is run from the repo root
- **THEN** the `app/frontend` package is recognized as a workspace member

## Cleanup: dev.sh Removal

### Requirement: Remove dev.sh

The system SHALL delete `dev.sh` from the repo root. Its functionality is replaced by `just dev`.

#### Scenario: dev.sh is absent

- **GIVEN** `dev.sh` has been deleted
- **WHEN** `ls dev.sh` is run from the repo root
- **THEN** the file does not exist

## Assumptions

| # | Grade | Decision | Rationale | Scores |
|---|-------|----------|-----------|--------|
| 1 | Certain | Use `app/backend` and `app/frontend` directory structure | Confirmed from intake #1 — user explicitly chose this in discussion | S:95 R:90 A:95 D:95 |
| 2 | Certain | Go tests co-located with source (`_test.go`) | Confirmed from intake #2 — standard Go convention, user confirmed | S:95 R:95 A:95 D:95 |
| 3 | Certain | Playwright E2E at `app/frontend/tests/e2e/` | Confirmed from intake #3 — user specified location | S:95 R:85 A:90 D:95 |
| 4 | Certain | No `internal/worktree/` package | Confirmed from intake #4 — dead code per architecture spec | S:90 R:90 A:90 D:95 |
| 5 | Certain | `just dev` replaces `dev.sh` | Confirmed from intake #5 — justfile is single task runner | S:95 R:85 A:90 D:95 |
| 6 | Certain | POST-only mutations in API client stubs | Confirmed from intake #6 — matches API spec POST convention | S:95 R:80 A:90 D:95 |
| 7 | Certain | Go module name `run-kit` with Go 1.22.0 | Upgraded from intake #7 (Confident→Certain) — verified from current go.mod | S:95 R:90 A:95 D:95 |
| 8 | Certain | Same frontend dependency versions (React 19, Tailwind 4, TanStack Router, xterm.js 5, Vite 7, Vitest 4) | Upgraded from intake #8 (Confident→Certain) — verified from current package.json | S:95 R:80 A:95 D:95 |
| 9 | Certain | `pnpm-workspace.yaml` updated to `["app/frontend"]` | Upgraded from intake #9 (Confident→Certain) — straightforward path change | S:95 R:90 A:95 D:95 |
| 10 | Certain | Same Go dependencies (chi, cors, gorilla/websocket, creack/pty, yaml.v3) | Verified from current `go.mod` — exact same dependency set | S:95 R:90 A:95 D:95 |
| 11 | Certain | Vitest config includes `@testing-library/jest-dom/vitest` setup | Verified from current `vitest.config.ts` and `test-setup.ts` | S:95 R:90 A:95 D:95 |
| 12 | Confident | `just dev` uses background processes with `&` and `wait` | justfile doesn't have `pnpm concurrently` available for the new setup; simple shell backgrounding is sufficient | S:70 R:90 A:80 D:80 |
| 13 | Confident | Placeholder smoke test uses `test.skip` to avoid failures when no server is running | Standard Playwright pattern; avoids false failures during `just test-e2e` at scaffold stage | S:75 R:95 A:85 D:85 |

13 assumptions (11 certain, 2 confident, 0 tentative, 0 unresolved).
