Skip to content

Add ao spawn + ao project add (spawn a real worker end-to-end)#77

Merged
harshitsinghbhandari merged 6 commits into
mainfrom
codex/ao-spawn
Jun 2, 2026
Merged

Add ao spawn + ao project add (spawn a real worker end-to-end)#77
harshitsinghbhandari merged 6 commits into
mainfrom
codex/ao-spawn

Conversation

@yyovil
Copy link
Copy Markdown
Collaborator

@yyovil yyovil commented Jun 2, 2026

Summary

Closes the last gap to a live ao spawn: a registered project can now spawn a real worker agent (claude-code/codex) in a zellij pane, end-to-end from the CLI. Stacked on #65 (the agent-adapter + session-manager/daemon wiring this builds on).

What's new

  • DB-backed RepoResolver — the daemon resolves a project's on-disk repo path from the projects table, replacing the empty StaticRepoResolver{} that failed every lookup. A registered project is now spawnable.
  • Server-side branch defaulting — an empty spawn branch becomes ao/<session-id> (unique per session; gitworktree can't add a worktree on a branch already checked out, e.g. main).
  • ao project add --path <repo> — register a local git repo (POST /api/v1/projects).
  • ao spawn --project <id> [--harness] [--branch] [--prompt] [--issue] — spawn a worker (POST /api/v1/sessions); harness defaults to the daemon's AO_AGENT.
  • Shared postJSON daemon client (run-file port lookup + API error envelope surfacing).

Demo flow

ao start
ao project add --path /path/to/a/git/repo --id demo
ao spawn --project demo --harness claude-code --prompt "say hi"
zellij attach demo-1     # watch Claude run in the pane

Requires zellij and the claude/codex binary on PATH.

Tests

  • projectRepoResolver resolves a registered project / errors on an unregistered one.
  • session_manager defaults the branch to ao/<id> and keeps an explicit branch.
  • ao spawn/ao project add reject missing required flags before any network call.
  • go build / go vet / golangci-lint (0 issues) / gofmt clean.

Base is codex/agents-plugin for a clean stacked diff. After #65 merges, retarget this PR to main.

Copilot AI review requested due to automatic review settings June 2, 2026 02:49
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR wires up the full ao spawn and ao project add end-to-end flow: a DB-backed projectRepoResolver replaces the stub StaticRepoResolver, server-side branch defaulting ensures unique per-session worktrees, and two new CLI commands (spawn, project add) drive the daemon over a shared postJSON client.

  • projectRepoResolver resolves registered projects from the SQLite projects table, wrapping lookup failures in ErrProjectNotResolvable so writeSessionError maps them to HTTP 400. The archived-project and missing-path guard clauses make failures explicit rather than opaque.
  • Branch defaulting in session_manager.Spawn computes ao/<session-id> before the workspace is created, ensuring gitworktree never tries to add a worktree on a branch that's already checked out.
  • CLI commands (spawn, project add) define local request/response structs and share the postJSON client; a DTO-drift E2E test exercises the real HTTP round-trip through the actual router to detect silent JSON field-name divergence.

Confidence Score: 5/5

Safe to merge — all critical paths (repo resolution, branch defaulting, error mapping) are correct and covered by tests.

The three core behavioural changes — DB-backed repo resolution, per-session branch defaulting, and the CLI commands — are all wired together correctly. Error wrapping with %w is consistent throughout so the ErrProjectNotResolvable to HTTP 400 mapping works end-to-end. The DTO-drift E2E test exercises the real HTTP router, catching any JSON field-name divergence at test time rather than at runtime. The zellij socket-dir fix is well-guarded (MkdirAll + warn-not-abort) and the socket-path budget test verifies the constraint quantitatively.

No files require special attention.

Important Files Changed

Filename Overview
backend/internal/cli/client.go New shared postJSON helper: reads run-file for port, validates daemon liveness, sets a 2-minute client timeout on a copy of the injected HTTP client, and surfaces the API error envelope.
backend/internal/cli/spawn.go New ao spawn command: validates --project before any network call, POSTs spawnRequest, and prints a copy-pasteable attach hint using zellij.SessionName and zellij.DefaultSocketDir.
backend/internal/daemon/lifecycle_wiring.go Replaces StaticRepoResolver with projectRepoResolver backed by SQLite; guards archived projects and missing paths, wrapping all failures in ErrProjectNotResolvable for HTTP 400 mapping.
backend/internal/session_manager/manager.go Branch defaulting added before workspace.Create: defaults to ao/{session-id}; ws.Branch (not the local variable) is persisted in metadata.
backend/internal/service/project/service.go validateProjectID now rejects embedded .. to prevent invalid git branch names surfacing as opaque 500s at spawn time.
backend/internal/adapters/runtime/zellij/zellij.go DefaultSocketDir and SessionName exported; fixes macOS unix socket path budget overflow for long session IDs.

Reviews (6): Last reviewed commit: "fix(cli,daemon): address review findings..." | Re-trigger Greptile

Comment thread backend/internal/cli/spawn.go Outdated
Comment thread backend/internal/daemon/lifecycle_wiring.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR closes the loop for an end-to-end “spawn a real worker” flow by adding CLI commands to register a local repo as a project and then spawn a worker session against that registered project. On the daemon side, it replaces the prior always-failing static repo resolver with a DB-backed resolver and adds server-side branch defaulting to ensure git worktree creation succeeds when the user doesn’t specify a branch.

Changes:

  • Default session worktree branch to ao/<session-id> when none is provided during spawn.
  • Wire a DB-backed RepoResolver that resolves a project’s repo path from the projects table.
  • Add CLI commands ao project add and ao spawn, plus a shared postJSON client helper and basic “missing required flags” tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
backend/internal/session_manager/manager.go Defaults spawn branch to a unique per-session branch before creating the worktree.
backend/internal/session_manager/manager_test.go Adds unit tests for default-branch behavior and preserving explicit branches.
backend/internal/daemon/lifecycle_wiring.go Switches gitworktree wiring to a DB-backed repo resolver and implements projectRepoResolver.
backend/internal/daemon/wiring_test.go Adds a resolver test ensuring registered projects resolve to repo paths and unregistered projects error.
backend/internal/cli/root.go Registers new spawn and project commands on the CLI root.
backend/internal/cli/client.go Adds shared postJSON helper for POST requests to the daemon API.
backend/internal/cli/spawn.go Implements ao spawn command (POST /api/v1/sessions).
backend/internal/cli/spawn_test.go Adds fast-fail tests for missing required flags (no network calls).
backend/internal/cli/project.go Implements ao project add (POST /api/v1/projects).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/internal/cli/spawn.go
Comment thread backend/internal/cli/spawn.go Outdated
Comment thread backend/internal/daemon/lifecycle_wiring.go
Comment thread backend/internal/daemon/lifecycle_wiring.go Outdated
Comment thread backend/internal/cli/client.go Outdated
Comment thread backend/internal/cli/client.go
yyovil added a commit that referenced this pull request Jun 2, 2026
- `ao spawn` no longer prints a branch the sessions API doesn't return
  (session metadata is json:"-"), so the output is no longer misleading.
- Unregistered/archived/no-path projects now surface a 400
  PROJECT_NOT_RESOLVABLE with an actionable message instead of a generic
  500: a new sessionmanager.ErrProjectNotResolvable sentinel the resolver
  wraps and writeSessionError maps.
- postJSON reuses the injected Deps.HTTPClient (cloned, with a longer
  timeout) instead of a fresh client, keeping HTTP behaviour stubbable.
- postJSON treats a stale run-file (dead PID) as "not running" via
  ProcessAlive, matching its docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@harshitsinghbhandari harshitsinghbhandari changed the base branch from codex/agents-plugin to main June 2, 2026 11:33
yyovil and others added 5 commits June 2, 2026 17:06
Make a registered project spawnable end-to-end from the CLI:

- DB-backed RepoResolver: the daemon resolves a project's on-disk repo
  path from the projects table (replacing the empty StaticRepoResolver
  that failed every lookup), so a session's worktree is cut from the
  right repo.
- session_manager defaults an empty spawn branch to ao/<session-id> — a
  fresh, unique branch per session, since gitworktree can't reuse a
  branch already checked out elsewhere (e.g. main).
- `ao project add --path <repo>`: register a local git repo (POST /api/v1/projects).
- `ao spawn --project <id> [--harness] [--branch] [--prompt] [--issue]`:
  spawn a worker session (POST /api/v1/sessions); harness defaults to the
  daemon's AO_AGENT.
- Shared postJSON daemon client (reads the run-file for the port, surfaces
  the API error envelope).

Stacked on #65, which lands the agent-adapter + session-manager wiring
this depends on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `ao spawn` no longer prints a branch the sessions API doesn't return
  (session metadata is json:"-"), so the output is no longer misleading.
- Unregistered/archived/no-path projects now surface a 400
  PROJECT_NOT_RESOLVABLE with an actionable message instead of a generic
  500: a new sessionmanager.ErrProjectNotResolvable sentinel the resolver
  wraps and writeSessionError maps.
- postJSON reuses the injected Deps.HTTPClient (cloned, with a longer
  timeout) instead of a fresh client, keeping HTTP behaviour stubbable.
- postJSON treats a stale run-file (dead PID) as "not running" via
  ProcessAlive, matching its docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile review: harden TestProjectRepoResolver to verify the unregistered
-project error wraps ErrProjectNotResolvable, so a future regression in the
sentinel wrapping (which the HTTP 400 mapping relies on) is caught.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause: the daemon built the zellij runtime with an empty SocketDir,
so zellij fell back to its $TMPDIR-based default (long on macOS). That
left almost none of the ~103-byte unix-socket-path budget for the session
name, so a long session id (e.g. "aoagents-agent-orchestrator-1", derived
from a long project id) was rejected by zellij with "session name must be
less than 0 characters". runtime.Create failed, the spawn 500'd, and the
worktree was rolled back (leaving an orphan ao/ branch).

- New zellij.DefaultSocketDir(): a short, stable per-user socket dir
  (/tmp/ao-zellij-<uid>); the daemon uses it (and MkdirAll's it).
- ao spawn's attach hint now prefixes ZELLIJ_SOCKET_DIR so it stays
  copy-pasteable against the daemon's socket dir.
- Regression test guards that the socket dir leaves >= 48 bytes for the
  session name within the 103-byte limit.

Verified: ao spawn against a long-id project now succeeds (session live,
worktree created) where it previously 500'd.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CLI keeps its own request structs (spawnRequest, addProjectRequest)
separate from the daemon's canonical DTOs (controllers.SpawnSessionRequest,
project.AddInput). Nothing verified the JSON field names agreed, so a renamed
tag on either side would compile but break at runtime.

Drive `ao spawn` and `ao project add` through the real httpd router and
controllers (fakes only at the service layer) over a real loopback round trip
via postJSON, asserting each field decodes into the right SpawnConfig/AddInput
field. Runs in the normal test lane (no extra ports/processes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- spawn: print the sanitised zellij session name (zellij.SessionName) in the
  attach hint; a long/non-conforming session id is registered under a different
  name, so the raw id sent users to a missing session.
- client: surface the daemon error envelope's requestId so a failed command can
  be correlated with daemon logs.
- daemon: don't swallow the zellij socket-dir MkdirAll error — log it, since a
  failure otherwise surfaces later as an opaque socket-bind error on every spawn.
- project: reject an embedded ".." in a project id up front; it passed the id
  pattern but yielded an invalid branch (ao/a..b-1) and an opaque 500 at spawn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@harshitsinghbhandari harshitsinghbhandari merged commit 57bb637 into main Jun 2, 2026
0 of 7 checks passed
harshitsinghbhandari added a commit that referenced this pull request Jun 2, 2026
* feat(messenger): ao send + live zellij pane ping (live agent nudges)

Replace the daemon's noopMessenger stub with a composite AgentMessenger
that writes a durable inbox file (primary) and types a live pointer into
the running zellij pane (best-effort secondary), plus the `ao send` CLI
that drives the existing POST /api/v1/sessions/{id}/send route.

- composite: fans Send to inbox then panep, pinning one timestamp so both
  derive the same filename; a secondary failure is logged at WARN and
  swallowed (the file is on disk), a primary failure aborts the call.
- inbox: writes <workspace>/.ao/inbox/<rfc3339nano>_<hash>.md.
- panep: types "new message at .ao/inbox/<file>" + Enter via a new narrow
  zellij WriteChars seam (RuntimePaneWriter), kept off ports.Runtime.
- wiring: newSessionMessenger composes inbox+panep over the shared store;
  startSession takes the messenger instead of the noop stub.

Carries across @Aa-43's work from PR #74 (staging), adapted to main's
post-#65/#77 daemon wiring shape.

Closes #79

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(inbox): use O_EXCL so a filename collision errors instead of clobbering

os.WriteFile opens with O_CREATE|O_WRONLY|O_TRUNC, which silently overwrites
an existing file. The doc comment already stated the intent ("we do not retry
on EEXIST"), but O_TRUNC never yields EEXIST — two identical messages sent on
the same composite-pinned nanosecond would produce the same filename and the
second Send would silently lose the first message. Switch to
O_CREATE|O_EXCL|O_WRONLY so a collision surfaces as an error; O_EXCL also
refuses to follow a symlink at the final path component. Add a regression test.

Addresses greptile review on PR #83.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(inbox): remove the freshly-created file when write or close fails

The O_EXCL switch creates the inbox file before writing its body; if
WriteString or Close then fails, the empty/partial .md was left on disk and
the agent's next inbox scan would pick up a truncated ghost message. Remove
the file on those error paths. O_EXCL guarantees the file did not exist before
this call, so the cleanup can only delete our own partial write, never a
legitimate earlier message.

Addresses greptile review on PR #83.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(messenger): reduce ao send to live pane delivery

* fix(send): preserve messages and map lookup errors

* fix(send): reject terminated sessions
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.

3 participants