* feat(config): persist per-project agent config and resolve it at spawn
Each project can now carry its own agent config (model, permissions,
adapter-specific keys) that survives daemon restart and is resolved into
the launch command when a session spawns.
- storage: add nullable projects.agent_config JSON column (migration 0008);
marshal/unmarshal in the store so the domain carries map[string]any
- resolution: session manager loads the project row and populates
LaunchConfig.Config before GetLaunchCommand
- validation: claude-code declares a ConfigSpec (model, permissions) and
rejects unknown keys / bad types / bad enums at spawn; it applies the
model override and config-driven permission mode (explicit Permissions
still wins)
- surface: PUT /projects/{id}/agent-config + `ao project set-config`
(--set/--config-json/--clear), config shown in `ao project get`
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(claudecode): validate string-list/required config keys and unhandled types
Address review on per-project agent config validation:
- handle ConfigFieldStringList (list of strings) explicitly
- reject unhandled ConfigFieldType via a default case rather than
silently passing
- enforce Required fields are present
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(config): make per-project agent config a typed struct
Replace the free-form map[string]any agent config with a typed
domain.AgentConfig{Model, Permissions} so values are validated when set
(CLI/API) instead of silently dropped at spawn, and the OpenAPI/TS schema
and UI get real typed fields.
- domain: AgentConfig struct + Validate(); PermissionMode moves to domain
and ports re-exports it as a type alias (zero adapter churn)
- storage: marshal/unmarshal the typed struct (IsZero → SQL NULL)
- service: validate on Add and SetAgentConfig; read-model exposes a typed
*AgentConfig
- claudecode: read typed cfg.Config.Model/.Permissions; drop the
map/spec-based validateConfig in favor of the typed Validate()
- cli: typed `ao project set-config --model/--permission/--clear`
- docs: add docs/design/per-project-config.md blueprint sequencing the
remaining # Projects fields toward fully typed per-project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(config): full typed per-project ProjectConfig (store, resolve, surface)
Expand per-project config from agentConfig-only to the full legacy
`projects.<id>` surface, modeled as one typed domain.ProjectConfig
persisted in a single projects.config JSON column.
Wired end-to-end at spawn:
- defaultBranch → base branch for the session worktree (ports.WorkspaceConfig.BaseBranch)
- env → merged into the runtime env (AO-internal vars still win)
- symlinks → repo files linked into the workspace
- postCreate → commands run in the workspace (OS-agnostic shell)
- agentRules / agentRulesFile / orchestratorRules → merged into the prompt
- worker/orchestrator role overrides → harness + agent-config resolution
Stored + validated + surfaced now, consumption deferred (no consumer yet):
tracker, scm(+webhook), opencodeIssueSessionStrategy; sessionPrefix feeds
the display prefix only (session-id generation unchanged).
Validation lives on domain.ProjectConfig.Validate() and runs when config is
set (CLI/API). PermissionMode/AgentConfig stay typed; harness names validated
via domain.AgentHarness.IsKnown().
Surface: PUT /projects/{id}/config (replaces /agent-config) + typed
`ao project set-config` flags (--default-branch/--env/--symlink/--post-create/
--agent-rules/--worker-agent/… or --config-json). OpenAPI + TS regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(lint): tighten symlink dir perms to 0o750 (gosec G301)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(config): centralize default project config + tests
Add domain.DefaultProjectConfig / ProjectConfig.WithDefaults with a single
DefaultBranchName ("main") source of truth, replacing the literal "main"
scattered in the read-model and the gitworktree adapter. Unconfigured
projects now resolve the default branch through one path; every other field
defaults to its zero value.
Tests: defaults present for all fields (DefaultProjectConfig/WithDefaults),
and an unconfigured project reports the default branch + derived session
prefix while omitting the empty config object.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(config): encode documented defaults (branch=main, tracker=github)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(config): fail-safe paths for missing/corrupt per-project config
Address review on default-config / fail-safe spawning:
- projectRules: a missing AgentRulesFile is optional context, skipped
rather than aborting every spawn (only a real read error surfaces)
- store: a corrupt config JSON column degrades to a zero config instead
of failing GetProject/ListProjects/FindProjectByPath for that row
- restore: re-apply the project's resolved AgentConfig so a configured
model/permissions carry across a restore (matches fresh spawn)
Tests: missing rules file skips, corrupt config degrades to zero, restore
applies the project agent config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(config): trim per-project config to consumer-backed fields
Drop config that has no live consumer yet, so this PR lands only the
fields actually read at spawn/display:
- Remove prompt rules (agentRules, agentRulesFile, orchestratorRules)
from ProjectConfig. Project/agent instructions belong on the system
prompt path or repo-local AGENTS.md, not another rules family.
- Remove future-only integration config with no consumer: tracker, scm,
scm.webhook, and opencodeIssueSessionStrategy (plus their types,
constants, the github tracker default, CLI flags, and spec schemas).
These return in focused PRs alongside the code that reads them.
Kept: defaultBranch, sessionPrefix, env, symlinks, postCreate,
agentConfig (model/permissions), and worker/orchestrator role
overrides. Cross-agent model/permissions support stays follow-up (#157).
Regenerated openapi.yaml + frontend schema.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(config): reject unknown config JSON keys; confine symlink paths
Two review hardenings on the now-trimmed per-project config surface:
- Project add/set-config endpoints decode with DisallowUnknownFields, so
a misspelled or removed config field surfaces as a clear 400 instead
of being silently dropped. Locks the removals from e213b68 (and any
future trims) at the API gate. Covered by new controllers test.
- applySymlinks now refuses absolute paths and any ".." segment via a
safeRelPath guard, so a project config cannot escape the project or
workspace tree via a malicious symlinks entry. Covered by new
session_manager test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(config): reject symlink path traversal at config write time
greptile flagged ProjectConfig.Symlinks as a write-time path-traversal
gap on PR #154 — the runtime guard in applySymlinks catches a malicious
entry on every spawn, but the config itself accepted it. Move the check
into ProjectConfig.Validate so a bad symlinks entry surfaces as
INVALID_PROJECT_CONFIG when set (CLI/API) instead of silently sitting in
the row until the next spawn. The runtime guard stays as
defense-in-depth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
* fix(cdc): emit pr_review_thread_resolved on replace polls (#152 bug 5)
writePRRows was DELETE-then-UPSERT on the Replace path, so every poll's
upserts hit the INSERT branch and the AFTER UPDATE trigger that emits
pr_review_thread_resolved never fired in production. Replaces the
blanket delete with a set-diff: upsert observed threads first (so
unchanged thread_ids go through ON CONFLICT DO UPDATE and fire the
UPDATE trigger when resolved flips), then delete orphans whose
thread_id is not in the observed set, all inside the existing tx.
Adds DeletePRReviewThread query (sqlc-generated form hand-edited; no
sqlc binary available locally — sqlc generate from backend/ produces an
identical file).
Tests: TestPRReviewThreadsCDC_EmitsResolvedOnReplacePoll (regression —
fails without fix) and TestPRReviewThreadsReplace_PrunesOrphansWithoutReinserting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(observe): emit scm-disabled log on startup with no subjects (#152 bug 7)
checkCredentials lived only inside Poll, which short-circuits when
discoverSubjects is empty. On a fresh daemon with no tracked PRs the
documented "scm observer disabled: provider credentials unavailable"
warn was unreachable, leaving users with no signal that the SCM
observer was a no-op.
Calls checkCredentials once in Observer.loop before the first Poll.
The existing credentialsChecked guard preserves once-per-process
semantics; provider construction still uses SkipTokenPreflight so
daemon readiness doesn't block on gh.
Test: TestStart_LogsDisabledWarningWhenNoTokenAndNoSubjects with a
race-safe syncBuffer for capturing slog from the observer goroutine.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(api,spawn): typed errors + project/branch/binary preflight (#152 bugs 1-4,6)
Closes the long tail of opaque-500-and-orphan-row failures that
discussion #149's smoke walk surfaced. The common shape: spawn created
the session row before validating preconditions, and the underlying
errors weren't typed, so toAPIError defaulted to INTERNAL_ERROR.
Bug 1 (orphan row + opaque 500 on unknown projectId):
Service.Spawn / SpawnOrchestrator now call store.GetProject first and
return apierr.NotFound("PROJECT_NOT_FOUND", ...) before manager.Spawn,
eliminating the create-row-then-fail-workspace ordering.
Bug 2 (Restore opaque 500 on half-spawned/terminated session):
Manager.Restore gained the ErrIncompleteHandle guard that Kill has at
manager.go:189-193. toAPIError now maps both restore and kill to the
same SESSION_INCOMPLETE_HANDLE 409 envelope.
Bug 3 (--branch unfetched / checked-out-elsewhere → opaque 500):
gitworktree pre-checks listRecords for branch-in-other-worktree, falls
back to refs/tags on missing local/remote head, and emits two new port
sentinels (ErrWorkspaceBranchCheckedOutElsewhere,
ErrWorkspaceBranchNotFetched) mapped to BRANCH_CHECKED_OUT_ELSEWHERE
(409) and BRANCH_NOT_FETCHED (400).
Bug 4 (orphan terminated row on claim-pr rollback):
Adds Store.DeleteSession gated to seed-state rows only (preserves the
no-resurrection guarantee for live sessions), transactional change_log
cleanup, Manager.RollbackSpawn (delete-then-fallback-to-kill), a new
POST /sessions/{id}/rollback endpoint, and rewires
cli/spawn.rollbackSpawnedSession to use it. The exit-0 sub-symptom was
unreproducible from current source and is left unaddressed.
Bug 6 (agent binary not on PATH → silent idle session):
Drops the "return name, nil" anti-pattern from all 21 agent adapters
and returns the new ports.ErrAgentBinaryNotFound on exec.LookPath miss.
Manager.Spawn gained a validateAgentBinary pre-flight (with injectable
LookPath so tests don't need real binaries on PATH) that aborts before
runtime.Create. Mapped to AGENT_BINARY_NOT_FOUND (400). Integration
tests in internal/integration/ stub LookPath to /usr/bin/true.
Tests cover each bug end-to-end. OpenAPI regenerated for /rollback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: gofmt + regen frontend schema.ts for /rollback
CI fixes for #153:
- gofmt/goimports on kilocode and kiro adapters that the bug 6 audit
left mis-grouped.
- openapi-typescript regen against the new /rollback endpoint added in
the Lane A commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(store): guard change_log delete behind seed probe + regen sqlc (#152, PR #153 review)
Addresses @greptile-apps P1 and P2 review feedback on PR #153.
P1 (CDC events deleted for live sessions in rollback fallback):
DeleteChangeLogForSession ran unconditionally inside the transaction
before DeleteSeedSession's seed-state predicates filtered the session
delete to a no-op. For a live session reaching DeleteSession (the
delete-then-kill fallback path inside RollbackSpawn), the seed delete
returned 0 rows but the session_created/session_updated CDC events
had already been purged. Now probes via a new SessionIsSeed query
first and short-circuits the whole tx — including the change_log
cleanup — when the row is not in seed state.
P2 (regen sqlc): installed sqlc 1.31.1 and ran `sqlc generate` from
backend/, replacing the hand-edited pr_review_threads.sql.go (and
producing minor format-only churn in models.go, pr.sql.go,
sessions.sql.go, changelog.sql.go).
The regen surfaced two issues:
1. GetPR / ListPRsBySession had their return types hand-changed to
gen.PR by the previous PR; sqlc actually emits GetPRRow /
ListPRsBySessionRow when queries enumerate columns. Fixed by
collapsing those two queries to `SELECT * FROM pr` so sqlc returns
gen.PR (which is what the store's prRowFromGen converter expects),
and pr.last_nudge_signature now lands in the result alongside the
existing 37 columns.
2. sqlc 1.31.1's SQLite parser silently strips trailing `?`
placeholders and string literals from DELETE statements (reproduced
with sqlc.arg, IFNULL, rowid subquery, and second-predicate
workarounds — all eaten). DeleteSeedSession and
DeleteChangeLogForSession both tripped it. They are now run as
plain tx.ExecContext calls inside Store.DeleteSession, inside the
same write transaction as SessionIsSeed; both queries are removed
from the queries/ directory and the workaround context is
documented inline in queries/sessions.sql and queries/changelog.sql
to keep future contributors from re-adding them.
Verified: go build ./... clean, go test -race ./... 1097/1097 pass.
* refactor(backend): LLD maintainability fixes in controllers/service layers
Addresses the high + medium severity findings from the LLD review of
backend/internal/httpd and backend/internal/service (#95):
1. Controllers no longer import internal/session_manager. Session sentinel
errors are now *domain.ServiceError values carrying their own HTTP mapping,
so the controller translates them with one generic errors.As — no
cross-package sentinel imports.
2. One error pattern across services: project.Error is now an alias of the
shared domain.ServiceError, and session_manager sentinels use it too. A
single writeServiceError replaces the per-resource error switches.
3. Clean-orchestrator business logic moved out of the controller into
session.Service.SpawnOrchestrator(ctx, projectID, clean).
4. isGitRepo no longer treats case-different paths as equal on case-sensitive
filesystems; case-insensitive compare is gated to darwin/windows via samePath.
5. Project repo check sits behind an injectable GitChecker, so the service is
testable without a real git binary.
6. httpd exports only the production constructors (NewWithDeps,
NewRouterWithControl); the 3 test-only wrappers are removed and the
"router with empty deps" convenience moved to an unexported test helper.
Closes#95
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(backend): standardize service errors on internal/httpd/errors
Replace the domain.ServiceError approach with a REST-API-scoped error package
and a single envelope renderer, per review feedback:
- Add internal/httpd/errors (package errors, aliased apierr): one structured
Error type with semantic Kinds (Internal/Invalid/NotFound/Conflict) and
constructors. Imports nothing, so any layer can depend on it.
- envelope.WriteError is now the single path from a service error to the wire
APIError, and the only place a Kind becomes an HTTP status/word. The
per-resource writeProjectError/writeSessionError translators are gone.
- Delete domain/errors.go (keeps domain pure of HTTP-flavored kinds) and
service/project/errors.go (no per-service error files); services build
errors inline via apierr constructors.
- session_manager sentinels are apierr.Error values (pointer identity still
works with errors.Is).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* revert(backend): drop GitChecker seam and isGitRepo case-sensitivity change
Defer findings #4 (isGitRepo case-sensitivity) and #5 (GitChecker seam) out
of this PR. Restores the original exec-based isGitRepo and the New(store)
constructor; removes git.go, git_test.go, and the test-only export shims. The
error-standardization and other findings are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(session): translate engine errors to API errors at the facade
The session_manager is the internal command engine and must not depend on the
REST API error vocabulary. Revert its sentinels to plain errors.New values and
move the engine→API translation into the service/session facade (toAPIError),
which is the correct boundary. Controllers still see apierr.Error and never
import the engine; the engine no longer imports internal/httpd/errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(session): tighten error comments to state what the code does
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(envelope): make KindInternal an explicit case in httpStatus
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(apierr): rename package, test SpawnOrchestrator, parity fixes
Address review feedback on PR #96:
- Rename internal/httpd/errors → internal/httpd/apierr (package apierr) so
importers no longer alias around the stdlib errors package.
- Add a commander seam to session.Service and unit-test the relocated
clean-orchestrator rule: clean=true kills all active orchestrators before
spawning; clean=false spawns without kills.
- project.Add: wrap the UpsertProject store error in apierr.Internal for parity
with its sibling paths (was a raw 500).
- Document that KindInternal is iota's zero value, so a zero-value Error
defaults to 500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 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
* Add `ao spawn` and `ao project add`; resolve project repos for worktrees
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>
* Address Copilot review on #77
- `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>
* Assert the project-not-resolvable sentinel in the resolver test
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>
* Fix ao spawn 500 on long session ids (zellij socket-path overflow)
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>
* test(cli): guard CLI/daemon DTO drift with an e2e round-trip
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>
* fix(cli,daemon): address review findings on ao spawn
- 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>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
* feat(plugin): add agents plugin (first iteration)
Faithful copy of the agents plugin implementation from yyovil/better-ao
(internal/plugin/ -> backend/internal/plugin/) plus its PRD
(prds/plugins/agents/PRD.md), as a first-iteration proposal for review.
Imports are left at their original github.com/yyovil/better-ao/... paths and
are NOT yet reconciled to this repo's module; see PR description for the
integration deltas (module path, missing internal/utils dependency).
Co-authored-by: Claude <noreply@anthropic.com>
* Move agent adapters under backend adapters
* Keep daemon ports and session out of adapter move
* Remove Better-AO naming from flake
* Keep flake as dev shell only
* Use goimports for local formatting
* Wire session manager to per-session agent adapters
Move the Agent port into internal/ports and have the claude-code and
codex adapters implement it directly, alongside their workspace-local
activity hooks and a manifest-keyed adapter registry. Rename
RuntimeConfig.LaunchCommand to Argv and update the tmux and zellij
runtimes to match.
The session Manager now resolves a real agent adapter per session via a
new ports.AgentResolver: from cfg.Harness on Spawn and the stored harness
on Restore, so one daemon runs claude-code and codex sessions side by
side. The daemon backs the resolver with the registry; AO_AGENT selects
the default harness (default claude-code), validated at startup. Removes
the temporary noopAgent stub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(agent): point the agent contract at internal/ports/agent.go
The Agent interface moved from internal/adapters/agent to internal/ports;
update the PRD's Goal and Agent Contract sections (and the SessionInfo
references) to match the code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Wire the session service into the daemon
daemon.Run now builds the controller-facing session service — a session
manager over the zellij runtime, a gitworktree workspace, the shared
store + LCM, and the per-session agent resolver (AO_AGENT default,
validated at startup) — and mounts it at httpd APIDeps.Sessions, so the
session REST routes are backed by a real service. startLifecycle moves
ahead of the HTTP server so both share one LCM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address Greptile review: complete the live spawn path
- Spawn and Restore now install workspace-local activity hooks
(GetAgentHooks) and run the adapter's optional PreLaunch step before
launch, via a shared prepareWorkspace helper. PreLaunch is how Claude
Code records workspace trust, so its interactive "trust this folder?"
dialog can't hang the headless pane; the spawned env now also carries
AO_DATA_DIR so the installed hook commands find the store.
- claudecode and codex hook/config writes are now atomic (temp + rename)
instead of os.WriteFile, so a crash mid-write can't leave a partial
file the agent fails to parse.
- ensureWorkspaceTrusted serializes its read-modify-write under a package
mutex, so concurrent spawns to different workspaces don't drop each
other's ~/.claude.json trust entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ports): pin MetadataKeyAgentSessionID to domain.SessionMetadata json tag
The equality between ports.MetadataKeyAgentSessionID and the json tag on
domain.SessionMetadata.AgentSessionID is a hand-maintained invariant; this
test fails loudly if either side drifts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(adapters): use ports.MetadataKeyAgentSessionID in claudecode + codex
The native session id metadata key is defined in ports for cross-package
consumption; drop the duplicated literals in each adapter so the constant
has one home.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(codex): cover ensureCodexHooksFeatureEnabled TOML edge cases
The helper is a string editor over config.toml; pin its content
transformation for missing/empty files, existing [features] blocks,
the no-op case, and the legacy codex_hooks=true migration paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(adapters): document Registry concurrency contract
Registry registration runs at daemon boot before any goroutine calls Get,
so the underlying map needs no lock; pin that contract in the doc comment
so a future change doesn't quietly introduce a race.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* style(codex): gofmt codex_test.go after constant rename
The previous commit (7c5b2a9) replaced codexAgentSessionIDMetadataKey with
ports.MetadataKeyAgentSessionID inside a map literal; the longer key threw
off gofmt's column alignment on the adjacent codexTitleMetadataKey /
codexSummaryMetadataKey lines. Caught by agent-ci's Check formatting step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <dev@theharshitsingh.com>