Skip to content

feat(ao): ao spawn CLI + POST /api/v1/sessions route#71

Merged
harshitsinghbhandari merged 4 commits into
stagingfrom
feat/ao-spawn-cli
Jun 1, 2026
Merged

feat(ao): ao spawn CLI + POST /api/v1/sessions route#71
harshitsinghbhandari merged 4 commits into
stagingfrom
feat/ao-spawn-cli

Conversation

@harshitsinghbhandari
Copy link
Copy Markdown
Collaborator

Summary

This is the user-facing CLI for the Session Manager that PR #70 wired into the daemon. After this lands, a user can run ao spawn --project <id> --prompt "<task>" and a real agent boots in a real git worktree in a real zellij pane.

  • New POST /api/v1/sessions route in httpd/controllers/sessions.go. Body {projectId, prompt, agent?} → 201 {sessionId, workspacePath, runtimeHandle}. Maps to 400 (missing/invalid fields), 404 (unknown project — unwrapped from *project.Error through the linear %w chain session.Manager.Spawn produces), 503 sessions_disabled (SM nil in APIDeps), 500 SPAWN_FAILED (other failures).
  • New session.Spawner interface so the controller depends on the narrow seam *session.Manager already satisfies — symmetric with how project.Manager is consumed by the projects controller. The controller field is session.Spawner; a var _ Spawner = (*Manager)(nil) compile-time assertion locks the satisfaction.
  • New ao spawn cobra subcommand in internal/cli/spawn.go (next to the existing start/stop/status subcommands — cmd/ao/main.go is the thin shim, internal/cli/ is where every subcommand lives). Reads the daemon address from the runfile, POSTs to /api/v1/sessions, prints Spawned session <id> in <workspacePath>\nAttach: zellij attach <runtimeHandle> on 201. Empty prompt or missing project → usage error (exit 2). 4xx/5xx/network → exit 1 with the server's error surfaced via stderr.
  • daemon.Run now constructs the session stack BEFORE httpd.NewWithDeps so the SM can be plumbed into APIDeps.Sessions. The original code had a _ = ss placeholder explicitly noting that γ would land routes; this PR removes it.

Out of scope (kept out per the brief): --open auto-attach, --claim-pr, positional <issue> (needs tracker integration), restore/list/get/kill HTTP routes.

Prerequisite: #70 (wires SM + agent shim + RepoResolver + inbox messenger into the daemon).

Test plan

  • TDD followed — controller test and CLI test went RED before each implementation
  • go test -race ./... green (only pre-existing failure is terminal.TestSessionStreamsRealZellijPane, a zellij-required integration test that fails identically on staging without these changes)
  • go vet ./... clean
  • gofmt -l backend/ clean
  • Self-review via superpowers:code-reviewer — ship-ready, no critical/important findings
  • httpd controller test covers 201/400/404/503/500 with a fake session.Spawner recording the SpawnConfig it received
  • CLI integration test against httptest.NewServer covers 201/4xx/5xx/network-down and exit codes 0/1/2
  • Real production error wrapping shape (fmt.Errorf("spawn %s: workspace: %w", id, projectresolver-wrap)) exercised so the errors.As unwrap to *project.Error is verified end-to-end

🤖 Generated with Claude Code

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR introduces ao spawn as the user-facing CLI for the Session Manager by adding a POST /api/v1/sessions HTTP route and a matching ao spawn --project <id> --prompt "<task>" subcommand that wires end-to-end into the already-landed daemon infrastructure from #70.

  • session.Spawner interface (session/spawner.go) narrows the controller's dependency to a single Spawn method, with a compile-time var _ Spawner = (*Manager)(nil) assertion; sessions.go controller maps the SM's errors to 201/400/404/503/500, surfacing only client-flavoured project.Error kinds verbatim and hiding internal kinds behind the generic SPAWN_FAILED envelope.
  • ao spawn CLI (cli/spawn.go) uses a dedicated http.Client with a 90 s context deadline (well above the server's 60 s DefaultRequestTimeout) so synchronous worktree/zellij/agent setup never races against a client-side timeout, and exits 2 for usage errors and 1 for runtime/server errors.
  • Daemon wiring (daemon.go) reorders startup so buildSessionStack runs before httpd.NewWithDeps, enabling ss.sm to be plumbed into APIDeps.Sessions; several ancillary files receive doc comments, a security-tightened inbox permission (0o600/0o750), and minor lint fixes.

Confidence Score: 5/5

Safe to merge. The new route and CLI subcommand are well-isolated and the error classification logic is tested exhaustively, including the previously addressed opaque-internal-error contract.

All three layers — daemon wiring, HTTP controller, and CLI — are implemented consistently with the existing patterns in the codebase. The two issues raised in prior review threads (client timeout too short, internal project.Error leaking) were fixed before this review. Error paths in daemon.go correctly cancel the context before returning, so session-stack goroutines are bounded by signal propagation through ctx. The narrow Spawner interface with a compile-time assertion and thorough table-driven tests on both the controller and CLI sides leave little room for hidden breakage.

No files require special attention.

Important Files Changed

Filename Overview
backend/internal/httpd/controllers/sessions.go New controller wires POST /sessions with correct nil-guard (503), field validation, and a well-documented writeSpawnError that only exposes client-flavoured project.Error kinds (bad_request/not_found/conflict) and hides all others behind SPAWN_FAILED.
backend/internal/cli/spawn.go New CLI subcommand with a dedicated http.Client and 90 s context deadline, proper exit-code semantics (2 for usage, 1 for runtime/server errors), and clean error surfacing from the JSON envelope.
backend/internal/session/spawner.go Minimal narrow interface with compile-time satisfaction assertion; clean seam that lets controller tests substitute a fake without real runtime dependencies.
backend/internal/daemon/daemon.go Startup reordering moves buildSessionStack before httpd.NewWithDeps so ss.sm can be passed into APIDeps; error paths correctly stop lcStack and cdcPipe, and ss goroutines are bounded by context cancellation via stop().
backend/internal/httpd/api.go Adds Sessions field to APIDeps and wires SessionsController into the chi router group alongside the existing projects controller; symmetric with existing pattern.
backend/internal/httpd/controllers/sessions_test.go Comprehensive table-driven tests covering 201/400/404/503/500, default-agent logic, wrapped project.Error unwrap, and the InternalKindIsOpaque contract; uses a thread-safe fakeSpawner with mutex.
backend/internal/cli/spawn_test.go Integration tests against httptest.Server cover all exit-code paths (0/1/2) and stdout shape; captured request body validated via JSON unmarshal.
backend/internal/adapters/messenger/inbox/inbox.go Permission tightened from 0o644→0o600 for inbox files and 0o755→0o750 for the inbox directory; both are security improvements assuming single-owner operation.
backend/.golangci.yml Adds G703 gosec exclusion with the same rationale as the existing G304 exclusion — paths are daemon-owned, not user-supplied.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as ao spawn (CLI)
    participant Daemon as Daemon (httpd)
    participant SC as SessionsController
    participant SM as session.Manager
    participant WS as Workspace/Zellij

    User->>CLI: ao spawn --project id --prompt task
    CLI->>CLI: validate --prompt, --project (exit 2 if empty)
    CLI->>CLI: read runfile to get daemon port
    CLI->>CLI: context.WithTimeout(90s)
    CLI->>Daemon: POST /api/v1/sessions
    Daemon->>SC: spawn(w, r)
    SC->>SC: nil Mgr check returns 503 sessions_disabled
    SC->>SC: validate projectId and prompt returns 400
    SC->>SM: Spawn(ctx, SpawnConfig)
    SM->>WS: create git worktree
    SM->>WS: launch zellij pane
    SM->>WS: start agent process
    WS-->>SM: Session with ID workspacePath runtimeHandle
    SM-->>SC: domain.Session or project.Error
    SC->>SC: writeSpawnError maps kind to status
    SC-->>Daemon: 201 with sessionId workspacePath runtimeHandle
    Daemon-->>CLI: HTTP 201
    CLI-->>User: Spawned session id in path, Attach zellij attach handle
Loading

Reviews (3): Last reviewed commit: "fix(lint): lift loop condition in scm po..." | Re-trigger Greptile

Comment thread backend/internal/cli/spawn.go Outdated
Comment thread backend/internal/httpd/controllers/sessions.go
Review fixes (PR #71):
- spawn CLI now uses a dedicated 90 s timeout (90 s > server's 60 s
  DefaultRequestTimeout) via context.WithTimeout, and stops sharing
  deps.HTTPClient — that client is sized for fast /healthz/shutdown
  probes (2 s) and was preempting the synchronous Spawn long before the
  daemon could finish provisioning a worktree + zellij pane + agent.
- Harden writeSpawnError so a *project.Error with a non-client Kind
  ("internal", "not_implemented", or anything unknown) falls through to
  the generic 500 SPAWN_FAILED envelope instead of passing the project
  error's Code/Message verbatim to the client. Adds three subtests that
  pin down the opacity contract.

Lint debt cleared (inherited from PRs #65/#70):
- Add doc comments on every exported symbol in the agent / claudecode /
  codex / adapters-registry packages (revive: exported)
- gosec G306/G301: inbox file/dir perms 0644→0600 and 0755→0750
- gosec G703 (path traversal via taint): excluded globally with the same
  rationale as G304 — adapter paths are daemon-config/worktree-derived,
  not user input
- gocritic emptyStringTest: len(strings.TrimSpace(...)) > 0 → != ""
- gocritic paramTypeCombine: combine adjacent same-type params
- errcheck: wrap deferred os.Remove(tmpName) in a closure
- prealloc: preallocate cmd slices on the resume paths
Inherited from PR #72 merging into staging after this branch opened.
golangci-lint v2.12.2 → 0 issues.
@harshitsinghbhandari harshitsinghbhandari merged commit e72b5b9 into staging Jun 1, 2026
7 checks passed
harshitsinghbhandari added a commit that referenced this pull request Jun 1, 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