This repository has been archived on 2026-07-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
codespace/docs/superpowers/plans/2026-07-02-codespace-backend-skeleton.md
T
2026-07-02 15:07:54 +08:00

1088 lines
29 KiB
Markdown

# codespace Backend Skeleton Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a runnable Go backend MVP in the current project root that manages workspaces, file operations through an abstract filesystem, and OpenCode processes.
**Architecture:** HTTP handlers decode requests and delegate to service objects. Services coordinate `workspace.Manager`, `fs.FileSystem`, and `process.Manager`. Local filesystem and OpenCode process execution are implementation details hidden behind focused interfaces.
**Tech Stack:** Go 1.22+, standard library HTTP router, YAML config via `gopkg.in/yaml.v3`, local filesystem, OS processes.
## Global Constraints
- Current directory `/Users/taochen/llm/codespace` is the project root; do not create a nested `codespace/` directory.
- First version is a runnable backend MVP, not a complete IDE.
- Do not add Docker.
- Do not implement authentication, Git integration, LSP, AI Agent orchestration, web frontend, or full watcher/WebSocket event streaming.
- Handlers must not call `os.*` file APIs or `exec.*`; file and process work goes through services.
- File API paths are workspace-relative; never expose host absolute paths in file responses.
- Workspace IDs allow only letters, digits, `-`, `_`, and `.`.
- Prevent `../` and absolute-path escape from workspace roots.
- Process command defaults to `opencode`, but must be configurable.
- Current directory is not a git repository, so this plan does not include commit steps unless the user initializes git separately.
- Code generation/refactoring must be delegated to Codex MCP using exactly `model: "kimi/kimi-k2.7-code"`, `sandbox: "danger-full-access"`, and `approval-policy: "on-failure"`.
---
## File Structure
### Create
- `go.mod` — module declaration and dependencies.
- `Makefile` — local development commands.
- `README.md` — project overview, API examples, run/test instructions.
- `configs/config.yaml` — default runtime config.
- `cmd/server/main.go` — app entrypoint, config load, dependency wiring, HTTP server start.
- `pkg/config/config.go` — config structs and load logic.
- `pkg/response/response.go` — consistent JSON success/error responses.
- `pkg/logger/logger.go` — tiny standard-library logger wrapper.
- `internal/util/path.go` — reusable path validation helpers.
- `internal/util/errors.go` — typed app errors and helpers.
- `internal/workspace/workspace.go``Workspace` model.
- `internal/workspace/path.go` — workspace ID validation and root path construction.
- `internal/workspace/manager.go` — local workspace manager implementation.
- `internal/workspace/path_test.go` — workspace ID/path tests.
- `internal/fs/fileinfo.go` — file metadata model.
- `internal/fs/filesystem.go``FileSystem` interface.
- `internal/fs/localfs.go` — local filesystem implementation bound to one workspace root.
- `internal/fs/localfs_test.go` — path escape and file operation tests.
- `internal/process/session.go` — session/status structs.
- `internal/process/manager.go` — process manager interface and implementation.
- `internal/process/opencode.go` — OpenCode command/session creation details.
- `internal/process/manager_test.go` — status/start/stop tests with safe test command.
- `internal/model/workspace.go` — workspace API request/response structs.
- `internal/model/file.go` — file API request/response structs.
- `internal/model/process.go` — process API response structs.
- `internal/service/workspace_service.go` — workspace use cases.
- `internal/service/file_service.go` — file use cases.
- `internal/service/process_service.go` — process use cases.
- `internal/api/router.go` — HTTP router and route registration.
- `internal/api/workspace_handler.go` — workspace endpoints.
- `internal/api/file_handler.go` — file endpoints.
- `internal/api/process_handler.go` — process endpoints.
- `internal/api/router_test.go` — health/workspace/file/process route smoke tests.
- `internal/watcher/watcher.go` — narrow watcher interface placeholder.
- `web/.gitkeep` — keeps frontend placeholder directory.
- `docs/superpowers/specs/2026-07-02-codespace-design.md` — already created design spec.
- `docs/superpowers/plans/2026-07-02-codespace-backend-skeleton.md` — this plan.
### External dependency
Only one non-stdlib dependency is allowed for the first version:
```text
gopkg.in/yaml.v3
```
Everything else must use the Go standard library.
---
## Task 1: Initialize Go module, config, responses, and docs
**Files:**
- Create: `go.mod`
- Create: `Makefile`
- Create: `README.md`
- Create: `configs/config.yaml`
- Create: `pkg/config/config.go`
- Create: `pkg/response/response.go`
- Create: `pkg/logger/logger.go`
- Create: `web/.gitkeep`
**Interfaces:**
- Produces: `config.Config`, `config.Load(path string) (Config, error)`.
- Produces: `response.JSON(w http.ResponseWriter, status int, value any)`.
- Produces: `response.Error(w http.ResponseWriter, status int, code string, err error)`.
- Produces: `logger.New() *log.Logger`.
- [ ] **Step 1: Create module and default config**
Create `go.mod`:
```go
module codespace
go 1.22
require gopkg.in/yaml.v3 v3.0.1
```
Create `configs/config.yaml`:
```yaml
server:
addr: ":8080"
workspace:
root: "./workspaces"
process:
opencodeCommand: "opencode"
```
Create `web/.gitkeep` as an empty file.
- [ ] **Step 2: Implement config loader**
Create `pkg/config/config.go` with these exported names:
```go
package config
type Config struct {
Server ServerConfig `yaml:"server"`
Workspace WorkspaceConfig `yaml:"workspace"`
Process ProcessConfig `yaml:"process"`
}
type ServerConfig struct {
Addr string `yaml:"addr"`
}
type WorkspaceConfig struct {
Root string `yaml:"root"`
}
type ProcessConfig struct {
OpenCodeCommand string `yaml:"opencodeCommand"`
}
func Default() Config
func Load(path string) (Config, error)
```
Required behavior:
- `Default()` returns `:8080`, `./workspaces`, and `opencode`.
- `Load(path)` starts from defaults.
- If `path` exists, YAML values override defaults.
- If `path` does not exist, return defaults and no error.
- Environment variables override both defaults and file values:
- `CODESPACE_ADDR`
- `CODESPACE_WORKSPACE_ROOT`
- `CODESPACE_OPENCODE_COMMAND`
- [ ] **Step 3: Implement response helpers**
Create `pkg/response/response.go` with this response shape:
```go
type ErrorBody struct {
Error ErrorDetail `json:"error"`
}
type ErrorDetail struct {
Code string `json:"code"`
Message string `json:"message"`
}
```
Functions:
```go
func JSON(w http.ResponseWriter, status int, value any)
func Error(w http.ResponseWriter, status int, code string, err error)
```
Required behavior:
- Always set `Content-Type: application/json`.
- `JSON` writes the status code and JSON-encodes `value`.
- `Error` writes `{"error":{"code":"...","message":"..."}}`.
- [ ] **Step 4: Implement logger helper**
Create `pkg/logger/logger.go`:
```go
package logger
func New() *log.Logger
```
Required behavior:
- Use standard library `log.New(os.Stdout, "codespace ", log.LstdFlags|log.Lshortfile)`.
- [ ] **Step 5: Create README and Makefile**
Create `Makefile` with these targets:
```makefile
.PHONY: test run fmt
test:
go test ./...
run:
go run ./cmd/server
fmt:
gofmt -w cmd internal pkg
```
Create `README.md` with:
- Project purpose.
- First-version scope.
- `make test` and `make run` instructions.
- Example curl for `POST /api/workspaces` and `GET /healthz`.
- Explicit note that Docker/auth/Git/LSP/frontend are not implemented in v1.
- [ ] **Step 6: Validate task**
Run:
```sh
go mod tidy
go test ./...
```
Expected:
- `go mod tidy` succeeds.
- `go test ./...` succeeds or reports no test files for created packages.
---
## Task 2: Implement typed errors and workspace manager
**Files:**
- Create: `internal/util/errors.go`
- Create: `internal/util/path.go`
- Create: `internal/workspace/workspace.go`
- Create: `internal/workspace/path.go`
- Create: `internal/workspace/manager.go`
- Create: `internal/workspace/path_test.go`
**Interfaces:**
- Produces: `util.AppError`, `util.Code`, `util.New(code Code, message string) error`.
- Produces: `workspace.Workspace`.
- Produces: `workspace.Manager` interface.
- Produces: `workspace.LocalManager` implementation.
- Produces: `workspace.ValidID(id string) bool`.
- [ ] **Step 1: Write workspace path tests first**
Create `internal/workspace/path_test.go` with tests covering:
```go
func TestValidIDAcceptsSafeSegments(t *testing.T)
func TestValidIDRejectsUnsafeSegments(t *testing.T)
func TestRootForUsesConfiguredRoot(t *testing.T)
```
Cases:
- valid: `user1`, `project-A`, `project_A`, `project.1`
- invalid: empty string, `/tmp/x`, `../x`, `a/b`, `a b`, `中文`, `*`
- `RootFor("/tmp/workspaces", "user1")` returns `/tmp/workspaces/user1` after filepath cleaning.
- [ ] **Step 2: Implement typed errors**
Create `internal/util/errors.go` with codes:
```go
type Code string
const (
CodeBadRequest Code = "bad_request"
CodeNotFound Code = "not_found"
CodeConflict Code = "conflict"
CodeInternal Code = "internal"
)
type AppError struct {
Code Code
Message string
Err error
}
func (e *AppError) Error() string
func (e *AppError) Unwrap() error
func New(code Code, message string) error
func Wrap(code Code, message string, err error) error
func CodeOf(err error) Code
```
Required behavior:
- `CodeOf(nil)` returns `CodeInternal`.
- `CodeOf(err)` returns embedded `AppError.Code` if present.
- Non-`AppError` defaults to `CodeInternal`.
- [ ] **Step 3: Implement path helper**
Create `internal/util/path.go` with:
```go
func CleanRelative(path string) (string, error)
```
Required behavior:
- Empty path and `.` return `.`.
- Absolute paths are rejected with bad request.
- Paths that clean to `..` or start with `../` are rejected with bad request.
- Backslash should be normalized by filepath semantics on the current platform; do not implement Windows-specific policy in v1.
- [ ] **Step 4: Implement workspace model and manager**
Create `internal/workspace/workspace.go`:
```go
type Workspace struct {
ID string `json:"id"`
Root string `json:"root"`
}
```
Create `internal/workspace/path.go`:
```go
func ValidID(id string) bool
func RootFor(baseRoot, id string) string
```
Create `internal/workspace/manager.go`:
```go
type Manager interface {
Create(id string) (*Workspace, error)
Get(id string) (*Workspace, error)
Delete(id string) error
}
type LocalManager struct {
root string
}
func NewLocalManager(root string) *LocalManager
```
Required behavior:
- `NewLocalManager` stores an absolute, cleaned root if possible.
- `Create` validates ID, creates the directory with `0755`, and returns workspace.
- `Get` validates ID and returns not found if directory does not exist.
- `Delete` validates ID and removes the workspace directory recursively.
- Manager must never accept unsafe IDs.
- [ ] **Step 5: Run focused tests**
Run:
```sh
go test ./internal/workspace ./internal/util
```
Expected:
- All tests pass.
---
## Task 3: Implement FileSystem interface and LocalFS
**Files:**
- Create: `internal/fs/fileinfo.go`
- Create: `internal/fs/filesystem.go`
- Create: `internal/fs/localfs.go`
- Create: `internal/fs/localfs_test.go`
**Interfaces:**
- Produces: `fs.FileInfo`.
- Produces: `fs.FileSystem`.
- Produces: `fs.NewLocal(root string) FileSystem`.
- [ ] **Step 1: Write LocalFS tests first**
Create `internal/fs/localfs_test.go` with tests:
```go
func TestLocalFSWriteReadListStat(t *testing.T)
func TestLocalFSRejectsPathEscape(t *testing.T)
func TestLocalFSRenameAndRemove(t *testing.T)
```
Required assertions:
- Write `dir/main.go` with content `package main\n`.
- Read returns exact bytes.
- List `dir` returns one item named `main.go` with path `dir/main.go` and `IsDir == false`.
- Stat `dir/main.go` returns size equal to content length.
- Read `../outside`, Write `../outside`, Mkdir `../outside`, Remove `../outside`, Rename `file` to `../outside` all fail.
- Rename `old.txt` to `new.txt` makes `old.txt` missing and `new.txt` readable.
- [ ] **Step 2: Define file models and interface**
Create `internal/fs/fileinfo.go`:
```go
type FileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
ModTime time.Time `json:"modTime"`
}
```
Create `internal/fs/filesystem.go`:
```go
type FileSystem interface {
List(path string) ([]FileInfo, error)
Read(path string) ([]byte, error)
Write(path string, data []byte) error
Mkdir(path string) error
Remove(path string) error
Rename(oldPath, newPath string) error
Stat(path string) (*FileInfo, error)
}
```
- [ ] **Step 3: Implement LocalFS safely**
Create `internal/fs/localfs.go` with:
```go
type LocalFS struct {
root string
}
func NewLocal(root string) *LocalFS
```
Required behavior:
- Store root as an absolute cleaned path if possible.
- A private `resolve(path string) (absolute string, relative string, error)` must call `util.CleanRelative` and ensure the final absolute path stays under `root`.
- `Write` creates parent directories with `0755` and writes file with `0644`.
- `Mkdir` creates directories with `0755`.
- `List` returns sorted entries by name for deterministic tests.
- `Read`, `List`, and `Stat` map missing files to `CodeNotFound`.
- `Remove` removes files or directories recursively.
- `Rename` creates destination parent directories and rejects destination escape.
- [ ] **Step 4: Run focused tests**
Run:
```sh
go test ./internal/fs ./internal/util
```
Expected:
- All tests pass.
---
## Task 4: Implement process manager for OpenCode sessions
**Files:**
- Create: `internal/process/session.go`
- Create: `internal/process/manager.go`
- Create: `internal/process/opencode.go`
- Create: `internal/process/manager_test.go`
**Interfaces:**
- Produces: `process.Status`.
- Produces: `process.Manager` interface.
- Produces: `process.NewManager(command string) *LocalManager`.
- [ ] **Step 1: Write process manager tests first**
Create `internal/process/manager_test.go` with tests:
```go
func TestStatusBeforeStartIsNotRunning(t *testing.T)
func TestStartRejectsDuplicateRunningSession(t *testing.T)
func TestStopMissingSessionIsNotFound(t *testing.T)
```
Use a safe long-running command:
```go
cmd := os.Args[0]
args := []string{"-test.run=TestHelperProcess", "--"}
```
And helper pattern:
```go
func TestHelperProcess(t *testing.T) {
if os.Getenv("CODESPACE_HELPER_PROCESS") != "1" {
return
}
select {}
}
```
The implementation must allow tests to override command and args, either through a constructor option or an unexported test helper in the same package.
- [ ] **Step 2: Define session/status structs**
Create `internal/process/session.go`:
```go
type Status struct {
WorkspaceID string `json:"workspaceId"`
Running bool `json:"running"`
PID int `json:"pid,omitempty"`
}
type Session struct {
WorkspaceID string
Root string
Cmd *exec.Cmd
}
```
- [ ] **Step 3: Define manager interface and implementation**
Create `internal/process/manager.go`:
```go
type Manager interface {
Start(workspaceID string, workspaceRoot string) error
Stop(workspaceID string) error
Restart(workspaceID string, workspaceRoot string) error
Status(workspaceID string) Status
}
type LocalManager struct {
command string
args []string
mu sync.Mutex
sessions map[string]*Session
}
func NewManager(command string) *LocalManager
```
Required behavior:
- Empty command defaults to `opencode`.
- `Start` rejects duplicate running session with `CodeConflict`.
- `Start` uses `exec.Command(command, args...)`.
- `Start` sets `cmd.Dir = workspaceRoot`.
- `Start` appends `HOME=<workspaceRoot>` to environment.
- `Start` starts the process and stores session by workspace ID.
- `Stop` returns not found if no running session exists.
- `Stop` kills the process and removes the session.
- `Restart` calls `Stop` if running, then `Start`.
- `Status` returns running and PID only if a session exists and process is started.
- [ ] **Step 4: Implement OpenCode command helper**
Create `internal/process/opencode.go` with small helpers only:
```go
const DefaultOpenCodeCommand = "opencode"
func normalizeCommand(command string) string
```
- [ ] **Step 5: Run focused tests**
Run:
```sh
go test ./internal/process
```
Expected:
- All tests pass.
- No real `opencode` binary is required for tests.
---
## Task 5: Implement services and API models
**Files:**
- Create: `internal/model/workspace.go`
- Create: `internal/model/file.go`
- Create: `internal/model/process.go`
- Create: `internal/service/workspace_service.go`
- Create: `internal/service/file_service.go`
- Create: `internal/service/process_service.go`
**Interfaces:**
- Produces: model request/response structs used by handlers.
- Produces: `service.WorkspaceService`.
- Produces: `service.FileService`.
- Produces: `service.ProcessService`.
- [ ] **Step 1: Create API models**
Create `internal/model/workspace.go`:
```go
type CreateWorkspaceRequest struct {
ID string `json:"id"`
}
type WorkspaceResponse struct {
ID string `json:"id"`
}
```
Do not include host root path in API responses.
Create `internal/model/file.go`:
```go
type WriteFileRequest struct {
Path string `json:"path"`
Content string `json:"content"`
}
type MkdirRequest struct {
Path string `json:"path"`
}
type RenameRequest struct {
OldPath string `json:"oldPath"`
NewPath string `json:"newPath"`
}
type ReadFileResponse struct {
Path string `json:"path"`
Content string `json:"content"`
}
```
Create `internal/model/process.go`:
```go
type ProcessStatusResponse struct {
WorkspaceID string `json:"workspaceId"`
Running bool `json:"running"`
PID int `json:"pid,omitempty"`
}
```
- [ ] **Step 2: Implement WorkspaceService**
Create `internal/service/workspace_service.go`:
```go
type WorkspaceService struct {
workspaces workspace.Manager
processes process.Manager
}
func NewWorkspaceService(workspaces workspace.Manager, processes process.Manager) *WorkspaceService
func (s *WorkspaceService) Create(id string) (*workspace.Workspace, error)
func (s *WorkspaceService) Get(id string) (*workspace.Workspace, error)
func (s *WorkspaceService) Delete(id string) error
```
Required behavior:
- `Delete` checks process status and stops it if running before deleting workspace.
- [ ] **Step 3: Implement FileService**
Create `internal/service/file_service.go`:
```go
type FileService struct {
workspaces workspace.Manager
}
func NewFileService(workspaces workspace.Manager) *FileService
func (s *FileService) List(workspaceID, path string) ([]fs.FileInfo, error)
func (s *FileService) Read(workspaceID, path string) ([]byte, error)
func (s *FileService) Write(workspaceID, path string, data []byte) error
func (s *FileService) Mkdir(workspaceID, path string) error
func (s *FileService) Remove(workspaceID, path string) error
func (s *FileService) Rename(workspaceID, oldPath, newPath string) error
func (s *FileService) Stat(workspaceID, path string) (*fs.FileInfo, error)
```
Required behavior:
- Each method calls `workspaces.Get(workspaceID)` first.
- Each method constructs `fs.NewLocal(ws.Root)` and delegates.
- [ ] **Step 4: Implement ProcessService**
Create `internal/service/process_service.go`:
```go
type ProcessService struct {
workspaces workspace.Manager
processes process.Manager
}
func NewProcessService(workspaces workspace.Manager, processes process.Manager) *ProcessService
func (s *ProcessService) Start(workspaceID string) error
func (s *ProcessService) Stop(workspaceID string) error
func (s *ProcessService) Restart(workspaceID string) error
func (s *ProcessService) Status(workspaceID string) (process.Status, error)
```
Required behavior:
- `Start`, `Restart`, and `Status` verify workspace exists.
- `Stop` should return process manager's result directly; if no process exists, return not found.
- `Restart` uses workspace root from manager.
- [ ] **Step 5: Run package tests**
Run:
```sh
go test ./internal/service ./internal/model ./internal/workspace ./internal/fs ./internal/process
```
Expected:
- All packages compile and tests pass.
---
## Task 6: Implement HTTP router and handlers
**Files:**
- Create: `internal/api/router.go`
- Create: `internal/api/workspace_handler.go`
- Create: `internal/api/file_handler.go`
- Create: `internal/api/process_handler.go`
- Create: `internal/api/router_test.go`
**Interfaces:**
- Produces: `api.NewRouter(workspaces *service.WorkspaceService, files *service.FileService, processes *service.ProcessService) http.Handler`.
- [ ] **Step 1: Write HTTP smoke tests first**
Create `internal/api/router_test.go` with tests:
```go
func TestHealthz(t *testing.T)
func TestCreateWorkspace(t *testing.T)
func TestWriteReadFile(t *testing.T)
func TestProcessStatus(t *testing.T)
```
Required assertions:
- `GET /healthz` returns `200` and `{"status":"ok"}`.
- `POST /api/workspaces` with `{"id":"user1"}` returns `201` and `{"id":"user1"}`.
- `PUT /api/workspaces/user1/files/write` with `{"path":"main.go","content":"package main\n"}` returns `204`.
- `GET /api/workspaces/user1/files/read?path=main.go` returns content.
- `GET /api/workspaces/user1/process/status` returns `200`, workspace ID, and `running:false`.
Test setup must use `t.TempDir()` for workspace root and a process manager with a harmless command. Do not require real OpenCode.
- [ ] **Step 2: Implement router**
Create `internal/api/router.go`:
```go
func NewRouter(workspaces *service.WorkspaceService, files *service.FileService, processes *service.ProcessService) http.Handler
```
Required routes:
```text
GET /healthz
POST /api/workspaces
GET /api/workspaces/{id}
DELETE /api/workspaces/{id}
GET /api/workspaces/{id}/files
GET /api/workspaces/{id}/files/read
PUT /api/workspaces/{id}/files/write
POST /api/workspaces/{id}/files/mkdir
DELETE /api/workspaces/{id}/files
POST /api/workspaces/{id}/files/rename
POST /api/workspaces/{id}/process/start
POST /api/workspaces/{id}/process/stop
POST /api/workspaces/{id}/process/restart
GET /api/workspaces/{id}/process/status
```
Use only the standard library. Since Go 1.22 `http.ServeMux` supports method/path patterns, use patterns like:
```go
mux.HandleFunc("GET /healthz", healthHandler)
mux.HandleFunc("POST /api/workspaces", workspaceHandler.create)
mux.HandleFunc("GET /api/workspaces/{id}", workspaceHandler.get)
```
Use `r.PathValue("id")` for path variables.
- [ ] **Step 3: Implement workspace handler**
Create `internal/api/workspace_handler.go`.
Required behavior:
- Decode JSON for create.
- Empty or invalid JSON returns `400`.
- Create success returns `201` with `model.WorkspaceResponse{ID: ws.ID}`.
- Get success returns `200` with ID only.
- Delete success returns `204`.
- Map `util.CodeBadRequest` to HTTP 400, `CodeNotFound` to 404, `CodeConflict` to 409, default to 500.
- [ ] **Step 4: Implement file handler**
Create `internal/api/file_handler.go`.
Required behavior:
- `List` reads query `path`; default `.`.
- `Read` reads query `path`; missing path returns `400`.
- `Write` decodes `model.WriteFileRequest`; missing path returns `400`; success returns `204`.
- `Mkdir` decodes `model.MkdirRequest`; missing path returns `400`; success returns `204`.
- `Remove` reads query `path`; missing path returns `400`; success returns `204`.
- `Rename` decodes `model.RenameRequest`; missing old or new path returns `400`; success returns `204`.
- File read returns `model.ReadFileResponse` with content as UTF-8 string.
- [ ] **Step 5: Implement process handler**
Create `internal/api/process_handler.go`.
Required behavior:
- Start/stop/restart return `204` on success.
- Status returns `model.ProcessStatusResponse`.
- Error mapping matches workspace handler.
- [ ] **Step 6: Run HTTP tests**
Run:
```sh
go test ./internal/api
```
Expected:
- All tests pass.
---
## Task 7: Wire server entrypoint and watcher placeholder
**Files:**
- Create: `cmd/server/main.go`
- Create: `internal/watcher/watcher.go`
**Interfaces:**
- Produces runnable server with `go run ./cmd/server`.
- Produces placeholder `watcher.Watcher` interface for future expansion.
- [ ] **Step 1: Implement watcher placeholder**
Create `internal/watcher/watcher.go`:
```go
type Event struct {
Type string `json:"type"`
Path string `json:"path"`
}
type Watcher interface {
Start(root string) error
Stop() error
Events() <-chan Event
}
```
Do not wire this into API in v1.
- [ ] **Step 2: Implement server main**
Create `cmd/server/main.go`.
Required behavior:
- Load config from `configs/config.yaml` with `config.Load`.
- Create logger with `logger.New()`.
- Create workspace manager with configured root.
- Create process manager with configured OpenCode command.
- Create services.
- Create router.
- Start HTTP server at configured address.
- Log address and workspace root.
Main dependency flow:
```go
cfg, err := config.Load("configs/config.yaml")
workspaces := workspace.NewLocalManager(cfg.Workspace.Root)
processes := process.NewManager(cfg.Process.OpenCodeCommand)
workspaceSvc := service.NewWorkspaceService(workspaces, processes)
fileSvc := service.NewFileService(workspaces)
processSvc := service.NewProcessService(workspaces, processes)
handler := api.NewRouter(workspaceSvc, fileSvc, processSvc)
server := &http.Server{Addr: cfg.Server.Addr, Handler: handler}
```
- [ ] **Step 3: Verify server starts**
Run:
```sh
go run ./cmd/server
```
Expected:
- Server logs that it is listening on `:8080`.
- Stop it manually after confirming startup.
In another shell, if needed:
```sh
curl -s http://localhost:8080/healthz
```
Expected response:
```json
{"status":"ok"}
```
---
## Task 8: Final validation and cleanup
**Files:**
- Modify only files created by Tasks 1-7 if validation exposes issues.
**Interfaces:**
- Produces fully validated backend skeleton.
- [ ] **Step 1: Format all Go code**
Run:
```sh
gofmt -w cmd internal pkg
```
Expected:
- Command exits successfully.
- [ ] **Step 2: Tidy modules**
Run:
```sh
go mod tidy
```
Expected:
- `go.sum` is created or updated if required by `gopkg.in/yaml.v3`.
- [ ] **Step 3: Run all tests**
Run:
```sh
go test ./...
```
Expected:
- All tests pass.
- [ ] **Step 4: Check handler boundary rule**
Run:
```sh
grep -R "os\.\|exec\." internal/api || true
```
Expected:
- No matches.
If matches exist, move those calls out of `internal/api` into services or lower-level packages.
- [ ] **Step 5: Run server smoke test**
Run server:
```sh
go run ./cmd/server
```
In another terminal:
```sh
curl -s http://localhost:8080/healthz
curl -s -X POST http://localhost:8080/api/workspaces \
-H 'Content-Type: application/json' \
-d '{"id":"user1"}'
curl -s -X PUT http://localhost:8080/api/workspaces/user1/files/write \
-H 'Content-Type: application/json' \
-d '{"path":"main.go","content":"package main\n"}'
curl -s 'http://localhost:8080/api/workspaces/user1/files/read?path=main.go'
```
Expected:
- Health returns `{"status":"ok"}`.
- Workspace create returns `{"id":"user1"}`.
- Write returns no body with HTTP 204.
- Read returns `{"path":"main.go","content":"package main\n"}`.
- [ ] **Step 6: Document final result**
Update `README.md` only if the actual commands or routes differ from the planned ones. Do not add future-roadmap promises beyond the explicit v1 non-goals.
---
## Codex Execution Prompt
Use this exact structure when dispatching Codex for implementation:
```text
## Context
- Project root: /Users/taochen/llm/codespace
- Tech Stack: Go 1.22+, standard library HTTP router, gopkg.in/yaml.v3 for config.
- Design spec: docs/superpowers/specs/2026-07-02-codespace-design.md
- Implementation plan: docs/superpowers/plans/2026-07-02-codespace-backend-skeleton.md
- Current directory is the project root. Do not create a nested codespace directory.
## Task
Generate the runnable Go backend MVP skeleton described by the implementation plan.
Steps:
1. Create the files listed in Tasks 1-7.
2. Implement tests before or alongside implementation for workspace path validation, LocalFS, process manager, and HTTP routes.
3. Run formatting, module tidy, tests, and boundary checks from Task 8.
## Constraints
- Do not add Docker, auth, Git, LSP, AI Agent orchestration, frontend, or full watcher/WebSocket streaming.
- Do not expose host absolute paths in API responses.
- Handlers must not call os.* file APIs or exec.*; use services.
- Prevent workspace path escape and unsafe workspace IDs.
- Use only standard library plus gopkg.in/yaml.v3.
- If an existing file has meaningful content, stop and report before overwriting it.
## Acceptance
- gofmt -w cmd internal pkg succeeds.
- go mod tidy succeeds.
- go test ./... passes.
- grep -R "os\.\|exec\." internal/api || true returns no matches.
- go run ./cmd/server starts and /healthz returns {"status":"ok"}.
```
---
## Self-Review
- Spec coverage: The plan covers project root handling, runnable backend, workspace manager, FileSystem abstraction, LocalFS, process manager, services, API handlers, config, docs, tests, and watcher placeholder. Non-goals are explicitly excluded.
- Placeholder scan: No `TBD`, `TODO`, or undefined future implementation steps remain.
- Type consistency: Function names and signatures are consistent across file structure, task interfaces, and wiring steps.
- Scope check: The plan is one cohesive MVP backend skeleton. Git, LSP, AI Agent, frontend, and watcher streaming remain outside v1 as required.