Commit Graph

209 Commits

Author SHA1 Message Date
swyam sharma 891fb79e79
fix(lifecycle): sanitize CI logs and reviewer comments before PTY paste (#323) 2026-06-20 10:27:52 +05:30
Vaibhaav Tiwari 7037f4aa4e
fix: surface missing reviewer harness (#318)
* fix: surface missing reviewer harness

* fix: satisfy review lint

---------

Co-authored-by: Vaibhaav <user@example.com>
2026-06-20 10:19:50 +05:30
Khushi Diwan 7d0ca02440
fix(project): resolve default branch from origin/HEAD, not checked-out branch (#289)
* fix(project): resolve default branch from origin/HEAD, not checked-out branch

Detecting the project default via `symbolic-ref --short HEAD` captured
whatever branch the repo happened to be on at add time. Adding a project
while on a feature branch (e.g. fix/pr-attachment) persisted that branch
as the default, so every session worktree based off it instead of main.

Prefer the remote default (origin/HEAD), falling back to the checked-out
branch only when no remote default is set. This still records a non-main
default like master correctly, while ignoring the active feature branch.

* test: cover branch-not-fetched API error
2026-06-19 19:50:19 +05:30
Laxman 43ee6c9b02
Feat/backend telemetry v0 (#307)
* feat(backend): add telemetry event plumbing

* feat(backend): emit session telemetry events

* feat(backend): add http and cli telemetry export

* feat(backend): add onboarding and dwell telemetry

* feat(frontend): add renderer posthog telemetry

* feat(frontend): bundle posthog project defaults

* feat(telemetry): add canonical active-user event

* fix(telemetry): repair cli test expectations

* fix(telemetry): sanitize remote payloads and respect event toggles
2026-06-18 22:00:25 +05:30
Madhav Kumar 6885af26b2
feat(windows): ConPTY terminal, zellij discovery, agent launcher trampoline, codex shim resolution (#310)
* fix(daemon): self-heal a stale run-file instead of refusing to start

On Windows the desktop supervisor can only TerminateProcess the daemon
(no POSIX signal reaches a detached child), so the daemon's graceful
shutdown never runs and ~/.ao/running.json is never removed. The leaked
file survives into the next launch, and because Windows reuses PIDs
aggressively the recorded PID usually belongs to an unrelated process.
The startup pre-flight trusted PID liveness alone (runfile.CheckStale ->
processalive.Alive), so it concluded a daemon was "already running" and
exited with "refusing to start" on every restart. A dead daemon then
makes the renderer's loopback REST calls (e.g. Spawn Orchestrator) fail
silently.

Verify the recorded port is actually served by an AO daemon with the
recorded PID (a /healthz probe matching service + pid, the same ground
truth inspectDaemon already uses) before refusing. A run-file left by a
crashed, hard-killed, or reused-PID predecessor is treated as stale and
overwritten, so startup is robust to a leaked run-file from any cause.

Fixes #256

* fix(release): build the desktop daemon natively on each target OS

build-daemon.mjs compiles the bundled `ao` daemon with the build host's
GOOS and names it off the host platform (ao.exe only when the builder is
Windows). The release workflow ran only on macos-latest, so a Windows
package would ship a macOS binary named `ao` with no `ao.exe`, and the
app could not launch a valid Windows daemon ("This program cannot be run
in DOS mode" / binary not found).

Run the release as a per-OS matrix (macOS + Windows) so host == target
and each installer bundles a daemon compiled for its own platform, and
pin the Go toolchain with setup-go since build-daemon needs it on every
runner.

Fixes #235

* feat(terminal): Windows ConPTY support for /mux attach

Replaces the Windows stub in internal/terminal/pty_windows.go with a real ConPTY implementation backed by github.com/aymanbagabas/go-pty, so the daemon's /mux attach can stream a live terminal to the renderer on Windows.

PTYSource.AttachCommand now returns (argv, env, err). On Windows the zellij attach is spawned directly (no powershell.exe wrapper) — wrapping ConPTY startup around a shell surfaces as modal application-error dialogs — and the per-session ZELLIJ_SOCKET_DIR is delivered via the spawn's CreateProcess env block instead of an 'env -u NO_COLOR' shim. Unix continues to use the env-shim wrapper and returns nil env.

Adds go-pty v0.2.3 (+ bumps golang.org/x/sys to v0.44.0 transitively). Updates the in-process test fakes (terminal/fakes_test.go, httpd/terminal_mux_test.go) for the new signature.

* feat(zellij): discover zellij binary on Windows and raise command timeout

Defaults the zellij binary to whatever exec.LookPath finds first (preferring zellij.exe on Windows), falling back to LOCALAPPDATA\Programs\zellij\zellij.exe and ProgramFiles{,(x86)}\{zellij,Zellij}\zellij.exe so a fresh-installed Windows user gets a working runtime without setting Options.Binary.

Raises the per-command timeout from 5s to 30s on Windows: the first zellij invocation after install routinely takes longer than 5s on Windows due to filesystem/AV warmup, which was causing benign DeadlineExceeded failures during session create.

* feat(zellij,cli): Windows agent launcher trampoline for codex argv

On Windows, zellij's KDL `args` quoting cannot round-trip codex's --config key=value flags (or any argv with embedded quotes), and shell-wrapping the agent in powershell/cmd quoting is equally unsound. This adds a small launch trampoline so zellij runs a known-fixed argv and the real argv is delivered out-of-band.

How it works on Windows:

1. zellij.Runtime.writeLayout persists cfg.Argv to a temp JSON spec via the new agentlaunch package (AO_LAUNCH_SPEC env var points at the file).

2. The KDL layout runs the trampoline as `<ao.exe> launch` (windowsLaunchArgv); PATH is augmented so the trampoline resolves.

3. The new hidden `ao launch` subcommand reads the spec, deletes the temp file, and execs the real agent with cfg.Argv inside cfg.WorkspacePath.

Also adds:

- runner.Start fire-and-forget path (process_windows.go uses powershell.exe -EncodedCommand + Start-Process -WindowStyle Hidden with CREATE_NEW_CONSOLE so the daemon is not blocked on zellij's --create-background settling).

- powerShellEncodedCommand helper and switch from -Command to -EncodedCommand for the existing powershell shellLaunchSpec (avoids brittle KDL→PowerShell quoting round-trips).

Unix is unchanged: writeLayout passes cfg.Env straight through, createSession stays synchronous via runner.Run, and process_other.go is a stub that returns an error if anyone calls into the background path.

* feat(codex): Windows binary resolution, terminal compat flags, TOML literal strings

Three Windows-targeted refinements to the codex agent plugin so a default Windows install lands in a working state:

1. ResolveCodexBinary now follows .cmd/.ps1 shims to the underlying codex.exe (resolveNativeWindowsCodex + windowsNativeCodexCandidatesForShim). The npm-distributed codex shim cannot be exec'd directly under ConPTY without a shell wrapper; jumping straight to the .exe avoids that wrapper.

2. appendTerminalCompatibilityFlags adds Windows-specific args (e.g. --no-alt-screen) so codex's TUI renders correctly inside zellij's pane without the alternate-screen buffer churn that breaks ConPTY redraws.

3. hooks.go gains codexTOMLLiteralString / codexTOMLConfigString / containsTOMLControl so paths and other values with backslashes and quotes round-trip through codex's --config TOML parser using literal strings ('...') when basic strings would require unsafe escaping.

* fix(lint): paramTypeCombine in pty_unix.go, revive doc comments in agentlaunch, codex test quotes

* fix: stabilize windows zellij sessions

---------

Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
Co-authored-by: Madhav <madhavkumar@microsoft.com>
Co-authored-by: Vaibhaav <user@example.com>
2026-06-18 21:51:05 +05:30
neversettle 9ae9f08887
fix(session): make worker branch namespace child-safe (#309)
* fix(session): make worker branch namespace child-safe

* test(cli): wait for daemon exit after shutdown
2026-06-18 20:00:15 +05:30
Harshit Singh Bhandari 5fff98087a
feat(import): rewrite-side legacy → rewrite first-boot import (#314)
* feat(import): rewrite-side legacy → rewrite first-boot import

Port the legacy-side TS reader (AgentWrapper #2144/#2129) to Go and run the
migration inside the rewrite as an opt-in import, per the FINAL v2 plan. Reads
the legacy flat-file store (~/.agent-orchestrator) read-only and writes the
rewrite's own SQLite DB via the native storage layer; legacy files are never
touched, and a re-run skips existing rows, so a declined or failed import loses
nothing.

What's included:
- internal/legacyimport: Go reader + field mappers (issue #247). Lifecycle
  double-decode (lifecycle key or statePayload+stateVersion:"2"),
  role/orchestrator detection, sessionPrefix fallback (first 12 chars of id),
  8→4 activity-state map, per-harness resume-id selection, permission/harness
  remap, and the claude transcript slug + relocation to the rewrite's
  orchestrator worktree path ({DataDir}/worktrees/{id}/orchestrator/{prefix}-orchestrator).
- store.ImportSession: verbatim session insert (explicit id/num, ON CONFLICT
  DO NOTHING) so the orchestrator lands at id "{prefix}-orchestrator", num 0.
- `ao import`: explicit, idempotent import with --from/--dry-run/--yes/--json.
  Refuses while a live daemon owns the run-file (the daemon is sole writer; the
  import runs offline, matching the #2129 reference).
- First-boot opt-in: `ao start` offers the import before launching the daemon
  when legacy data is present and the rewrite DB has no projects yet. Declining
  or any failure is non-fatal; a non-interactive boot prints a hint instead of
  auto-importing.

Scope (gist §6): all projects + per-project settings, and the single
non-terminated orchestrator session per project (claude-code/codex/opencode;
aider skipped with a note). Workers are not imported (they respawn fresh).

Resume-id mapping (#247 §2.2): agent_session_id carries claudeSessionUuid /
codexThreadId / opencodeSessionId by harness. codexModel and
restoreFallbackReason have no rewrite column, so they are dropped and surfaced
as import notes — codex resumes from the thread id alone, the rest is forensic.

Gate: `go build ./... && go test -race ./...` green (1423 tests).

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

* fix(import): resolve golangci-lint errcheck/gocritic/nilerr findings

- start.go: check fmt.Fprint* returns in the first-boot import path
- project.go: combine same-typed return params (gocritic paramTypeCombine)
- claude.go: use a pathExists helper so a missing transcript source is a normal
  skip, not an err-then-return-nil (nilerr)
- importer.go: fold best-effort transcript relocation into a switch so the
  non-fatal path no longer returns nil from an error branch (nilerr)

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

* fix(import): resolve transcript dest path like the daemon; harden lifecycle parse

Code-review follow-ups on the legacy importer:

- claude.go: compute the Claude transcript DESTINATION slug from the
  symlink-resolved orchestrator worktree path (new resolvePhysical, mirroring
  gitworktree.physicalAbs), not the literal path. The daemon resolves that cwd
  through physicalAbs before `claude --resume` runs, so a literal-path slug
  missed the resume bucket whenever any component of AO_DATA_DIR was a symlink
  (custom data dir, macOS /tmp→/private/tmp, symlinked $HOME) — the orchestrator
  would have resumed without its prior context. Source slug now uses the same
  resolver for symmetry.
- orchestrator.go: accept a numeric stateVersion (JSON 2 → float64) as well as
  the string "2" when falling back to statePayload, so a V2 record carried only
  in statePayload is not misparsed as stateless.
- orchestrator.go: build the dropped-resume-metadata note as a joined list
  instead of string concatenation.

Tests: added a symlinked-data-dir dest-slug test and a numeric-stateVersion
fallback test. Gate green: `go build ./... && go test -race ./...` (1425).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 19:54:51 +05:30
Pritom Mazumdar 3986d488c8
feat(session): support multiple PRs per session (#230)
* feat(session): support multiple PRs per session

A session can now own several pull requests (a root plus stacked
children) instead of being capped at one. The SQLite schema was already
1-session->many-PR (pr.url PK, session_id a plain FK), so this is a
behavioural change across the observe -> persist -> derive -> react
pipeline, not a migration.

- observe: the SCM observer discovers every open PR whose source branch
  matches a session branch or descends from it ("branch/..." stacking),
  attributing each to the owning session; the longest matching branch
  wins so a child session claims its own stacked PRs.
- derive: session status is a worst-wins aggregate over all owned PRs,
  with a stack model (B is a child of A iff B.target == A.source and A is
  open) exposed via prs[] on every session read DTO.
- react: per-PR reactions; a stacked child blocked by an open parent is
  exempt from the rebase/merge-conflict nudge (only the bottom of the
  stack is eligible), and the session completes only when no PR is open
  and at least one merged.
- tests: unit coverage across stack/status/observer/lifecycle, a
  real-SQLite ListPRFactsForSession test for the stacked-PR read path,
  and a functional end-to-end integration test driving the real store +
  lifecycle + observer through attribution, completion, and stacked-child
  nudge suppression.

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

* fix(scm): ignore fork heads in PR attribution and persist discovered siblings before completion

Branch-prefix attribution now requires a discovered PR's head branch to live
in the project repo. A fork PR can reuse a session's branch name while its
commits live in the fork, so the previous code could auto-claim foreign work.
Carry head repo full_name from the REST list response and skip any PR whose
head repo is not the base repo.

discoverNewPRs also writes each newly discovered PR as an open baseline row
before the refresh/lifecycle pass runs. A session can own several PRs, and a
terminal observation triggers a completion check that reads all of the
session's PRs from the store. Without the early write, an open sibling found
in the same poll was not yet durable and the session could terminate while
that PR was still open.

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

* fix(session): surface actionable signals from blocked stacked children; clarify worker prompt

Status aggregation previously dropped any open PR blocked by an open parent,
hiding actionable child signals (failing CI, draft, requested changes,
unresolved comments) behind the parent's status. A blocked child still cannot
merge, so its readiness signals (mergeable/approved/review-pending/open) stay
suppressed, but its problem signals now contribute to the worst-wins aggregate.
The all-blocked fallback is preserved so a session never goes dark.

The worker multi-PR prompt said independent PRs could branch off the base
branch as usual, which conflicts with branch-prefix attribution. Clarify that
a PR may target the base branch, but its source branch must stay under the
session branch namespace for AO to track it.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 17:31:28 +05:30
neversettle eeaddd221a
fix(review): reviewer posts to GitHub and records its verdict autonomously (#259)
* fix(review): make the reviewer post to GitHub and record its verdict autonomously

The claude-code reviewer never completed a review on its own. Three defects
in the reviewer launch + flow:

- It launched with no permission mode, so a headless pane stalled on the
  first tool-permission prompt and never ran gh/ao. Launch with
  bypassPermissions (read-only is enforced by the prompt, not a sandbox).
- The reviewer pane got no pinned PATH, so `ao review submit` resolved to a
  foreign `ao` on the inherited PATH and failed. Pin PATH to the daemon's
  own dir the same way worker sessions do — export HookPATH and reuse it in
  the launcher.
- The prompt did not enforce ordering. Make it post the review on the PR
  via gh first, then run `ao review submit`.

Fixes #258

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

* fix(review): fall back to a comment review when self-approval is rejected

GitHub does not let an author approve their own PR, so a reviewer running
under the same account can't post an `approve`. Tell the reviewer to post
the approval as a regular comment review (COMMENT event stating it is an
approval) when the provider rejects the self-approval, instead of failing.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:34:15 +05:30
Khushi Diwan 96d1649e46
fix(review): serialize concurrent triggers per worker to stop reviewer double-spawn (#246)
* fix(review): serialize concurrent triggers per worker to stop double-spawn

Engine.Trigger was a read-then-write (idempotency check -> reviewer spawn ->
InsertReviewRun) with no serialization and no backing constraint. Two near-
simultaneous triggers for the same worker at the same head SHA both passed the
GetReviewRunBySessionAndSHA check, both spawned a reviewer against the same
deterministic review-<id> handle, and both inserted a running run for one commit.

Add a per-worker keyed mutex (lockWorker) held across the whole Trigger body, so
the loser re-reads the freshly-recorded run and short-circuits to Created:false
instead of spawning. Back it with a partial unique index on
review_run(session_id, target_sha) (migration 0013) as a cross-restart safety
net; rows with an empty target_sha (head not yet observed) are excluded so they
are not blocked.

Adds a concurrency test asserting N simultaneous triggers spawn once and record
one run.

Closes #242

* fix(review): make migration 0013 dedup-safe and handle the unique conflict in Trigger

Pre-#242 daemons can already hold duplicate (session_id, target_sha)
review_run rows, on which CREATE UNIQUE INDEX fails and wedges startup.
Migration 0013 now collapses each duplicate group to a single survivor
(a completed pass over a still-running one, then newest by created_at)
before building the index.

Trigger now treats a unique-constraint hit as a fallback rather than an
error: InsertReviewRun maps it to the new domain.ErrDuplicateReviewRun
sentinel, and Trigger re-reads GetReviewRunBySessionAndSHA and returns
that run with Created:false instead of surfacing a raw error after the
reviewer may already have launched.
2026-06-17 00:43:10 +05:30
Khushi Diwan 198b79757c
fix(session): stamp session timestamps in UTC (#215)
Two clocks defaulted to local time.Now while the rest of the codebase writes
UTC, so ao session get showed created and updated in different timezones:

- session manager clock → spawn-stamped CreatedAt/UpdatedAt
- lifecycle manager clock → activity-driven LastActivityAt/UpdatedAt

A real spawn made this visible: createdAt came back UTC but updatedAt/
lastActivityAt were local once the agent reported activity. Default both clocks
to UTC.

Closes #214
2026-06-15 14:51:12 +05:30
Khushi Diwan d81d76280d
fix(project): detect the repo's default branch on add (#209)
Register the repo's actual checked-out branch as the project default so
session worktrees base off a ref that exists. Previously Config.DefaultBranch
was left empty and defaulted to "main", so a repo on master/develop/trunk
failed every spawn with BRANCH_NOT_FETCHED and had no CLI workaround.

Detection is best-effort (symbolic-ref --short HEAD); a detached HEAD or git
error falls back to the existing main default. Only persist when the branch
diverges from main, so the common main repo keeps a NULL config.

Closes #208
2026-06-15 14:51:07 +05:30
Khushi Diwan 9e84a3b5ae
fix(spawn): map invalid branch name to 400 instead of opaque 500 (#213)
validateBranch returned an untyped error for a name rejected by
git check-ref-format, so toAPIError fell through to INTERNAL_ERROR 500.

Add a ports.ErrWorkspaceBranchInvalid sentinel (mirroring the not-fetched /
checked-out-elsewhere ones), wrap it in validateBranch, and map it to
INVALID_BRANCH (400). Completes the residual of #152 Bug 3, which typed the
not-fetched and checked-out-elsewhere cases but left the invalid-format case
collapsing to 500.

Closes #212
2026-06-15 14:50:58 +05:30
Khushi Diwan cbf3f0a2db
fix(spawn): reject unknown harness with 400 instead of opaque 500 (#211)
An unknown --harness was only caught at the agent-registry lookup deep in
Spawn, after the seed row and worktree were already created: the untyped
error collapsed to INTERNAL_ERROR 500 and left a terminated orphan row.

Validate the harness against the registry before any durable state is
created and return a typed ErrUnknownHarness mapped to UNKNOWN_HARNESS (400).
Sibling to #152 Bug 6 (unknown binary on PATH), which did not cover a harness
with no registered adapter.

Closes #210
2026-06-15 14:50:53 +05:30
neversettle da30da5a45
feat(review): configurable AO code review backend (V1) (#192) (#197)
* feat(review): configurable AO code review backend (V1)

Add per-project configurable code review of a worker's PR. A reviewer
agent runs one-shot over the worker's own worktree and posts its result
to the PR; the worker picks the feedback up through the existing SCM
observer review-nudge path.

- domain: ProjectConfig.reviewers (+ default reviewer harness), Review /
  ReviewRun types and verdict/status vocab.
- storage: review + review_run tables (0011), sqlc queries, store methods.
- service/review: rewrite the in-memory stub as a persisted ReviewService
  (Trigger/Submit/List) with a reviewer Runner over agent resolver +
  runtime; ports.PRReviewPoster implemented on the GitHub adapter.
- http: session-scoped routes POST /sessions/{id}/reviews/trigger,
  POST .../submit, GET .../reviews; regenerated OpenAPI + TS types.
- cli: ao review trigger|submit|list.
- frontend: adapt ReviewDashboard to the per-worker reviews API.

Closes #192

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

* refactor(review): address review — drop submit/poster/CLI, default reviewer to worker harness

Per PR #197 review feedback:
- Reviewer agent posts its review to the PR itself, so remove the
  ports.PRReviewPoster port, the GitHub review poster, the submit HTTP
  route + DTO, and the service Submit method (#1, #4, #7).
- Trigger spawns the reviewer agent over the worker's worktree with its
  own review prompt, mirroring the session launch flow (resolve agent by
  harness -> argv -> runtime.Create) (#8, #9).
- Default reviewer harness reuses the worker's harness when supported,
  falling back to claude-code; reviewer config stays independent of the
  worker override (#5, #6).
- Drop the `ao review` CLI for this PR's scope (#2, #3).

Regenerated OpenAPI + TS types.

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

* feat(review): restore ao review submit (records verdict+body in AO)

Per maintainer request, bring back `ao review submit`. AO records the
reviewer's verdict and body on the review_run and marks the pass complete;
it does not post to GitHub — the reviewer agent posts its review to the PR
itself.

- storage: add review_run.body (0011), persist via Insert/UpdateReviewRunResult.
- service: restore Submit (no SCM poster) storing verdict + body.
- http: restore POST /sessions/{id}/reviews/submit + SubmitReviewInput.
- cli: ao review submit [worker] --verdict --body (worker from arg/--session/$AO_REVIEW_WORKER).
- runner: reviewer prompt instructs posting to GitHub and recording via ao review submit.

Regenerated OpenAPI + TS types.

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

* refactor(review): move reviewer runner to its own package; sharpen prompt

Per PR #197 review:
- Move the concrete reviewer runner out of the service layer into a new
  internal/review_runner package (package reviewrunner), beside other
  orchestration packages like session_manager. The service keeps only the
  Runner interface + RunSpec it depends on; the agent-resolver + runtime
  launch flow lives in review_runner.
- Sharpen the reviewer prompt: tell the agent to diff against the PR base,
  focus on high-confidence findings, post via `gh pr review`, and record
  the result with `ao review submit`; review-only (no commits/edits).
- Add unit tests for the runner.

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

* refactor(review): simplify review_run schema; provider-agnostic reviewer prompt

Per PR #197 review:
- review_run: status default 'running' (drop 'pending'), drop CHECK
  constraints on status/verdict, drop the updated_at column and the
  session/iteration index. Propagated through queries, domain, store,
  service, and tests.
- Reviewer prompt no longer hardcodes GitHub/gh commands — it instructs the
  agent to use whatever review tooling the provider offers, keeping the
  flow extensible across SCM providers.

Regenerated sqlc + OpenAPI/TS.

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

* refactor(review): launch reviewer before persisting the run

Trigger now spawns the reviewer agent first and then writes the review_run
with a status derived from the launch outcome (running on success, failed
if it never started), instead of inserting a running row and correcting it
to failed afterwards.

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

* refactor(review): pluggable reviewer registry distinct from worker harnesses

Reviewers are now their own pluggable adapter set, separate from the worker
agent registry — adding a reviewer (claude-code today, greptile tomorrow) is
a one-line registration that does not widen the worker harness vocabulary,
and a worker harness does not automatically become a valid reviewer.

- domain.ReviewerHarness: a distinct vocabulary (AllReviewerHarnesses) with
  its own IsKnown; ReviewerConfig/Review/ReviewRun use it. ResolveReviewerHarness
  reuses the worker harness only when it is itself a supported reviewer, else
  falls back to claude-code.
- ports.Reviewer: a reviewer-specific contract (ReviewCommand → argv + env)
  that models one-shot / non-prompt CLIs natively instead of forcing every
  reviewer through the worker's interactive GetLaunchCommand(Prompt:...).
- internal/adapters/reviewer: a separate registry + resolver (mirrors the
  worker agent registry) with the claude-code reviewer adapter, which owns the
  review prompt and reuses the worker claude-code launch construction.
- review_runner resolves via the reviewer registry (not the worker
  AgentResolver) and merges AO_REVIEW_WORKER into the adapter's env.
- daemon wires the reviewer resolver. Registry/domain parity is test-enforced.

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

* test(review): cover run-scoped reviewer submit

* fix(api): update generated review submit schema

* refactor(review): split core engine (internal/review) from API service

Move the review orchestration (Trigger/Submit/List, run-id generation,
deps, RunSpec/Runner, sentinels) into a transport-independent core package
internal/review (Engine). internal/service/review is now a thin API-flow
boundary: the controller-facing Manager interface + a Service that delegates
to the engine + error re-exports.

This keeps the service layer to API concerns and lets the same engine back a
future in-process CLI trigger without going through HTTP. review_runner now
depends on the core package; daemon builds the engine and wraps it in the
service. No API/schema changes.

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

* feat(review): commit-aware trigger, reviewer handle for UI, no env vars

Reworks the review trigger lifecycle and drops env-based coupling:

- review_run gains target_sha (the reviewed commit) and drops iteration.
  A repeat trigger for the same PR head short-circuits to the existing run.
- review gains reviewer_handle_id: the live reviewer pane's runtime handle,
  reused across passes and exposed in the reviews API so the UI can attach
  its terminal over /mux.
- Trigger flow: if a live reviewer pane exists and a new commit arrived,
  message it to re-review; otherwise spawn a fresh reviewer. The run is
  recorded only after the reviewer is launched.
- No environment variables: the reviewer adapter embeds the explicit
  `ao review submit --session <w> --run <id>` command in the spawn prompt
  and the re-review message. CLI submit requires --run/--session (no env
  fallbacks).
- Merge review_runner into internal/review as a Launcher (spawn/notify/alive).
- Trigger returns 201 for a new pass, 200 when reusing an existing run.

Regenerated sqlc + OpenAPI/TS.

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

* refactor(review): author the reviewer prompt centrally, not in the adapter

Mirror the worker model (session_manager builds the prompt; adapters just
place it via LaunchConfig.Prompt). The reviewer prompt now lives in
internal/review/prompt.go and is passed through ports.ReviewInvocation.Prompt;
the claude-code reviewer adapter just feeds inv.Prompt to its launch command
and returns it as the re-review message. One-shot CLI reviewers may ignore it.

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

* refactor(review): split reviewer prompt into system+task, mirroring buildSpawnTexts

Mirror session_manager.buildSpawnTexts for the reviewer: a standing role goes
in the system prompt, the per-pass task (PR/commit + exact `ao review submit`
command) goes in the user prompt. internal/review/prompt.go now returns
(prompt, systemPrompt); both flow through ports.ReviewInvocation and the
claude-code adapter places them via LaunchConfig{Prompt, SystemPrompt}. The
re-review message reuses the per-pass prompt (role already established in the
running pane).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-15 01:17:17 +05:30
Khushi Diwan a197ff6d88
fix(spawn): persist the resolved default agent on the session (#221)
A spawn with no explicit harness ran the daemon default (claude-code) but
stored an empty harness: effectiveHarness returned "", seedRecord persisted it,
and the empty->default resolution lived only inside agentRegistry.Agent. The API
then omitted harness and the UI defaulted to "codex" — mislabelling a Claude
Code session.

Inject the daemon's default agent (AO_AGENT / config.DefaultAgent) into the
session manager and resolve an unspecified harness to it before the seed row is
written, so the stored/returned harness matches the agent that actually runs.

Closes #220
2026-06-15 00:24:20 +05:30
Harshit Singh Bhandari 724b9a2d81
Use ~/.ao as canonical state home (#233)
* Use ~/.ao as canonical state home

* chore: format with prettier [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-14 22:52:25 +05:30
Adil Shaikh 17df922652
feat: add notifications v1 (#181)
* feat: add notifications v1

* fix: address notification review feedback

* fix: require passing CI for merge-ready notifications

* fix: simplify notification listing

* fix: ignore missing sessions for scm notifications

* fix: project notifications from cdc

* fix: stream notifications without cdc
2026-06-14 20:02:32 +05:30
Harshit Singh Bhandari c4961019d0
fix(session): remove orchestrator kickoff auto-prompt on spawn (#227)
* fix(session): remove orchestrator kickoff auto-prompt on spawn

Spawning a session without an explicit prompt injected a "Get oriented..."
kickoff turn for orchestrators, which surfaced as an unsolicited message to
the orchestrator. Drop the auto-prompt so a promptless spawn delivers nothing
to the agent, leaving it idle at an empty input box.

Closes #226

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

* fix(session): re-apply derived system prompt on agent resume

Restore re-derives the standing system prompt but only handed it to the
fresh-launch fallback, not the native GetRestoreCommand path, so a resumed
orchestrator/worker lost its role instructions. Pass SystemPrompt through to
the restore command too, matching adapters that re-append it on resume.

Also fix the recordingAgent test double to return ok=false when there is no
agent-session id, mirroring every real adapter, so the fallback-launch path is
actually exercised. These three TestRestore_* cases were red on main since #222.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:22:08 +05:30
yyovil f7df36bb8b
Split: backend runtime/session updates + frontend shell refactor (#222)
* feat(frontend): rebuild Electron desktop UI as a React + Vite renderer

Replaces the skeleton Electron frontend with a full React 19 + TypeScript
renderer (Vite, electron-forge, contextBridge preload), plus the backend
additions it needs.

Renderer:
- TanStack Query + EventTransport (CDC SSE on /api/v1/events)
- TanStack Router file-system routing (hash history for the file:// origin)
- Tailwind + shadcn/ui, react-resizable-panels, Zustand UI state
- @xterm/xterm per-session PTY over /mux WebSocket + WebGL addon
- openapi-typescript + openapi-fetch types off openapi.yaml
- electron-forge packaging + update-electron-app auto-updater
- Vitest + RTL · Playwright

Backend:
- cors.go — allowlist-only CORS, handles Private Network Access preflight
  for app:// renderer -> loopback daemon
- session.TerminalHandleID exposed in domain + OpenAPI spec
- project.Path added to OpenAPI spec, service, store, and tests

DESIGN.md documents the emdash-matched dark UI (tokens, blue accent, status
glyph spec, orchestrator-led layout).

Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b1e334c1e54a

* feat(terminal): port yyork's terminal rendering architecture

XtermTerminal becomes a self-contained, dependency-free renderer component
(yyork's xterm-terminal.tsx pattern):
- Nothing writes into the buffer at mount — status/empty-state is DOM chrome.
  Fixes the startup crash (xterm Viewport.syncScrollArea reading renderer
  dimensions on a zero-sized panel).
- Multi-trigger fit (rAF + 50/250ms settle + fonts.ready + ResizeObserver):
  FitAddon must re-measure after monospace font metrics settle or it
  over-counts columns. xterm only fires onResize on real grid changes, so
  repeated fits don't spam the PTY.
- Unicode11 width (agent CLIs print emoji/wide glyphs), WCAG-AA minimum
  contrast, WebGL→canvas renderer fallback, full ANSI-16 palette per DESIGN.md.

TerminalPane keeps ONE terminal instance across session switches — the
attachment effect re-points the mux and RIS-resets the screen instead of
remounting (a keyed remount drops the warm GPU surface mid-switch).

useTerminalSession: resize debounced 100ms trailing (one SIGWINCH per pane
drag, not dozens); the "Attaching…" writeln is gone (banner chrome covers it).

Test infra: vitest never loaded vite.renderer.config.ts after the forge split
(it only auto-discovers vite.config.*) so the whole suite ran without jsdom.
Point the test script at the config and type it via vitest/config. Fix
pre-existing type errors (notarytool field, maker-zip config, named
updateElectronApp import). 97/97 passing, typecheck clean.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: eb0f94fcf914

* fix(spawn): don't send base branch; surface real API errors

Two bugs found spawning a worker from the modal:

1. "Based on: main" sent branch:"main" in the POST, but git can't add a
   second worktree on a branch already checked out (main lives in the repo
   root) — the daemon returns 409 BRANCH_CHECKED_OUT_ELSEWHERE. The base
   branch must be OMITTED so the daemon mints a fresh ao/<sessionId> off the
   project default. Only a non-default branch (resume an existing session
   branch) is sent through.

2. The daemon's error body is {error,code,message,requestId}; App.createTask
   did String(error) on it → the modal showed "[object Object]". Add
   apiErrorMessage() to unpack message/error from the structured body, with
   Error/string fallbacks.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d3e718d393cd

* feat(terminal): Nerd Font glyph support via --font-mono (yyork pattern)

The terminal now resolves its fontFamily from the --font-mono CSS token
(styles.css @theme), which leads with the Nerd Font family stack
(JetBrainsMono Nerd Font Mono first). Agent TUIs get powerline separators
and file-type icons; box-drawing stays renderer-rasterized.

Mirrors yyork exactly: no font is bundled — the stack names system-installed
Nerd Fonts and the browser picks the first present, falling back to plain
monospace (no icon glyphs) when none are installed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 28120fa527a7

* chore(frontend): set up shadcn/ui foundation

Prep for the route-parity port: build new screens from shadcn primitives.
- components.json: Tailwind v4, css=styles.css, cssVariables, lucide, "@/" aliases
- "@/" -> src/renderer alias in tsconfig (paths) + vite (resolve.alias)
- fill the shadcn token gaps in @theme (card-foreground, input, destructive,
  destructive-foreground) mapped to existing emdash tokens so `shadcn add`
  components render on-brand without touching the design system
- add Card primitive (first use: Phase 1 board)

Did NOT run `shadcn init` (it would overwrite styles.css and wipe the emdash
tokens); the @theme already maps shadcn semantic names onto emdash raw tokens.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 9b9c8391e52c

* refactor(renderer): persistent _shell layout + per-route pages (projects vocab)

Phase 0 of the route-parity port. Replaces the single state-driven <App> with
real TanStack Router pages behind a persistent _shell layout, the foundation
every ported screen builds on.

- _shell.tsx: pathless layout owning the Sidebar + shared state (workspace
  query, daemon status, spawn modal, create project/task, theme, shortcuts);
  child routes render into <Outlet>. The daemon-status effect runs once here.
- Router owns selection: ui-store sheds view/selectedSession/selectedWorkspace
  (now route params); keeps only theme/sidebar/workbenchTab. Sidebar/SideRail
  navigate via router and read active state from useParams.
- Routes (projects vocabulary): / -> SessionsBoard (new board home, replacing
  the orchestrator-terminal home), /projects/$projectId -> scoped board,
  /projects/$projectId/sessions/$sessionId and /sessions/$sessionId ->
  SessionView (Topbar + terminal + git rail).
- Terminal persistence: it lives on the session route, so session->session is
  a param change (TanStack keeps the route mounted -> mux re-points, no
  remount); leaving for the board unmounts it and the server ring replays on
  return.
- shell-context.ts hands daemonStatus/openSpawn/create* to route content.

Removed the monolithic App.tsx (+ App.test.tsx, whose create/spawn coverage
moves to route/hook-level tests in Phase 5) and the old workspaces.* routes.
shadcn Card used for the board cards. typecheck clean, 91 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d10b22f0d841

* feat(board): attention-zone kanban home

Phase 1: SessionsBoard becomes the real kanban, porting agent-orchestrator's
getAttentionLevel state machine (packages/web/src/lib/types.ts) as a pure
function rebound to reverbcode's SessionStatus.

- attentionZone() buckets a session into urgency-ordered zones — merge (one
  click to clear, leftmost) → action (needs-you: needs_input/ci_failed/
  changes_requested, the collapsed respond+review) → pending (waiting on
  reviewer/CI) → working → done (archive).
- Board renders a horizontal column per non-empty zone; cards navigate into
  the session route. shadcn Card for cards. Styled to DESIGN.md (emdash
  hairlines, status dots, accent), not agent-orchestrator's tokens.
- 13 zone-mapping unit tests. typecheck clean, 104 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d315acacf784

* feat(settings): project settings form (Phase 2)

/projects/$projectId/settings — a settings page on reverbcode's own
ProjectConfig shape (not agent-orchestrator's agent/runtime/tracker/scm,
which the Go daemon doesn't have). Reuses agent-orchestrator's form structure:
read-only identity card + editable config.

- Reads GET /api/v1/projects/{id} (config + identity), saves via
  PUT /api/v1/projects/{id}/config. The PUT replaces the whole config, so the
  form merges edited fields over what loaded (keeps env/symlinks/postCreate
  it doesn't expose).
- Editable: defaultBranch, sessionPrefix, default worker/orchestrator agent,
  model override. React Query for load + mutation with inline save state.
- shadcn select + label added; settings gear in the project board header.

typecheck clean, 104 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 0633ebde603b

* feat(session): PR inspector in the session rail (Phase 2)

Ports agent-orchestrator's SessionInspector onto reverbcode's SessionPRFacts
(GET /api/v1/sessions/{id}/pr -> {prs: SessionPRFacts[]}). Mounts above the
git rail on the session route; renders nothing when the session has no PR.

- Shows PR number + state badge, and CI / mergeability / review facts with
  tone derived from the fact string (pass/fail/pending), plus an unresolved
  review-comments flag.
- React Query, fetched only when the session has a PR.

Completes Phase 2 (project board reuse + settings + inspector).
typecheck clean, 104 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: c75dadfd45b8

* feat(prs): pull-requests board (Phase 3)

/prs — a PR board ported from agent-orchestrator's PullRequestsPage. The Go
daemon has no PR-list endpoint, so rows are derived from session PR fields
(every session carries pullRequest), sorted open/draft above merged/closed.

- Per-row Merge (POST /prs/{number}/merge) and Resolve comments
  (POST /prs/{number}/resolve-comments) mutations with inline result; clicking
  a row opens the session (whose inspector has the full CI/review facts).
- shadcn Table; "Pull requests" + "Review" nav added to the sidebar footer.
- /review + /reviews routes added as placeholders (the reviews board needs a
  daemon backend — Phase 4); /reviews redirects to /review.

typecheck clean, 104 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 3526d847c568

* feat(reviews): code-review API + dashboard (Phase 4)

The long pole: the Go daemon had no reviews surface, so /review needed a
backend. Adds one, mirroring agent-orchestrator's reviews feature.

Backend:
- internal/service/review: in-memory reviews Manager (Run + Finding types,
  List/Execute/Send). Execution is not yet wired to a real review agent —
  Execute records a pending run so the surface is live; agent-backed findings
  + persistence are a follow-up (documented in the package).
- ReviewsController (GET /reviews, POST /reviews/execute, POST /reviews/{id}/send),
  wired through api.go + daemon.go (constructed, not nil — actually serves).
- genspec: reviewOperations() + tag + schemaNames; openapi.yaml + schema.ts
  regenerated. apispec parity/drift tests pass, go build + go test green.

Frontend:
- ReviewDashboard reads GET /reviews, lists runs with status + findings, lets
  you pick a worker and Run review (execute) and Send a run. Replaces the
  placeholder /review route. shadcn Card/Badge/Select.

Verified live: GET /reviews -> 200, POST /reviews/execute -> created run.
typecheck clean, 104 frontend tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: ce16c62dfdb0

* chore(renderer): Phase 5 polish — route prefetch + restored spawn coverage

- Route loader: _shell prefetches the workspace list via
  queryClient.ensureQueryData (parent loader runs before children), pairing
  with defaultPreload: "intent" so a hovered nav target is warm on click.
  workspaceQueryOptions exported so the loader and hook share one cache.
- Restored the spawn coverage dropped with App.test.tsx as a focused
  SpawnWorkerModal test: the base-branch-omission regression guard (the 409
  fix) + the empty-prompt gate.

Full parity surface green: 106 frontend tests, typecheck clean, backend
build + vet clean, all daemon endpoints 200.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 284ae668ffea

* feat(board): match agent-orchestrator's board verbatim

Per explicit request to mirror agent-orchestrator's app exactly (overriding
DESIGN.md/emdash for this screen). Rebuilds SessionsBoard from its actual
source (Dashboard + AttentionZone + SessionCard + mc-board.css), using its
exact tokens and values:

- 4 equal-width columns (grid 1fr), left->right flow: Working -> Needs you ->
  In review -> Ready to merge (SIMPLE_KANBAN_LEVELS), always rendered; "done"
  archived to a separate strip, not a column.
- Per-column vertical glow gradient (status-tinted top fading at 130px) +
  glow dots + uppercase tinted column titles; #0a0b0d base, #15171b cards.
- Topbar: project crumb + Coding/Reviews tabs + breathing "N working" pill +
  bell + blue "New worker" primary. "Board" subhead + subtitle.
- Card: status badge (dot + label) · mono id, 2-line title, mono branch line,
  hairline-topped PR footer ("no PR yet").

typecheck clean, 106 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 3569e49ba7d6

* feat(theme): clone agent-orchestrator's dark palette globally

Per the verbatim-clone directive (supersedes DESIGN.md/emdash). Remaps the
:root tokens to agent-orchestrator's exact values — #0a0b0d base, #15171b
card, #f4f5f7/#9ba1aa/#646a73 text, hairline white-alpha borders, #4d8dff
accent, orange/amber/green/red status — so every screen's base shifts at once.
Adds --color-working (orange) for the working status.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d70096d7f30d

* feat(renderer): clone agent-orchestrator ProjectSidebar verbatim

Rebuild the sidebar to match agent-orchestrator's ProjectSidebar: #08090b
rail, "Reverb / Code" brand with dimmed separator + collapse button,
uppercase PROJECTS label, project disclosure rows (rotating chevron +
hover-revealed New worker action + session count), nested session rows
with a 6px breathing working-dot and mono session id, and a single
Settings menu footer (Pull requests / Reviews / Search / Project
settings) plus a daemon-health dot.

Adds a shadcn dropdown-menu primitive (radix-ui unified package, matching
the existing select/label convention) for the footer menu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: c4e1827b6142

* feat(renderer): clone agent-orchestrator session topbar

Restyle the session header to match agent-orchestrator's SessionDetailHeader:
a "Kanban" back-to-board button + hairline divider, a stacked identity
(project / title over a mono branch line with a git-branch icon), and a
StatusBadge --pill (tinted bordered pill with a 6px dot that breathes while
the agent is working). Wire onOpenBoard from SessionView to navigate back to
the project board (or home).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: dcd3e4880af5

* feat(renderer): unify board/review/PR/settings chrome verbatim

Extract the mc-board dashboard header (project crumb · Coding/Reviews tabs ·
"N working" breathing pill · bell · settings · New worker) and the 21px
subhead into a shared DashboardTopbar/DashboardSubhead, then apply it to the
review, PR, and settings screens so every dashboard surface shares one stable
agent-orchestrator top strip. SessionsBoard now consumes the shared chrome
instead of its inline copy; review/PR/settings drop their minimal h-11 headers
for the crumb+tabs+subhead treatment on the #0a0b0d base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 2801a9c5c0ba

* docs: record agent-orchestrator-verbatim design direction

Per explicit user decision (2026-06-10), the renderer clones the
agent-orchestrator web app verbatim, superseding the older "match emdash"
direction. Add a prominent banner at the top of DESIGN.md (reference files,
live palette, the cloned surfaces, shadcn-primitive guidance), mark the
Aesthetic Direction section as superseded, and retarget CLAUDE.md's QA rule so
future review flags divergence from agent-orchestrator instead of emdash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: fe028f97d5d5

* feat(renderer): clone agent-orchestrator shell and inspector

Finish the agent-orchestrator-style renderer pass with shadcn sidebar chrome, titlebar navigation, resizable session inspector, orchestrator spawn affordances, and matching design tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: format with prettier [skip ci]

* fix: repair UI PR CI drift

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(terminal): per-client zellij attach replaces shared PTY + replay ring

Each WebSocket client that opens a pane now gets its own `zellij attach`
PTY (attachment.go) instead of sharing one PTY whose output was replayed
from a bounded byte ring. Zellij answers every fresh attach with its full
init handshake (alt screen, SGR mouse tracking, bracketed paste) and a
faithful repaint — the ring replay lost exactly that handshake, leaving
late subscribers without mouse reporting (dead wheel scroll). The cost is
one zellij client process per open pane per connection, which the zellij
server is built for (yyork ships the same model).

ring.go and session.go (fan-out, replay buffer) are deleted; manager.go
now tracks per-client attachments with liveness gating, and pty_unix.go
answers every resize frame with an explicit SIGWINCH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(renderer): re-assert settled terminal resize; align docs with per-client attach

After each debounced resize settles, send one follow-up resize frame with
the same grid (RESIZE_REASSERT_MS). xterm only fires onResize on actual
grid changes, so a resize update the zellij client loses (raced mid-attach
or coalesced during a drag) would otherwise desync the session layout from
the pane until the next real change. The backend answers every resize
frame with an explicit SIGWINCH, so the re-assert is a no-op when already
in sync.

Comments in the terminal hook/components now describe the per-client
attach model (fresh server-side `zellij attach` per open, no replay ring).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(renderer): full-width shell topbar; retire per-view topbars and review dashboard

The shell now owns a single full-width ShellTopbar (status pill, history
arrows, notifications, kanban/inspector toggles) with the sidebar pinned
below it, replacing the per-view Topbar/DashboardTopbar pair; board pages
get a lightweight DashboardSubhead. The standalone review dashboard and
its /review(s) routes are removed — review state lives on the PR board.
Approved divergence from the AO reference (full-height sidebar) recorded
in DESIGN.md; macOS traffic lights re-centered on the 56px header row.

Also hardens the session view around rrp v4:
- inspector defaultSize re-derived per panel mount (orchestrator → worker
  navigation kept SessionView mounted while the panel remounted), and the
  imperative expand/collapse effect no longer races panel registration
- onResize writes gated on data-separator="active" so flex-grow
  transition frames can't bounce the store (dead-looking toggle button)
- findProjectOrchestrator skips terminated orchestrators so the topbar
  offers Spawn instead of attaching to a dead zellij session
- inspector resize handle gets a visible 1px divider at rest
- playwright specs for history arrows + inspector toggle; test-results/
  gitignored

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document Electron app dev quick start

Add an "Electron app (dev)" section: npm install + npm run dev under
frontend/, with the explicit heads-up that the app does not start the
daemon — it attaches over loopback to a daemon started via `ao start`
(plus npm run dev:web for renderer-only work in a browser).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(fork): ignore local agent session dirs

Fork-only ignore entries (.entire/.claude/.gstack) — must not be included
in upstream PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

* feat: align backend session lifecycle with workspace runtime updates

* refactor: replace spawn modal with shell-native worker controls

* chore: add shared daemon launch helper and docs updates

* chore: format with prettier [skip ci]

---------

Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-13 12:18:55 +05:30
Harshit Singh Bhandari 295cda5787
fix: forward project permissions to agent launches (#198) 2026-06-12 16:26:42 +05:30
yyovil 7c97ee79cd
refactor(terminal): per-client zellij attach replaces shared PTY + replay ring (#194)
* refactor(terminal): per-client zellij attach replaces shared PTY + replay ring

Each WebSocket client that opens a pane now gets its own `zellij attach`
PTY (attachment.go) instead of sharing one PTY whose output was replayed
from a bounded byte ring. Zellij answers every fresh attach with its full
init handshake (alt screen, SGR mouse tracking, bracketed paste) and a
faithful repaint — the ring replay lost exactly that handshake, leaving
late subscribers without mouse reporting (dead wheel scroll). The cost is
one zellij client process per open pane per connection, which the zellij
server is built for (yyork ships the same model).

ring.go and session.go (fan-out, replay buffer) are deleted; manager.go
now tracks per-client attachments with liveness gating, and pty_unix.go
answers every resize frame with an explicit SIGWINCH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(renderer): re-assert settled terminal resize; align docs with per-client attach

After each debounced resize settles, send one follow-up resize frame with
the same grid (RESIZE_REASSERT_MS). xterm only fires onResize on actual
grid changes, so a resize update the zellij client loses (raced mid-attach
or coalesced during a drag) would otherwise desync the session layout from
the pane until the next real change. The backend answers every resize
frame with an explicit SIGWINCH, so the re-assert is a no-op when already
in sync.

Comments in the terminal hook/components now describe the per-client
attach model (fresh server-side `zellij attach` per open, no replay ring).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:19:38 +05:30
Harshit Singh Bhandari 9b4651c612
fix(session-manager): deliver orchestrator instructions via system prompt (#189)
Orchestrator role definitions and worker coordination hints were being
prepended/appended to the user-facing prompt string. They now go into
SystemPrompt in LaunchConfig so agents receive them as standing instructions
rather than part of the human's task request.

Closes #182

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 21:49:22 +05:30
Harshit Singh Bhandari 53ca81286a
feat(workspace): name orchestrator worktree orchestrator/{prefix}-orchestrator (#191)
* feat(workspace): name orchestrator worktree orchestrator/{prefix}-orchestrator

Orchestrator sessions now get a dedicated worktree path under
orchestrator/{prefix}-orchestrator within the project directory,
matching the pattern described in issue #184.

Workers retain the existing {sessionID} naming. The session prefix
falls back to the first 12 chars of the project ID when no explicit
SessionPrefix is configured.

Closes #184

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(lint): remove unnecessary string conversion in sessionPrefix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(lint): drop redundant string() casts — project.ID is already string
2026-06-11 21:37:47 +05:30
Harshit Singh Bhandari 9c7866daba
chore: change default data/config dir from ~/Library/Application Support to ~/.ao (#187)
Replaces os.UserConfigDir() + "agent-orchestrator/..." fallback with
os.UserHomeDir() + ".ao/..." in resolveRunFilePath() and resolveDataDir(),
so the default layout is:
  ~/.ao/running.json
  ~/.ao/data/

AO_RUN_FILE and AO_DATA_DIR env overrides are unchanged.

Closes #183
2026-06-11 20:09:36 +05:30
yyovil e493de6ad7
feat(renderer): clone agent-orchestrator shell and inspector 2026-06-11 16:45:00 +05:30
yyovil 0ee86a6314
fix(terminal): prevent zellij session resurrection 2026-06-11 16:38:19 +05:30
Ashish Huddar d60c49f5fb
fix(spawn): stop sending branch on spawn, render API errors, wire worker name (#171)
* fix(spawn): stop sending branch on spawn, render API errors, wire worker name

Three spawn-modal bugs, re-landed from the closed redesign branch (#156):

- createTask no longer sends `branch`: the API field names the session's
  NEW worktree branch, so submitting the modal's default ("main") made the
  daemon 409 with BRANCH_CHECKED_OUT_ELSEWHERE on every spawn. The "Based on"
  pane is informational — workers branch off the project's default branch in
  a fresh worktree.
- API errors render their envelope message instead of "[object Object]":
  openapi-fetch resolves non-2xx responses to a plain {code,error,message}
  object, not an Error; new apiErrorMessage unwraps it (message + code).
- The "Worker name" field is actually used: after spawn, a best-effort
  PATCH rename sets the displayName (a failed rename must not look like a
  failed spawn — the worker is already running).

Also revives the renderer test suite, which made these tests (and 7 of 9
suite files) impossible to run on main:

- vitest.config.ts re-exports vite.renderer.config so `vitest run` actually
  loads the jsdom environment + setup file. Forge's per-target
  vite.*.config.ts names are invisible to vitest, so the existing `test`
  block was dead config and every DOM-touching test died on
  "window is not defined".
- vite.renderer.config.ts imports defineConfig from vitest/config so its
  `test` key typechecks.
- routeTree.gen.ts + the session route are regenerated by the pinned
  @tanstack/router-plugin (it runs as part of loading the renderer config;
  the checked-in tree predated the installed plugin version and its route-ID
  drift caused 3 of main's 7 typecheck errors).
- App.test.tsx wraps App in TooltipProvider, mirroring routes/__root.tsx.

Frontend: 9/9 test files, 99/99 tests pass (was 2/9 files). Typecheck is
down from 7 errors to 3 — the survivors (forge.config notarize/maker types,
update-electron-app call signature) predate this branch and are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

* fix(gitworktree): base new session branches on the local default branch when no remote exists

Re-lands the remoteless fallback from the closed redesign branch (archived
in 641b712). Creating a session worktree resolved the base for a NEW branch
only via the remote-tracking ref (origin/<defaultBranch>), so a registered
repo with no remote failed every spawn with BRANCH_NOT_FETCHED — an error
that misleadingly names the new session branch and suggests `git fetch`,
which is impossible without a remote.

refs/heads/<defaultBranch> now follows origin/<defaultBranch> in the
candidate list: remote-tracking still wins whenever it exists, and a
remoteless repo bases session branches on its local default branch.

Verified live: a plain `git init` repo (no remote) that previously failed
now spawns, and the integration suite covers it
(TestWorkspaceIntegrationCreateInRemotelessRepo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-11 12:13:30 +05:30
Ashish Huddar 40bd2dfb2b
fix(sessions): stop AO hook files from making every worktree permanently dirty (#169)
* fix(sessions): stop AO hook files from making every worktree permanently dirty

Agent adapters write hook files (.codex/hooks.json, .opencode/plugins/
ao-activity.ts, .claude/settings.local.json, ...) into fresh session
worktrees as untracked files. `git worktree remove` (deliberately run
without --force) refuses on any untracked file, so Workspace.Destroy
failed for every session of the 12 workspace-writing harnesses:
POST /sessions/{id}/kill returned an unlogged 500 INTERNAL_ERROR and
`ao session cleanup` reported 'Would clean N' then '0 sessions cleaned'
with no reason, leaking workspaces forever.

Three coordinated fixes, none of which force-deletes user/agent work:

- Root cause: every adapter now writes a sentinel-guarded, self-ignoring
  .gitignore next to its hook files (hookutil.EnsureWorkspaceGitignore),
  so AO's own files no longer count as dirt while anything an agent
  drops — even in the same directory — still blocks teardown. A
  registry-wide conformance test enforces the contract for all current
  and future adapters. (Per-worktree .git/worktrees/<name>/info/exclude
  was evaluated first but git does not honor it.)
- Typed refusal: gitworktree.Destroy classifies a still-dirty refusal as
  ports.ErrWorkspaceDirty (git status probe). Kill maps it to success
  with freed=false (session terminated, worktree preserved); Cleanup
  reports it per-session as skipped-with-reason through the API
  (CleanupSessionsResponse.skipped), and the CLI prints
  'Skipped: <id> (workspace has uncommitted changes)' plus a summary.
- Observability: envelope.WriteError records the raw service error into
  a request-scoped slot and the access log attaches it to 5xx lines, so
  any remaining internal error is diagnosable server-side.

Worktrees created before this fix gain the .gitignore on restore (hook
install re-runs); their cleanup is otherwise reported as skipped instead
of erroring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cleanup): address Greptile P2s — surface dirty-probe failures, stop leaking raw errors

Two review findings on this PR:

- gitworktree.Destroy: when the isDirty probe itself failed, the error was
  silently discarded and the refusal looked identical to "registered but not
  dirty". The probe failure now rides the returned error (dirty probe: ...),
  so it reaches the access log via the 5xx error capture.

- Cleanup skip reasons: a non-dirty teardown failure put the raw error —
  including internal filesystem paths — into the public skipped[].reason
  field. The public reason is now the fixed string "workspace teardown
  failed"; the full cause goes to the daemon log (warn, with sessionID and
  path). The dirty-refusal reason is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(gitworktree): wrap the dirty-probe error with %w per errorlint

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 11:06:45 +05:30
Ashish Huddar 8d0c53ec1d
fix(codex): reliable activity signals — session-flag hooks, trust bypass, no_signal watchdog (#170)
* fix(codex): deliver activity hooks via -c session flags, trust worktree at launch

Codex (0.136+) never loads hook config from AO's per-session worktrees:
project-local .codex/ layers only load from trusted directories, and for
linked git worktrees codex sources hook declarations from the matching
folder in the root checkout — so the workspace-local .codex/hooks.json AO
wrote was dead config and codex sessions never reported activity.

Deliver the hooks on the launch/resume command instead:
- -c 'hooks.<Event>=[...]' session-flag config for SessionStart,
  UserPromptSubmit, PermissionRequest, and Stop; the session-flags layer
  is not trust-gated and aggregates with the user's own hooks. The
  existing --dangerously-bypass-hook-trust flag lets them run without a
  persisted trust hash.
- -c 'projects={"<worktree>"={trust_level="trusted"}}' (inline-table
  form; the dotted projects."<path>".trust_level key is corrupted by
  codex's naive -c dot-split) so spawns into never-trusted repos don't
  hang invisibly on the interactive directory-trust prompt. Both the
  literal and symlink-resolved worktree paths are trusted.
- -c notice.hide_rate_limit_model_nudge=true so the "switch to a cheaper
  model?" dialog can't hang a headless pane and swallow the spawn prompt.

GetAgentHooks no longer writes workspace files (worktrees stay clean); it
only strips entries older AO versions left in .codex/hooks.json,
preserving user hooks. UninstallHooks/AreHooksInstalled now operate on
those legacy files only.

Verified with a real spawn into a fresh untrusted repo: activity
transitions idle -> active -> idle hands-free, no .codex dir in the
worktree, no hook delivery failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(sessions): activity-signal watchdog + hook delivery hardening

A codex upgrade broke activity tracking silently: sessions showed a
confident "idle" forever while the agent worked. This bundle makes hook
delivery verifiable end to end and makes any future breakage loud
instead of invisible.

Watchdog (no_signal status):
- sessions.first_signal_at (migration 0010) records the FIRST hook
  callback per spawn/restore — raw signal receipt, independent of the
  derived activity state. lifecycle.ApplyActivitySignal stamps it (and
  writes through same-state repeats until stamped, e.g. Codex
  SessionStart reporting idle on an idle-seeded row); MarkSpawned clears
  it so every relaunch re-proves its hook pipeline.
- deriveStatus downgrades a live session with no receipt to the new
  no_signal display status after a 90s grace, instead of idle.
  Terminated/PR-derived statuses still win. The sessions CDC update
  trigger now also fires on first-signal receipt so the dashboard
  transition is pushed live.
- frontend maps no_signal -> needs_you (a human should look at the pane).

Hook callback hardening (re-landed from the closed redesign PR #156):
- the session manager pins each spawned session's PATH with the daemon
  executable's directory first, so the bare `ao` in hook commands
  resolves to the daemon that installed them, with a spawn-time warning
  when the pin cannot apply.
- `ao hooks` failures append to $AO_DATA_DIR/hooks.log (size-capped);
  `ao doctor` gains a hooks-log check that warns on failures from the
  last 24h, and an ao-binary identity check.

Codex launch-surface canary:
- `ao doctor` gains codex-launch-flags: it runs probes exported by the
  codex adapter (built from the same flag builders as the real spawn
  argv) against the installed binary, warning when codex rejects the
  hook-trust bypass flag or AO's -c session-flag overrides.
- codex hook callback timeout drops 30s -> 5s so a hung daemon cannot
  stall the agent's turn.

Docs: the agent PRD callback section now describes the implemented flow
(derive state, POST /sessions/{id}/activity, hooks.log) instead of the
unbuilt SQLite/metadata merge, and notes that hook-derived metadata
persistence (codex resume) is still not implemented.

Frontend note: main's renderer test suite has 7 pre-existing failing
files and a vite-config typecheck error unrelated to this change;
workspace.test.ts (the only frontend file touched) passes 26/26.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(store): restore TestSessionWorktreesRoundTrip lost in the re-landing port

The branch ported store_test.go wholesale from the closed redesign
branch, whose copy predates #165 — silently dropping the
session-worktrees round-trip test #165 added. Restore main's file and
re-apply only this branch's addition (TestSessionFirstSignalRoundTrip).
No other ported file lost main-side content (audited per-file against
main; the remaining deletions are this branch's intended refactors).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(status): only derive no_signal for harnesses that have a hook pipeline

Review finding: the no_signal downgrade had no harness-capability gate, but
first_signal_at can only ever be stamped by an `ao hooks` callback. Ten
spawnable harnesses (amp, aider, crush, grok, kimi, devin, auggie, continue,
vibe, pi) install no hooks at all, so every live session of theirs would have
flipped from idle to a permanent no_signal -> needs_you after the 90s grace.

The session service now takes a SignalCapable predicate; daemon wiring injects
activitydispatch.SupportsHarness (the deriver registry is the source of truth
for "this harness can signal"). Left nil, the service never claims no_signal.
A new dispatch test pins that every deriver token is a known harness name.

Also from the same review:
- lifecycle/manager.go and the 0010 migration claimed Codex's SessionStart
  reports idle as the first signal; both codex and claude-code derivers
  deliberately return no signal for session-start, so the comments now cite a
  real case (a lost "active" POST followed by a Stop hook landing idle).
- docs/agent/README.md documents the gate and the restore caveat: a restored
  session the user never prompts has nothing to signal, so it shows no_signal
  after the grace until a receipt-only session-start signal exists.
- 0010 migration uses DROP TRIGGER IF EXISTS per house style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:36:45 +05:30
Harshit Singh Bhandari 5244015802
feat: add workspace project registration foundation (#165)
* feat: add workspace project registration schema

* fix: satisfy workspace registration lint

* fix: harden workspace registration edge paths

- Reject linked-worktree and bare parents via validateWorkspaceParent before any mutation
- Roll back git init/.gitignore on failure in initWorkspaceParent so retries are clean
- Reject child repos named __root__ (reserved PK in session_worktrees)
- Serialise Service.Add with addMu to eliminate TOCTOU on concurrent same-path calls
- Fix ensureWorkspaceGitignore permission 0o600 -> 0o644
- Improve guardNoGitlinks suggestedFix with actionable git rm --cached guidance
- Remove dead CASE/__root__ ordering from ListWorkspaceRepos SQL (regenerated via sqlc)
- Resolve RepoOriginURL once per code path in Add (workspace vs single-repo)
- Add 7 tests covering the new edge paths
2026-06-10 16:10:14 +05:30
yyovil c2c4404c7d
feat(frontend): scaffold for frontend with complimentary backend changes (#168)
* feat(frontend): rebuild Electron desktop UI as a React + Vite renderer

Replaces the skeleton Electron frontend with a full React 19 + TypeScript
renderer (Vite, electron-forge, contextBridge preload), plus the backend
additions it needs.

Renderer:
- TanStack Query + EventTransport (CDC SSE on /api/v1/events)
- TanStack Router file-system routing (hash history for the file:// origin)
- Tailwind + shadcn/ui, react-resizable-panels, Zustand UI state
- @xterm/xterm per-session PTY over /mux WebSocket + WebGL addon
- openapi-typescript + openapi-fetch types off openapi.yaml
- electron-forge packaging + update-electron-app auto-updater
- Vitest + RTL · Playwright

Backend:
- cors.go — allowlist-only CORS, handles Private Network Access preflight
  for app:// renderer -> loopback daemon
- session.TerminalHandleID exposed in domain + OpenAPI spec
- project.Path added to OpenAPI spec, service, store, and tests

DESIGN.md documents the emdash-matched dark UI (tokens, blue accent, status
glyph spec, orchestrator-led layout).

Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b1e334c1e54a

* chore: format with prettier [skip ci]

---------

Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-10 11:40:17 +05:30
Harshit Singh Bhandari 5071364f91
fix(sessions): remove agent rules spawn path (#159) 2026-06-09 00:10:28 +05:30
neversettle 7698c24931
feat(config): persist per-project agent config and resolve it at spawn (#154)
* 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>
2026-06-08 21:35:29 +05:30
Harshit Singh Bhandari 33a1ee6df9
feat(tracker): ApplyTrackerFacts reducer + shared observer skeleton (#112) (#116)
* refactor(observe): extract shared observer skeleton

Move the observer-pattern-general pieces of the SCM observer into a new
backend/internal/observe package so the tracker observer (issue #35) can
build on the same primitives:

- StartPollLoop: goroutine supervisor with immediate-first-poll + ticker
  + ctx-done exit. SCM Observer.Start now delegates to it.
- CheckCredentialsOnce: lazy first-poll credential gate driven by a
  CredentialProbe closure. SCM observer keeps credentialsChecked/disabled
  as Observer fields; the shared helper mutates them via pointer so
  state ownership stays single-source.
- CacheSet[V any] / CacheDelete[V any]: one generic bounded-FIFO helper
  replaces the three near-identical cacheSet{String,Time,Bool} bodies
  and the standalone evictStrings. The SCM-side methods are now
  one-line wrappers that thread o.Cache.max into the shared helper, so
  existing call sites and tests are untouched.

SCM behavior is unchanged. The full 21-test SCM suite (including the
end-to-end test added in PR #115) plus 577 backend tests stay green
under `go test -race`.

Part of #112.

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

* feat(tracker): ports.TrackerObservation DTO + ApplyTrackerFacts reducer

Land the contract that the future Tracker observer (issue #35) and its
provider adapters must satisfy. No observer is wired in this PR — the
DTO + reducer are the deliverable, and locking the shape now lets the
observer + adapter work happen in small follow-up PRs.

DTO (backend/internal/ports/tracker_observations.go):
- TrackerObservation mirrors ports.SCMObservation: Fetched bool,
  ObservedAt time.Time, Provider/Host/Repo, normalized Issue facts,
  Comments, and a Changed{State, Assignee, Comments} discriminator.
- TrackerIssueObservation carries the minimal facts lifecycle needs
  today (state, assignee, title, body, timestamps); richer
  per-provider metadata stays inside each adapter.
- TrackerCommentObservation carries the comment fields needed for the
  bot-mention nudge (Author, Body, IsBot, ID for dedup).

Reducer (backend/internal/lifecycle/reactions.go):
- ApplyTrackerFacts(ctx, sessionID, ports.TrackerObservation) error,
  mirroring ApplySCMObservation's "Fetched gate → terminal-state →
  per-bucket reactions" shape.
- Three initial reactions:
    * Issue state == done | cancelled → MarkTerminated (idempotent).
    * Changed.Assignee → log only via slog.Default(). The "assignee
      changed away from AO" policy is reserved for #40.
    * Changed.Comments with bot comments → one-time nudge with
      strings.Join'd bot bodies, deduped by comment IDs.
- The nudge path reuses sendOnce with an empty prURL so the in-memory
  dedup applies but the PR-row persistence path is skipped. Tracker
  signature persistence will land with #35 alongside issue-row storage.

Tests in backend/internal/lifecycle/manager_test.go cover each branch:
terminate (done + cancelled), log-only assignee, nudge fires on new
bot comment, nudge suppressed on repeat, new bot comment id refires,
not-fetched is no-op, terminated session ignores observations.

Part of #112.

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

* fix(observe): rename CacheSet param to avoid shadowing built-in max

golangci-lint revive flagged the CacheSet generic helper's max
parameter as shadowing the built-in max() function. Rename to
maxEntries; signature change is internal to the observe package and
the SCM observer's one-line wrappers pass the value positionally, so
no call sites need updating.

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

* fix: honour disabled state in CheckCredentialsOnce + tighten bot-comment filter

Two P1 review findings on #116:

1. observe.CheckCredentialsOnce was returning (true, nil) on every
   call after the gate ran, even when the probe had marked the
   observer disabled, because the *checked short-circuit ignored
   *disabled. The SCM observer didn't surface this in practice — its
   Poll method has an independent `if o.disabled { return nil }`
   guard that runs first — but a future Tracker observer that relies
   on the helper's documented contract ("Observer stays disabled")
   would silently flip back to "credentials available" after the
   first poll. Change the short-circuit to `return !*disabled, nil`
   and lock the behavior with a regression test that issues repeat
   calls after the probe reported unavailable.

2. lifecycle.newBotCommentContent's "skip uninteresting comments"
   filter used && where it needed ||. A bot comment with an empty ID
   but a non-empty body slipped through and appended "" to the ids
   slice. If every bot comment in the observation had an empty ID,
   strings.Join produced "" — which matches the zero value of the
   in-memory dedup map, so sendOnce treated the nudge as
   already-sent and silently suppressed it forever. Switch to || so
   any comment missing either an ID or a body is dropped, and add a
   regression test that an empty-ID bot comment never nudges (and
   does not pollute the dedup state for a follow-up comment that has
   a real ID).

586 tests pass with -race.

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

* test(observe): capture deadline once in poll-error spin-wait

The `TestStartPollLoop_LogsPollErrorWithoutPanic` spin-wait was
computing the loop bound as `time.Now().Before(time.Now().Add(200ms))`
on every iteration, which is permanently true — the loop could only
exit via the `break`. Under a scheduler delay (heavy CI load or
`GOMAXPROCS=1`) where two polls never land in time, the test would
hang until the wall-clock kill rather than failing fast.

Capture the deadline once before the loop, and tighten the assertion
to actually require two polls + done-channel closure within a bounded
window, matching `TestStartPollLoop_FirstPollImmediateThenTicks`.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 21:29:00 +05:30
Harshit Singh Bhandari c343c55c14
fix: 7 bugs from discussion #149 smoke walk (envelope, spawn, CDC, observer) (#153)
* 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.
2026-06-07 07:35:46 +05:30
Harshit Singh Bhandari 9dedae905f
feat(agents): add remaining 15 adapters (droid, amp, agy, crush, aider, goose, auggie, continue, devin, cline, kiro, kilocode, vibe, pi, autohand) (#150)
* feat(agents): add droid adapter

Registers the droid harness, stacked on the agent platform. Includes its own activity deriver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agents): add amp adapter

Registers the amp harness, stacked on the agent platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agents): add agy adapter

Registers the agy harness, stacked on the agent platform. Includes its own activity deriver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agents): add crush, aider, goose, auggie, continue, devin, cline, kiro, kilocode, vibe, pi, autohand adapters

Cherry-pick batch landing the remaining 12 yyovil adapter directories per
Discussion #148 recipe, on top of #145 (grok/cursor/qwen/copilot/kimi) and
the droid/amp/agy commits earlier on this branch. Each adapter is a
self-contained package under backend/internal/adapters/agent/<name>/;
registry.Constructors(), activitydispatch.Derivers (for adapters with
activity.go), and wiring_test.go are unified to register all 23 shipped
adapters in one place. No new migration: 0007_allow_implemented_harnesses
already widens the sessions.harness CHECK to cover every adapter.

* fix(agents/kilocode): return error from json.Marshal of permission config

Previously the marshal error was discarded and the function returned a
prefix carrying an empty KILO_CONFIG_CONTENT. An unrecoverable marshal
failure for the typed map should never happen in practice, but if it ever
did, Kilo would silently launch with default permissions regardless of
the requested mode. Surface it as "no prefix" so the caller's mode choice
can't be misrepresented.

---------

Co-authored-by: yyovil <itsyyovil@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:46:52 +05:30
Harshit Singh Bhandari b13f413515
feat(agents): add grok, cursor, qwen, copilot, kimi adapters (#145)
* feat(agents): add grok adapter

Registers the grok harness (xAI Grok CLI). grok installs Claude Code-compatible
hooks, so it reuses the claude-code activity deriver already in the platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agents): add cursor adapter

Registers the cursor harness, stacked on the agent platform. Includes its own activity deriver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agents): add qwen adapter

Registers the qwen harness, stacked on the agent platform. Includes its own activity deriver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agents): add copilot adapter (#128)

* feat(agents): add copilot adapter

Registers the copilot harness, stacked on the agent platform. Includes its own activity deriver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update backend/internal/adapters/agent/copilot/hooks.go

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(copilot): map permission-request to documented preToolUse event

Copilot CLI does not document a "permissionRequest" hook event. Per
https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks
the documented camelCase events are sessionStart, sessionEnd,
userPromptSubmitted, preToolUse, postToolUse, errorOccurred, agentStop.
Writing "permissionRequest" into .github/hooks/ao.json silently disables
that hook because Copilot does not recognize the key.

Remap AO's permission-request sub-command onto preToolUse (the closest
documented signal — fires before any tool invocation, including ones
that would prompt for approval) and add a tripwire test asserting the
JSON keys AO writes match the documented camelCase names.

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

* chore(copilot): gofmt the new tripwire test

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: Harshit Singh Bhandari <dev@theharshitsingh.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>

* feat(agents): add kimi adapter

Registers the kimi harness, stacked on the agent platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agents/kimi): drop approval flags on -p and --session paths

Kimi rejects `--prompt` combined with `--yolo`/`--auto`/`--plan`, and
rejects `--yolo`/`--auto` combined with `--session`/`--continue`
(non-interactive and resumed sessions inherit the auto permission
policy). The previous mapping appended one of those flags before `-p`
on every launch and before `--session` on every restore, so every
non-interactive launch would fail at startup. The local binary
(v1.37.0) additionally has no `--auto` option at all, which would
fail even on otherwise-permissible paths.

- GetLaunchCommand: emit approval flags only on the interactive path
  (no prompt). The `-p <prompt>` path is now bare.
- GetRestoreCommand: never emit approval flags; resumed sessions
  inherit the original session's approval settings.
- Tests assert no approval/plan flag leaks onto either path for any
  PermissionMode, and keep the interactive mapping unchanged.

Refs: https://moonshotai.github.io/kimi-code/en/reference/kimi-command.html

* fix(agents/qwen): sync hook settings temp file

* fix(agents/grok): delegate hook cleanup lifecycle

---------

Co-authored-by: yyovil <itsyyovil@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-07 01:17:53 +05:30
yyovil 3152cdc948
feat(agents): agent platform — registry, activity hooks, harness allowlist (#119)
Introduces the shared platform that per-agent adapters plug into, wired for the
three shipped harnesses (claude-code, codex, opencode):

- adapters/agent/registry: single source of truth for shipped adapters
  (Constructors), consumed by the daemon to resolve a session's harness.
- adapters/agent/activitydispatch + 'ao hooks' command: maps an agent's native
  hook callbacks onto AO activity states (active/idle/waiting/...).
- claudecode/codex/opencode: emit SessionStart/UserPromptSubmit/Stop activity.
- HTTP + OpenAPI: report session activity state.
- db: single migration widening sessions.harness to all shipped harnesses, so
  adding an adapter needs no further migration.
- domain: harness constants + --agent alias for 'ao spawn'.

Adding a new agent is now one adapter package plus a line in Constructors().

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Harshit Singh Bhandari <claudeagain@pkarnal.com>
2026-06-07 00:52:40 +05:30
yyovil 3c7344b233
[codex] add ao hooks activity command (#113)
* feat: add ao hooks activity command

* fix(activity): address review nits

- lcm: sameActivity ignores LastActivityAt so same-state repeats no-op
  and don't churn UpdatedAt / CDC events.
- cli/hooks: surface stdin read errors to stderr for parity with the
  daemon-error path; still exit 0 so a failed hook can't break the agent.
- claudecode: GetAgentHooks docstring covers Notification + SessionEnd
  (the slice already included them; only the comment was stale).

---------

Co-authored-by: harshitsinghbhandari <dev@theharshitsingh.com>
2026-06-06 20:29:00 +05:30
Vaibhaav Tiwari a9b08cd368
feat(cdc): add SSE event stream replay (#106)
* feat(cdc): add SSE event stream replay

* fix(cdc): document SSE route registration

* test(httpd): cover SSE dedupe and Last-Event-ID cursor paths

Two gaps in events_test.go coverage:

- TestEventsStreamDeduplicatesLiveEventOverlappingReplay: a live event whose
  seq falls within the already-replayed range must be silently dropped by
  writeSSEEvent so the client sees each seq exactly once. Publishes seq=5
  (duplicate of replay) and seq=6 (new) into the live buffer before replay
  returns; asserts the client receives [5,6], not [5,5,6].

- TestEventsStreamParsesLastEventIDHeader: Last-Event-ID header must be used
  as the replay cursor when the after query param is absent. Source returns
  after+1, so receiving seq=8 proves the header was parsed as 7.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: regenerate schema.ts for GET /api/v1/events

Regenerated with npm run api after merging the OpenAPI spec generation
tooling from main. Adds the streamEvents operation and its after cursor
parameter to the TypeScript API types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: harden SSE event stream headers

---------

Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: Pritom14 <pritommazumdar1995@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 19:32:21 +05:30
Harshit Singh Bhandari 3413acca33
feat: ao session claim-pr + spawn --claim-pr wiring (#101)
* feat: add session PR claiming CLI and API

* fix: tighten PR claim rollback and CDC facts

* fix: align PR claim branch with latest main
2026-06-06 00:01:03 +05:30
Harshit Singh Bhandari 378addf4a0
test(scm): end-to-end integration coverage for SCM observer (#109) (#115)
* test(scm): end-to-end integration coverage for SCM observer (#109)

Adds backend/internal/integration/scm_observer_test.go, the regression
guard for the SCM observer wiring landed in PR #114. Drives
scmobserve.Observer.Poll against a real sqlite.Store, a real
lifecycle.Manager with a recording messenger spy, and a canned
observe/scm.Provider, asserting the full observation -> reducer ->
store -> messenger pipeline.

Three table-driven subtests, each on its own tmpdir fixture:

- A CI-failing observation persists the pr row (provider-neutral
  columns + semantic hashes), persists pr_checks mirroring the
  observation, delivers exactly one nudge with the failed-log tail,
  persists last_nudge_signature, and produces no additional nudge on
  an identical re-poll.
- A Merged: true observation MarkTerminated's the session and sends
  no nudge.
- A branch with no open PR writes nothing and sends no nudge.

Closes #109

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

* test(scm): address review — drop string key indirection, document idempotency path

- Key cannedSCMProvider.observations/reviews by PR number directly so
  the fake no longer carries a string key that resembled (but did not
  actually need to mirror) the observer's internal prKey. Every case
  in this test uses scmTestRepo, so number alone is unambiguous.
- Add an explicit pointer in the CI-failing subtest noting it
  exercises the hash-match short-circuit in prepareForPersistence;
  the ETag-driven 304 short-circuit on the same SHA is covered by
  observe/scm/observer_test.go (Poll_RepoETag304SkipsDetectPR,
  Poll_CIETagChangeRefreshesWhenRepoUnchanged).

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 23:46:38 +05:30
Harshit Singh Bhandari bfb6e9860b
feat(scm): wire observer messenger + RepoOriginURL + persist dedup (#108) (#114)
* feat(daemon): thread runtime messenger into Lifecycle Manager (#108)

The daemon used to construct the LCM with a nil messenger, so every
SCM-driven nudge dropped silently inside sendOnce. Move newSessionMessenger
above startLifecycle and pass the real messenger through, so CI-failure,
review-feedback, and merge-conflict nudges actually reach the agent.

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

* feat(project): populate RepoOriginURL at add + lazy observer backfill (#108)

project.Add now shells out to `git -C path remote get-url origin` and
captures the result on the new project row, so the SCM observer can parse
it on the first poll. A missing remote falls back to "" rather than failing
project add — non-git roots and remoteless repos stay registerable.

To cover projects added before this change, the observer's discoverSubjects
lazily backfills RepoOriginURL via the same shell-out and persists it
through UpsertProject, so subsequent polls skip the fork-exec.

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

* feat(lifecycle): persist reaction-dedup signatures across restart (#108)

Add migration 0005 with `pr.last_nudge_signature TEXT NOT NULL DEFAULT ''`
and two scoped sqlc queries (Get/UpdatePRLastNudgeSignature). Lifecycle
serialises the per-PR slice of its seen/attempts maps to that column as a
small JSON document; sendOnce loads it lazily on first touch of each PR
and persists after every successful send.

This closes the post-restart re-nudge gap: the daemon used to lose the
seen map on bounce, so a still-failing CI re-prompted the agent on the
first post-restart observer poll even when it had already been told.

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

* fix(lifecycle): silence nilerr on intentional corrupt-payload swallow

golangci-lint's nilerr flagged the `if err := json.Unmarshal(...); err != nil { return nil }`
path in loadPRSignaturesLocked. The swallow is deliberate (a corrupt persisted
payload should not crash the lifecycle write path), so compare against nil
directly so no `err` is bound and the lint goes quiet.

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

* fix: silence nilerr, address reviewer notes, drop task-tagged comments

- reactions.go: discard the json.Unmarshal error explicitly via `_ =` so
  golangci-lint's nilerr stops flagging the intentional corrupt-payload
  swallow; behavior unchanged.
- reactions.go: document the Send → memory → persist order in sendOnce so
  the "one extra nudge on restart after a transient persist failure"
  trade-off is explicit (vs. the inverse risk of losing a real nudge).
- service.go: stop reaching for slog.Default() in resolveGitOriginURL;
  align with the observer's identical helper that just returns "" on git
  failure rather than logging through the global logger.
- tests: drop "issue #108" / "guards the regression from #X" framing in
  test docstrings — explain WHAT the test asserts, not the PR context.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 23:20:02 +05:30
Adil Shaikh 19b6ca5093
feat: add provider-neutral SCM observer (#76)
* feat: add provider-neutral scm observer

* fix: satisfy scm batch query lint

* fix: avoid scm token preflight on startup

* fix: bound github scm review refresh

* docs: clarify scm observer fields

* fix: gate scm observer credentials lazily

* fix: preserve scm review threads in legacy pr writes

* fix: harden scm observer state refresh

* fix: retry scm lifecycle after persistence

* fix: persist scm lifecycle acknowledgement cursor

* fix: tune scm graphql pagination

* chore: remove scm observer no-op code

* fix: tighten scm bot and log-tail handling

* fix(scm): emit CDC events for pr_review_threads

* fix(db): renumber scm observer migration

* fix: preserve cdc triggers during scm migration

* fix: preserve scm comments across legacy writes

* fix: retry transient scm credential checks

* fix: preserve review rows during lifecycle ack

* fix: harden scm observer review follow-ups

---------

Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
2026-06-04 22:26:07 +05:30
neversettle 7880f59cf5
refactor(backend): LLD maintainability fixes in controllers/service layers (#95) (#96)
* 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>
2026-06-03 18:09:02 +05:30
yyovil e25b2ad4de
feat(agent): opencode adapter + activity plugin hooks (#80)
* feat(agent): add opencode adapter + activity plugin hooks

Add an opencode (sst/opencode) agent adapter implementing the 6-method
ports.Agent interface and register it in the daemon's agent resolver, so a
session with harness "opencode" spawns and restores a real opencode worker.

opencode diverges from the claude-code/codex adapters in two ways the adapter
bridges:

- No native command-hook config. Unlike Claude Code (.claude/settings.local.json)
  and Codex (.codex/hooks.json), opencode has no "run this command on event"
  config (see sst/opencode#5409). Its only lifecycle surface is a JS/TS plugin
  loaded from .opencode/plugins/. GetAgentHooks therefore //go:embeds an
  AO-owned plugin (assets/ao-activity.ts) and writes it atomically; install is an
  idempotent overwrite and uninstall is a sentinel-guarded delete, so
  user-authored plugins are never touched. The plugin maps opencode events onto
  AO's three normalized activity events: session.created -> session-start,
  message.updated/message.part.updated -> user-prompt-submit, and
  session.status(idle) -> stop (NOT the deprecated session.idle, which is
  unreliable under `opencode run`). It shells `ao hooks opencode <event>` via a
  guarded sh -c so a missing `ao` binary is a silent no-op.

- A single approval flag. opencode exposes only --dangerously-skip-permissions
  (no graduated accept-edits/auto) and no system-prompt flag, so those map to a
  bypass-only permission flag and a documented no-op for the system prompt
  (deferred to opencode's own config).

Launch uses the interactive TUI (`opencode --prompt <p>`); restore continues a
captured native session via `opencode --session <id>`.

opencode_test.go mirrors codex_test.go (12 tests) and the daemon wiring test now
asserts the opencode harness resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent): address Greptile review on opencode adapter (#80)

- Dispatch all opencode plugin hooks synchronously (Bun.spawnSync). The
  session.created handler previously fired session-start via an async
  Bun.spawn; if opencode does not await the event handler, a following
  message.updated -> user-prompt-submit (sync) could complete before the
  in-flight async session-start, so AO would see the prompt before the session
  was registered. A sync spawn blocks opencode's single-threaded event loop, so
  events are now reported strictly in dispatch order. Removes the now-unused
  async callHook helper.
- Fix package/doc comments that said .opencode/plugin/ (singular) to match the
  plural .opencode/plugins/ the adapter actually writes to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent): surface opencode hook failures instead of swallowing them

The activity plugin previously discarded every failure: callHookSync ignored
the subprocess exit code/stderr and both catch blocks were empty, so a failing
`ao hooks` invocation or a malformed event payload was completely invisible.

Now failures are reported through opencode's structured logger (client.app.log)
while still never crashing opencode:
- callHookSync pipes stderr and checks result.success; a non-zero exit (a real
  `ao hooks` failure — the `command -v ao` guard makes a missing binary exit 0)
  is logged with its exit code and stderr.
- spawn exceptions (e.g. no `sh`) are caught and logged.
- the event-handler catch logs the offending event type instead of swallowing.
- logHookFailure is itself best-effort (optional-chained, rejection swallowed),
  so logging can never throw back into opencode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent): address opencode adapter review — install guard, prompt dedup, hook timeout

Maintainer review on #80 surfaced three pre-merge issues:

- GetAgentHooks could clobber a user file: install overwrote
  .opencode/plugins/ao-activity.ts unconditionally while uninstall was
  sentinel-guarded. Install now refuses (loud error) to overwrite a file that
  isn't AO-managed; absent/AO-managed targets still write idempotently.

- Empty-prompt report poisoned the dedup: message.updated fired
  user-prompt-submit with an empty prompt AND marked the message seen, so the
  text from the following message.part.updated was deduped away and never
  reached AO — breaking title-from-prompt. reportUserPrompt now reports at most
  twice: an optional early empty report (keeps run-mode flows active) that does
  NOT block a later text report, and a text report that is terminal.

- Bun.spawnSync had no timeout, so a hung `ao hooks` could block opencode
  indefinitely. Each spawn is now time-boxed at 30s, matching the claude-code
  and codex hook timeouts.

Adds TestGetAgentHooksRefusesToClobberForeignFile and a spawn-timeout assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: harden opencode activity hooks

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
2026-06-03 17:06:32 +05:30
Harshit Singh Bhandari 210c9df758
feat(cli): enrich ao doctor (#90) (#99)
* feat(cli): enrich ao doctor

* fix(cli): address doctor review feedback
2026-06-03 16:50:54 +05:30
Harshit Singh Bhandari bab0d2d167
feat: add light backend CLI commands (#98) 2026-06-03 16:18:00 +05:30
Harshit Singh Bhandari 010b422bb5
feat(cli): add ao session ls/get/kill/restore (#90) (#92)
* feat(cli): add session commands

* test(cli): cover session json output

* chore(cli): trim unused session response fields
2026-06-03 04:50:45 +05:30
Harshit Singh Bhandari ae9fa0e341
feat(cli): add ao project ls/get/rm (#90) (#91)
* feat(cli): add project ls get rm

* fix(cli): satisfy project confirmation lint

* chore(ci): remove agent-ci dockerfile

* test(cli): cover project json output

* fix(cli): label project agent as default harness
2026-06-03 04:46:55 +05:30
neversettle fab5451a9f
feat(api): PR action routes — merge + resolve-comments (#88)
* feat(api): register PR action route shells (merge + resolve-comments)

Adds two 501 Not Implemented route shells for the SCM/PR action lane
as specified in issue #21. No business logic — the routes are stubs
that return a structured planned body with the embedded OpenAPI spec
slice, consistent with the existing route-shell pattern.

Routes registered:
  POST /api/v1/prs/{id}/merge
  POST /api/v1/prs/{id}/resolve-comments

Closes part of #18.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(api): PR action routes — full impl (merge + resolve-comments)

Builds the two SCM/PR action routes end-to-end per issue #21:

  POST /api/v1/prs/{id}/merge
  POST /api/v1/prs/{id}/resolve-comments

**ports/scm.go** — new PRService interface, MergeResult, ResolveResult.

**adapters/scm/github** — adds ErrNotMergeable/ErrUnprocessable sentinels
to the client (405/409/422 classification) and MergePR / ListUnresolvedThreadIDs /
ResolveThread methods to the Provider.

**internal/scm/pr_service.go** — concrete PRService over PRProvider. Parses
the path ID as a PR number, calls the provider, maps github sentinel errors to
domain errors (ErrPRNotFound / ErrPRNotMergeable / ErrPRPreconditions /
ErrNothingToResolve). Nil PRService keeps routes registered but returns
OpenAPI-backed 501s.

**httpd/controllers/prs.go** — real handlers; writePRError maps the four domain
errors to 404 / 409 / 422 / 500.

**prs_test.go** — httptest coverage: 501 (nil service), 200/404/409/422 for
both routes, spec-slice present in 501 body.

**scm/pr_service_test.go** — table-driven unit tests with a fake PRProvider.

Closes part of #18. Closes #21.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(api): PR action routes — merge + resolve-comments (#21)

Implements POST /api/v1/prs/{id}/merge and POST /api/v1/prs/{id}/resolve-comments.

Service logic lives in internal/service/pr (ActionManager interface + ActionService
struct). Controllers use the projects pattern — import the service package directly
rather than going through a ports interface. Drops the internal/scm intermediary
package and the ports/scm.go file added in earlier iterations.

Also fixes the ContentLength-based body-decode guard in resolveComments, which
silently dropped JSON bodies sent with chunked transfer encoding; now decodes
unconditionally and treats io.EOF as an absent body.

Closes #21.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(specgen): remove dead path-param entries from schemaNames

ControllersProjectIDParam, ControllersSessionIDParam, and ControllersPRIDParam
are never matched by the schemaName interceptor — swaggest reflects path-param
structs inline rather than as $ref component schemas, so the hook is never
called for these types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(pr): anchor resolve-comments to stated PR when explicit IDs supplied

When commentIDs were provided, the parsed PR number was never used — any
thread ID could be resolved regardless of which PR was in the URL path.
Add a ListUnresolvedThreadIDs existence probe in the else branch so the PR
must be reachable before iterating the caller-supplied IDs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(controllers): exclude io.ErrUnexpectedEOF from isEmptyBody

A truncated body (e.g. {"commentIds":["T_1") returns io.ErrUnexpectedEOF,
not io.EOF. Treating it as an absent body caused the handler to fall through
to "resolve all unresolved threads" instead of returning 400. Only io.EOF
(reader returned no bytes) is a genuine empty-body signal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(api): revert to route shell — stubs, no adapter changes

- Remove adapter changes (ErrNotMergeable/ErrUnprocessable from client.go,
  MergePR/ListUnresolvedThreadIDs/ResolveThread from provider.go)
- ActionService returns dummy values with TODO; no business logic
- Errors (ErrPRNotFound etc.) moved to controllers/errors.go
- PR DTOs moved to controllers/dto.go
- Remove 501 guards — stub service always wired via NewAPI default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: restore client.go, move PR errors to service/pr, fix lint

- Restore original client.go alignment (no functional change)
- Move PR sentinel errors to service/pr/errors.go
- controllers/errors.go now only contains writePRError, referencing prsvc sentinels
- Fix schemaNames alignment in specgen/build.go (goimports lint)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(api): replace fake-success stub with 503 when SCM not configured

The nil-service fallback was silently injecting a stub that returned
fake merge/resolve success, misleading callers when no SCM is wired.
Remove the injection; nil Svc now returns 503 SCM_NOT_CONFIGURED.
Also inline writePRError into prs.go and delete controllers/errors.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(specgen): mark resolve-comments request body as optional

reqBody: nil removes the requestBody.required: true annotation so
generated SDK clients can omit the body (which resolves all threads).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(prs): align nil-service guard with spec (501) and echo prID in stub

Use apispec.NotImplemented (501) instead of 503 so nil-service responses
match the OpenAPI spec and generated clients hit the documented 501 branch.

Echo prID as PRNumber in the stub Merge to avoid claiming the wrong PR
was merged if NewActionService is wired by accident before real impl lands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 04:43:50 +05:30
Harshit Singh Bhandari 9058017439
fix: prefix ao send messages with sender session (#85)
* fix: prefix ao send messages with sender session

* feat: add orchestrator-aware spawn prompts

* fix(session): return first active orchestrator
2026-06-02 21:39:21 +05:30
Harshit Singh Bhandari 5435246c9a
feat(cli): add minimal ao send (#83)
* 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
2026-06-02 20:02:47 +05:30
yyovil 57bb63701d
Add `ao spawn` + `ao project add` (spawn a real worker end-to-end) (#77)
* 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>
2026-06-02 18:39:13 +05:30
yyovil 3346c6cb6c
Add agent adapters and wire per-session agents into the session manager (#65)
* 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>
2026-06-02 16:51:32 +05:30
neversettle 3a93e33331
refactor: move project manager to service layer (#68)
* refactor(project): manager talks to the sqlite store; drop the in-memory store

The project Manager now runs only against the durable backend store: remove the
process-local MemoryStore (and NewMemoryManager), and require a real Store. The
daemon already wires the sqlite store; tests now build a real temp-dir sqlite
store instead of the mock.

- Move Row + the Store port to project/store.go. The Store interface stays
  because it is the dependency-inversion port that lets the manager reach the
  backend without an import cycle (storage imports project.Row), not an extra
  mock layer — there is no longer any in-memory implementation.
- NewManager requires a non-nil Store (no in-memory fallback).
- Add project/manager_test.go: List/Add/Get/Remove happy paths +
  PATH_REQUIRED/NOT_A_GIT_REPO/PATH_ALREADY_REGISTERED/ID_ALREADY_REGISTERED,
  PROJECT_NOT_FOUND/INVALID_PROJECT_ID, and UpdateConfig — all against a real
  sqlite store (the service-logic tests #47 lacked).

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

* refactor(project): trim routes, consolidate package, add code-first OpenAPI

- Remove POST /reload, PATCH /{id}, POST /{id}/repair routes and their
  Manager methods (Reload, UpdateConfig, Repair) and DTOs (ReloadResult,
  UpdateConfigInput) — not needed at this stage
- Merge Manager interface into manager.go; delete project.go (single-impl
  split served no purpose)
- Remove dead notImplemented helper from errors.go
- Port PR #59 code-first OpenAPI generation: controllers/dto.go named
  response types, specgen/build.go (4 routes), parity + drift tests,
  cmd/genspec, go generate wiring; regenerate openapi.yaml
- Add swaggest deps; add YAML() method to apispec.Spec

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(project): address PR review comments

- t.Skipf → t.Fatalf in gitRepo helper: git failures now hard-fail
  instead of silently skipping manager tests on a misconfigured runner
- FindProjectByPath: add AND archived_at IS NULL so archived paths don't
  permanently block re-registration (update queries/projects.sql and
  generated gen/projects.sql.go)
- Add TestManager_ReaddAfterRemove to lock the fix

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fixed lint and fmt

* addressed greptile comments

* Apply suggestions from code review

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* project tests fix

* project_tests fix

* fix: Linting and formatting fix

* refactor: move project manager into service layer (#68)

* refactor: split service package by resource (#68)

* fix: ignore archived project id conflicts (#68)

* refactor: move pr manager into service layer (#68)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
2026-06-02 01:26:48 +05:30
prateek 424e6e824b
refactor: move session status assembly to service (#62) (#67)
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
2026-06-01 23:31:21 +05:30
Harshit Singh Bhandari f9b08aada4
feat(scm): GitHub provider adapter — Observe(prURL) → PRObservation (#69)
* feat(scm): GitHub provider adapter — Observe(prURL) → PRObservation

A fresh GitHub SCM provider adapter under
backend/internal/adapters/scm/github/ exposing one method:

  (*Provider).Observe(ctx, prURL) (ports.PRObservation, error)

It performs a REST GET on /repos/{o}/{r}/pulls/{n} for the authoritative
draft/merged/closed/head-SHA, one GraphQL query for the reviewDecision +
mergeStateStatus + statusCheckRollup + unresolved review threads, and
(only for failure-class CheckRuns) a REST GET on
/actions/jobs/{job_id}/logs to splice the last 20 lines of the failed
job into the observation.

The package is the observation primitive; the polling loop, cadence
selection, daemon wiring, persistence and webhook receiver are all
intentionally out of scope (separate PRs / lanes).

Closes #27 — this supersedes PR #28's attempt, which targeted types
(domain.SCMProvider / SCMSnapshot / ports.SCMObserveRequest) that the
PR #62 simplification refactor has since removed. The GraphQL queries
and mergeability composition logic are credited to @whoisasx from
PR #28's provider.go; the package was re-implemented against the
current ports.PRObservation seam (post-#62) rather than rebased.

Bot-author detection uses ONLY GitHub's typed signal (__typename
"Bot" / User.Type "Bot"). The strings.Contains(login, "bot") fallback
from PR #28 was intentionally dropped — aa-18's review flagged it as
a false-positive magnet for logins like "robothon" / "lambot123".

46 table-driven tests against httptest.NewServer cover happy path,
draft, merged, closed (not merged), CI passing/failing/pending,
StatusContext legacy, log-tail extraction (and the best-effort
log-fetch failure case), mergeability mergeable/conflicting/blocked
(including ci-failing → blocked even when GitHub still says CLEAN —
the load-bearing aa-18 contract)/unstable/unknown, review
approved/changes-requested/required/none, bot-author filtering
(including the robothon false-positive guard), unresolved-only
threads, all-bots → empty Comments, ETag-304 cache hit, primary +
secondary rate-limit (with errors.As → *RateLimitError), 401 →
ErrAuthFailed, malformed JSON → Fetched:false, network error →
Fetched:false, Authorization Bearer header injection,
StaticTokenSource blank/whitespace rejection, GHTokenSource memoize
+ invalidate.

Verification:
- go build ./...               clean
- go vet ./...                 clean
- gofmt -l backend/internal/adapters/scm/   clean
- golangci-lint run ./... (v2.12, repo .golangci.yml)   0 issues
- go test -race ./internal/adapters/scm/github/...      46/46 PASS

References:
- aa-18 review of PR #28: ~/.ao/agent-reports/aa-18.md
- aa-26 tracker adapter (sibling Go-adapter pattern): #36 / agent-reports/aa-26.md

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

* fix(scm): address greptile review on #69

Four fixes from the greptile review of PR #69:

1. CI rollup pagination (P1) — when GraphQL reports
   pageInfo.hasNextPage=true for the statusCheckRollup contexts, a
   visible "all passing" set could be hiding a failing context on the
   next page. ciSummaryFromGraphQL now degrades Passing / Pending /
   Unknown to CIUnknown in that case; a known CIFailing on the visible
   page is still safe and is NOT degraded. Also bumped the per-page
   limit from 50 to 100 (GraphQL's documented max for the contexts
   connection). Two new tests pin both branches.

2. Empty GraphQL inline fragment (P2) — dropped
   `... on User { }` from the reviewThreads author selection. The
   empty selection set was technically invalid GraphQL and a future
   API tightening could reject the query. __typename already tells us
   whether the actor is a Bot, so the fragment carried no information.

3. rest.MergeStateStatus dead-code (P2) — the field decoded from the
   non-existent REST `merge_state_status` was always empty, making the
   firstNonEmpty fallback dead code. Removed the field and switched
   the tiebreaker to rest.MergeableState (the actual REST field, upper-
   cased so the same switch covers both GraphQL and REST shapes).

4. Wrong Accept header on /actions/jobs/{id}/logs (P2) — GitHub's
   REST API validates the Accept header before issuing the 302 to the
   log blob; sending text/plain risks a 406. Switched to the canonical
   application/vnd.github+json; the redirected blob serves text/plain
   regardless.

Verification:
- go build ./...               clean
- go vet ./...                 clean
- golangci-lint run ./...      0 issues
- go test -race ./internal/adapters/scm/github/...   48 / 48 PASS

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 21:44:56 +05:30
prateek c8f6050539
refactor: remove activity source tracking (#62) (#66)
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
2026-06-01 09:26:18 +05:30
prateek a34094e7d8
refactor: simplify session lifecycle and zellij runtime (#62)
* refactor: remove canonical lifecycle state

* refactor: move sqlite stores into subpackage (#62)

* refactor: strengthen sqlite generated model types (#62)

* refactor: remove lifecycle notifications (#62)

* docs: remove notification cleanup leftovers (#62)

* refactor: narrow lifecycle manager scope (#62)

* refactor: keep PR nudges in lifecycle (#62)

* refactor: trim unused storage and lifecycle contracts (#62)

* refactor: align storage and runtime observation surfaces (#62)

* refactor: remove stale daemon and adapter bloat (#62)

* test: fix terminal ring race assertion (#62)

* refactor: trim lifecycle and http boilerplate (#62)

* refactor: expose sqlite CDC source directly (#62)

* refactor: share process liveness checks (#62)

* test: trim lifecycle store fake surface (#62)

* refactor: separate PR observations from storage rows (#62)

* refactor: trim remaining cleanup surfaces (#62)

* refactor: narrow observation and PR display APIs (#62)

* refactor: move PR write DTOs out of domain (#62)

* refactor: normalize PR domain storage types (#62)

* refactor: remove unused session port interface (#62)

* fix: reject unexpected CLI arguments (#62)

* refactor: use session metadata for spawn completion (#62)

* refactor: narrow session runtime dependency (#62)

* fix: validate zellij version in doctor (#62)

* refactor: split observation port DTOs (#62)

* chore: add sqlc generation script (#62)

* refactor: clarify terminal mux naming (#62)

* fix: tolerate nil loggers (#62)

---------

Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
2026-06-01 08:42:49 +05:30
prateek 8df074b1c9 chore(backend): add golangci-lint with a strong ruleset and clear the tree
Introduces backend/.golangci.yml (27 linters across correctness, dead-code/
boilerplate, style, and security), wires it into CI as a blocking job, and
fixes every finding so the tree starts at zero.

Config:
- 27 linters: errcheck, govet, staticcheck, errorlint, bodyclose,
  sqlclosecheck, rowserrcheck, nilerr, makezero, unused, unparam, unconvert,
  wastedassign, copyloopvar, prealloc, dupl, revive (incl. exported-symbol doc
  comments), gocritic, misspell, usestdlibvars, predeclared, nakedret, gosec, …
- Tuned for signal over noise: govet/shadow and gocritic hugeParam/rangeValCopy/
  unnamedResult disabled (idiomatic-Go false positives); sqlc-generated code and
  tests get scoped exclusions; gosec G304 excluded (paths are config/run-file/
  worktree-derived, not user input); nilerr excluded in cli/status.go (probe
  failures are the reported status, not a command error).

CI:
- New blocking lint job (golangci-lint-action, latest binary for Go-version
  compatibility).
- go-version now read from go.mod (was pinned 1.22 while go.mod declares 1.25).

Cleanup to reach zero (no behavior change):
- errcheck: wrap deferred/inline Close()/Remove()/Rollback() with `_ =`.
- gosec: tighten dir/file perms (0755->0750, 0644->0600).
- unparam: drop always-nil error return from startLifecycle; drop unused
  shellPath param (zellij PowerShell) and always-500 fallbackStatus param
  (writeProjectError).
- gocritic: regexp \d, s != "", switch->if, combined appends.
- revive: doc comments on all exported symbols; rename project.ProjectRow ->
  project.Row (stutter); rename `max` locals shadowing the builtin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 04:58:41 +05:30
prateek cb2a00a0c2
Revert "feat: add notifier delivery runtime" 2026-06-01 04:46:38 +05:30
Adil Shaikh d06c0ce8c2
Merge pull request #58 from aoagents/feat/55
feat: add notifier delivery runtime
2026-06-01 04:26:41 +05:30
whoisasx 5c07e818a1 fix: preserve notification surface disables 2026-06-01 03:49:31 +05:30
whoisasx f0c57ac2e2 docs: clarify notification routing migration 2026-06-01 03:47:53 +05:30
whoisasx d39e8e0da3 fix: address notifier review cleanup 2026-06-01 03:47:44 +05:30
whoisasx 041c8c8f7f fix: harden notification delivery leases 2026-06-01 03:47:39 +05:30
whoisasx d4622fe223 fix: address notifier delivery review feedback 2026-06-01 03:47:39 +05:30
whoisasx 217f6b1652 feat: add notifier delivery runtime 2026-06-01 03:47:19 +05:30
prateek 42eab57d49 refactor(storage): add compile-time port guards on *Store
Re-add the blank-identifier interface assertions lost when wiring.Adapter was
collapsed: *Store now directly satisfies ports.SessionStore and ports.PRWriter,
so prove it at the point of definition. Drift between either port and the
implementation now fails here instead of at the call sites in lifecycle_wiring
or tests.

Addresses greptile review comment on #60.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 03:40:33 +05:30
prateek 28e1205d28 refactor(backend): collapse duplicate PR row types into one domain definition
Each PR-child table (pr / pr_checks / pr_comment) had three near-identical
structs — gen.* (generated), sqlite.*Row, and ports.* — with wiring.Adapter
copying field-by-field between them. Collapse to one shared definition per
table in domain (PRRow / PRCheckRow / PRComment), used by both the PRWriter
port and the sqlite store; gen.* stays sealed inside the storage layer.

- *sqlite.Store now satisfies ports.SessionStore + ports.PRWriter directly,
  so the entire wiring.Adapter package is deleted (lifecycle.New(store, store)).
- The bool PR state <-> single state column, int<->int64, and enum-default
  translation now lives only at the gen<->domain boundary in pr_store.go.
- WritePRObservation renamed WritePR to match the port; the integration test
  and composition root drop their adapter copies.

Net -280 lines, behaviour unchanged. go test -race ./... green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 03:40:33 +05:30
itrytoohard c0bf99eb22 test(cli): port the E2E suite to cross-platform Go; slim the Docker harness
Replaces the growing bash smoke test with a Go os/exec suite behind the `e2e`
build tag (backend/internal/cli/e2e_test.go). It builds the real binary and
drives start/status/doctor/stop + the daemon-control HTTP surface against
isolated state (temp dir + OS-assigned free port), and now runs natively on
ubuntu + macOS + WINDOWS in CI — finally covering the Windows
CREATE_NEW_PROCESS_GROUP detach path and per-OS os.UserConfigDir resolution
that a Linux container can't observe. `go test -tags e2e -v` logs every command
and its output, replacing the bash -v flag.

- backend/internal/cli/e2e_test.go: 8 table-style TestE2E_* cases; strips any
  inherited AO_* env so a real daemon's AO_PORT can't leak in.
- test/cli/install-check.sh: small, linear fresh-install proof the Dockerfile
  runs (binary on PATH, no toolchain) — kept as the hardening tier.
- test/cli/Dockerfile: run install-check.sh instead of the full bash suite.
- .github/workflows/cli-e2e.yml: `native` is now a go test matrix over
  ubuntu+macos+windows; `container` builds the image and runs it with --init.
- Removes test/cli/smoke.sh and test/cli/run-local.sh (superseded by `go test`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 03:14:08 +05:30
itrytoohard e6661e3f3b test(cli): pin ExitCode mapping (usage=2, runtime=1, nil=0)
Closes the one nit from the regression audit: the exit-code wiring was correct
and covered end-to-end by the smoke test, but not pinned by a unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 02:38:25 +05:30
itrytoohard 2d00e4675d fix(cli): harden daemon control surface and stop CLI from writing the store
Addresses review findings on PR #53 (on top of the rebase onto main).

- doctor: stop opening/migrating SQLite. The daemon is the sole store
  writer/migrator (architecture.md §7); the CLI must not run migrations or
  open a second writer against a DB a live daemon owns. doctor now reports
  database-file presence and gains --json.
- stop: only remove running.json when it still belongs to the PID we
  stopped, so a concurrent `ao start` that wrote a new run-file is not
  clobbered into looking stopped.
- httpd: gate POST /shutdown to loopback callers with no Origin header,
  closing the CSRF / DNS-rebinding vector against an unauthenticated,
  state-changing endpoint.
- start: detach the spawned daemon into its own session/process group so a
  Ctrl-C while `ao start` waits for readiness doesn't also kill it.
- cli: exit 2 for usage errors (bad flag / arg count) vs 1 for runtime
  failures.
- daemon: unexport newLogger (only used in-package).
- tests: /shutdown guard (cross-origin + rebinding) and stop run-file
  ownership guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 01:55:14 +05:30
harshitsinghbhandari 0d8ffcd17a fix(httpd): update server_test for termMgr arg after rebase
The shutdown endpoint test was authored against the pre-rebase
httpd.New(cfg, log) signature. After rebasing onto main, the terminal
manager (from #50) made termMgr a required third arg. Pass nil — the
test exercises /shutdown, not /mux, so the terminal surface stays off.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:50:43 +05:30
Dhruv Sharma 2f4662b470 fix(cli): handle stale start and graceful shutdown 2026-06-01 01:45:59 +05:30
Dhruv Sharma a614462d38 fix(cli): preserve live daemon state on probe failures 2026-06-01 01:45:04 +05:30
Dhruv Sharma 925e70763d fix(cli): verify daemon ownership before stop signal 2026-06-01 01:45:04 +05:30
Dhruv Sharma f72facb9e5 fix(cli): address greptile review comments 2026-06-01 01:44:43 +05:30
Dhruv Sharma 4671d27307 feat(backend): add cobra cli foundation 2026-06-01 01:44:43 +05:30
whoisasx 5303c51d29 feat: add durable notification foundation 2026-06-01 00:07:55 +05:30
Harshit Singh Bhandari f8611decc0
Merge pull request #52 from aoagents/feat/wire-session-manager
feat(backend): wire Session Manager into the daemon (real tmux + gitworktree, stub Agent)
2026-05-31 23:48:15 +05:30
Pritom Mazumdar 11b602b186
Merge pull request #50 from aoagents/feat/terminal-streaming
feat(backend): PTY-attach terminal streaming over /mux WebSocket
2026-05-31 23:46:00 +05:30
Pritom14 eda39a156a fix(terminal): guard subscribe-to-assign window in openTerminal
A session can exit and run onExit (which deletes c.terms[id]) in the gap
between subscribe returning exited=false and openTerminal assigning
c.terms[id]. The delete is a no-op there since the key isn't set yet, so
the later assign resurrects a stale entry for a dead pane, trapping every
future open for that id on the connection. Re-apply the delete after the
assign when onExit fired in the window, tracked by a c.mu-guarded flag.

Add a stress regression test that races the exit against the assign.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 23:42:59 +05:30
Pritom14 4f77062aed fix(terminal): clear conn entry before sending exited frame
The exit callback enqueued the exited frame before deleting c.terms[id],
so a client reopening on receipt of exited could hit the open guard while
the entry was still set and have its open dropped. Delete first so the
cleared entry is visible by the time the client sees exited.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 23:09:16 +05:30
Pritom14 a766a80f76 fix(terminal): keep open re-servable after a pane exits
Opening a terminal whose session has exited left c.terms[id] set to a
no-op (already-exited path) or to a never-cleared unsubscribe (exit after
open), so the open guard silently dropped every later open for that id on
the connection until close/reconnect. Clients also saw exited/data before
the opened ack.

Ack opened before subscribe so it always precedes replay/data/exited;
have subscribe report whether the pane was already terminal and skip
registering in that case; and clear the connection entry from the exit
callback for panes that exit after open.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 23:04:15 +05:30
Pritom Mazumdar 438b830b40
Merge pull request #49 from aoagents/session/aa-31
test(integration): LCM+SM live-fire against real SQLite store
2026-05-31 21:21:51 +05:30
Pritom14 3ce8115e6d Merge remote-tracking branch 'origin/main' into feat/terminal-streaming
# Conflicts:
#	backend/internal/httpd/router.go
2026-05-31 20:50:26 +05:30
Pritom14 67f42150b4 fix(terminal): make creackPTY.Close idempotent to avoid shutdown deadlock
The session run loop closes the PTY after copyOut returns, and session.close
(via Manager.Close) closes the same PTY again. creackPTY.Close called cmd.Wait
each time, and a second concurrent Wait on the same process blocks forever, so
daemon shutdown deadlocked whenever a terminal was still attached. fakePTY is
idempotent via sync.Once, so the unit suite never exercised this; a real tmux
attach surfaced it.

Guard close+kill+wait with a sync.Once so Wait runs exactly once. Add a
regression test that double-closes a real PTY under a watchdog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 20:46:12 +05:30
Vaibhaav Tiwari 9a10eacc39
feat(api): implement project routes with mock manager/store (#47)
* feat(backend): HTTP daemon skeleton — config, health, runfile, graceful shutdown (#10)

Phase 1a of the Go HTTP daemon lane (#10). Stands up the loopback-only
sidecar skeleton the later REST/SSE/WS/static surfaces build on:

- config: env-driven (AO_HOST/PORT/ENV/timeouts/run-file) with zero-config
  defaults; binds 127.0.0.1:3001; validates and fails fast on bad input.
- httpd: chi router with the recoverer → request-id → logger → real-ip
  middleware stack and /healthz + /readyz probes. Per-request timeout is
  carried in config but intentionally not global — it scopes to /api/v1 in
  Phase 1b so it never throttles SSE/WS/health.
- runfile: atomic PID + port handshake (running.json) for the Electron
  supervisor, with a dead-PID stale check so a crashed predecessor doesn't
  block startup while a live one fails fast.
- server: bind-before-publish (port conflict fails fast), graceful shutdown
  on SIGINT/SIGTERM via signal.NotifyContext with a 10s hard timeout, and
  run-file cleanup on exit.

Why: the daemon must be safely supervisable as a child process — the
supervisor needs a discoverable PID/port and the daemon must not leave a
half-started process or stale handshake behind. Locking the lifecycle down
now keeps the future port split a small change rather than a rewrite.

Tests cover config defaults/overrides/validation, run-file round-trip and
live/dead PID detection, health probes, full Run lifecycle, and port-conflict
fail-fast.

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

* refactor(backend): drop Env config field — not needed yet (#10)

Per review on #14: AO_ENV / Config.Env / IsProduction() weren't load-bearing
for Phase 1a — they only switched the slog handler. Removing them now keeps
the surface minimal; the env knob can come back later when a real consumer
needs it.

- config: remove Env field, AO_ENV parsing, and IsProduction helper.
- main: collapse newLogger to a single text-handler path.
- httpd: drop the env field from the listening log line.
- tests: drop the env assertions and AO_ENV fixture.

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

* docs: add backend run + config quick-start to README (#10)

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

* fix(backend): address Phase 1a review comments (#10)

- config: drop AO_HOST entirely — the daemon is loopback-only by design,
  so making the bind host env-configurable was a security footgun
- config: use net.JoinHostPort in Addr() so IPv6 literals stay valid
- config: reject zero/negative AO_REQUEST_TIMEOUT and AO_SHUTDOWN_TIMEOUT
  (time.ParseDuration accepts both; either would silently break the
  daemon — instant request expiry / no graceful drain)
- runfile: split processAlive into unix/windows build-tagged files so
  liveness detection is reliable on both platforms (Windows uses
  OpenProcess; POSIX keeps signal 0)
- runfile: document os.Rename overwrite semantics (atomic on POSIX,
  REPLACE_EXISTING on Windows) so the temp-then-rename pattern's
  cross-platform behaviour is explicit
- httpd tests: give probe/waitForHealth clients an explicit per-request
  timeout so a stalled connect can't hang the test on the outer deadline

* fix(backend): strip trailing blank line from runfile.go (#10)

gofmt CI was failing because removing the orphan processAlive doc
comment left an extra newline at EOF.

* fix(backend): cross-platform run-file replace + AO_HOST rationale (#10)

- runfile: introduce build-tagged atomicReplace — POSIX rename(2) on
  Unix, MoveFileEx with MOVEFILE_REPLACE_EXISTING on Windows. The Go
  runtime happens to do the Windows call internally already, but
  invoking it directly makes the cross-platform contract explicit
  instead of a runtime implementation detail
- runfile: tighten process_unix.go build tag from `!windows` to `unix`
  so plan9/js/wasm fail to build rather than silently using a broken
  signal-0 probe
- runfile: add TestWriteOverwritesExisting covering the stale run-file
  replace path that none of the previous tests exercised
- config: anchor the loopback-only decision in the LoopbackHost doc so
  the next contributor doesn't reintroduce AO_HOST without the security
  rationale

* fix(backend): route chi access logs through slog/stderr (#10)

chi's middleware.Logger writes via stdlib log to stdout, but the
daemon's slog logger writes to stderr — so REST traffic and daemon
logs landed on different streams in different formats. Replace it
with a small slog-backed requestLogger that:

- Wraps the response writer via middleware.NewWrapResponseWriter so
  status/bytes are accurate even when handlers return without an
  explicit WriteHeader.
- Reads the request id off the context set by middleware.RequestID
  (kept mounted just before this middleware so the id is available).
- Emits one structured Info line per request with method, path,
  status, bytes, duration, and remote — same key=value shape as the
  rest of the daemon, one stream for the Electron supervisor to
  capture.

* feat(api): projects route shell (7 routes, REST-corrected) — #20

Mounts the /api/v1 surface on the skeleton router (#10·1a) and registers
the 7 canonical project routes as 501 stubs that emit a structured
PlannedRoute body documenting the future contract. Shared scaffolding
landed here (api.go, errors.go, stubs/, controllers/) so #21/#22 plug in
without re-touching the wiring.

WHY: opens the route-shell PRs in the Go HTTP daemon lane. Doing it
interface-first lets the dashboard team build against the contract
before any handler logic exists; the locked APIError envelope and
PlannedRoute shape become #19's OpenAPI source-of-truth.

REST audit corrections vs the legacy TS surface:
  R3 PUT /projects/:id alias of PATCH: PUT not registered → 405.
  R4 POST /projects/:id repair overload: canonical /repair; legacy 405.
  R5 degraded GET returns 200 with error field: discriminator status.
  R6 ok/success flag flips: drop on 2xx; return affected resource.
  R9 bare {error: msg}: locked {error,code,message,requestId,details?}.

Legacy paths are deliberately NOT registered; each canonical handler
carries PlannedRoute.Legacy so consumers can discover the migration.

Zod schemas (TrackerConfig, SCMConfig, AgentConfig, ReactionConfig,
LocalProjectConfig, RoleAgentConfig) ported to typed Go structs with an
Extra map reserved for .passthrough() round-tripping in later PRs.

Closes part of #18; targets feat/issue-10 until #14 merges.

* refactor(api): collapse ProjectService → ProjectManager — #20

Controllers now depend on ONE inbound interface per resource — ports.ProjectManager —
mirroring the existing ports.SessionManager + LifecycleManager pattern.
Whether the manager impl reaches into the registry, the LCM, an outbound
port, or all three is its own concern; the HTTP layer no longer has to
know any of that.

WHY: the original split named the boundary type "ProjectService" and put
it in a sibling services.go. That implied a second category of port
distinct from inbound.go's *Manager interfaces, even though they play
the same role (things HTTP/CLI call into the core). Per review feedback,
collapse them onto one Manager-per-resource pattern.

Mechanical changes:
- ports/inbound.go gains ProjectManager next to SessionManager.
- ports/services.go renamed to projects.go; keeps only the DTOs the
  ProjectManager methods take/return.
- ProjectsController.Svc renamed to Mgr; APIDeps.Projects type bumped
  to ports.ProjectManager.

All tests pass unchanged; no behavioural change.

* refactor(api): replace stubs/ with OpenAPI-as-source-of-truth — #20

The first cut of the route shell duplicated each route's contract twice:
once as a Go literal (stubs.PlannedRoute{...}) in the controller, and
implicitly in the PR description. The Go literal was ~230 LoC of pure
throwaway that would be deleted in handler-impl PRs.

This commit eliminates the duplication:

  - backend/internal/httpd/apispec/openapi.yaml: full OpenAPI 3.1 doc
    covering the 7 project routes + shared schemas (Project, APIError,
    config types). x-replaces records the legacy → canonical mapping
    REST-audit corrections produced.
  - apispec/apispec.go: //go:embed the YAML, expose Operation(method,
    path) → the spec slice as a map, NotImplemented(w, r, method, path)
    → 501 with that slice embedded as `spec`.
  - controllers/projects.go: each of 7 handlers is now a one-liner:
    apispec.NotImplemented(w, r, "GET", "/api/v1/projects").
  - /api/v1/openapi.yaml serves the embedded document so tooling
    (SDK gen, the validator slated for #19, dashboard dev tools) can
    fetch the whole spec from the same origin as the routes.
  - stubs/ package deleted.

When a real handler lands, only the apispec.NotImplemented line goes
away — nothing else does. The spec stays as documentation; consumers
never had to know it was throwaway. #19 (OpenAPI follow-up) is now
half-folded into this PR; the validation middleware remains its own
follow-up.

Tests reshaped: assert envelope + spec.operationId + spec.x-replaces
(replaces the old planned.legacy assertion); add TestOpenAPIYAMLServed
to cover the static spec serve; add apispec_test.go for embed/lookup
behaviour.

* refactor(api): move projects contract to internal/project package — #20

Pilots the feature-package layout the backend is migrating toward: a
resource's inbound interface and its DTOs live with the resource, not in
a central ports/ catch-all.

WHY: review flagged ports/ as vague. It conflates three jobs — the
outbound capability seam (legit), single-impl inbound interfaces (Go
idiom wants these consumer-side), and DTOs that aren't ports at all.
This moves the projects contract out as the reference shape #21/#22
follow; the merged session/lifecycle/outbound contracts are left
untouched and migrated separately.

Scope: INTERFACE ONLY. No implementation — handlers still answer via
apispec.NotImplemented and the injected project.Manager stays nil. The
impl lands in a later handler-impl PR.

Changes:
- new internal/project: project.go (Manager interface, 7 endpoints) +
  dto.go (AddInput/GetResult/UpdateConfigInput/RemoveResult/ReloadResult,
  moved verbatim from ports/projects.go, Project-prefix dropped).
- ports/projects.go deleted; ProjectManager removed from ports/inbound.go.
  outbound.go and facts.go untouched.
- controllers/projects.go and httpd/api.go depend on project.Manager.

Domain entities (Project, ProjectSummary, DegradedProject, config types)
stay in domain/ as shared vocabulary.

go build/vet/test/gofmt all clean; no behavioural change.

* refactor(api): consolidate project types into internal/project — #20

Addresses PR review: (1) "why are config_types required at the moment?"
and (2) "project objects already defined in project/ — how do we
differentiate?"

Both had the same root cause: project types were split across domain/
and project/. Fix — keep ALL project types in the project package; only
domain.ProjectID (shared with sessions/lifecycle/workspace) stays in
domain.

- domain/project.go → project/types.go: Project, Summary, Degraded
  (renamed from ProjectSummary/DegradedProject; the package name carries
  the "Project" prefix now).
- domain/config_types.go deleted. Kept only the 4 shapes the projects
  API actually exposes — TrackerConfig, SCMConfig, SCMWebhookConfig,
  ReactionConfig — moved into project/types.go. Dropped AgentConfig,
  AgentPermission, RoleAgentConfig, LocalProjectConfig (zero references)
  and the speculative `Extra map[string]any` passthrough fields (no
  marshaller existed, so they silently dropped data — premature).
- project/dto.go + project/project.go reference the local types; ids
  stay domain.ProjectID.

Net: one home for project types, no dead code. go build/vet/test/gofmt
clean; no behavioural change (handlers still 501 via apispec).

* feat(api): implement project routes with mock manager/store

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* merge: resolve conflicts with origin/main

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* refactor(httpd): share JSON/API error envelope helpers

* fix(api): align project mock store with sqlite schema

* fix(api): address project API review semantics

* canonicalize both paths with filepath.EvalSymlinks before comparing

* style(project): gofmt git repo validation

---------

Co-authored-by: Aditi Chauhan <aditi1178@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-31 20:31:22 +05:30
harshitsinghbhandari 7b9a9f5962 fix(backend): drain reaper + cdc poller when startSession fails
If startSession returned an error, run() returned immediately and the
reaper + cdc poller goroutines kept running while defer store.Close()
fired — a data race against the SQLite handle. Mirror the bottom-of-run
shutdown sequence on the error path (cancel ctx, drain reaper, drain
poller) so both goroutines have exited before the store is closed. The
explicit-not-defer ordering is the same the existing
post-srv.Run shutdown block uses; piling on more defers would hit the
LIFO trap the same comment already warns about.

Reported by Greptile on PR #52.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 20:25:56 +05:30
harshitsinghbhandari c1b9e7ec1f chore(backend): name startSession's ctx param for forward use
Renames the unused context.Context parameter from `_` to `ctx` so the
parameter name is already in place when a future plugin constructor
needs to honor cancellation (tmux/gitworktree are synchronous today).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 20:15:46 +05:30
harshitsinghbhandari 27fb82dbeb feat(backend): wire Session Manager into the daemon (real tmux + gitworktree, stub Agent)
Constructs a live *session.Manager in main alongside the LCM, sharing the
exact same SessionStore + LCM dependencies the lifecycle stack already
holds.

Refactor: storeAdapter moves from package main to a new internal
package, wiring.Adapter, so the daemon's composition root and any
in-process integration tests can share a single bridge.

Stubbed for now: ports.Agent has no production adapter on main; a loud
*noopAgent returns sentinel AO_AGENT_HARNESS_NOT_WIRED and logs a
warning once on first call, so a future Spawn through this lane fails
at the runtime layer with a clear breadcrumb rather than starting a
broken session quietly. ports.Notifier and ports.AgentMessenger remain
stubbed alongside the LCM.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 20:12:38 +05:30
Pritom14 cd84d94ee4 fix(terminal): deliver ring replay and fanout atomically under session lock
A new subscriber could receive the same PTY bytes twice: the ring snapshot
was taken under s.mu but replayed after unlock, while copyOut appended to the
ring and fanned out as two separate lock acquisitions. A chunk appended before
the snapshot could then be fanned out after the replay, delivering it in both.
The same gap let an exited frame overtake the replay.

Make append+fanout one atomic step under s.mu (deliver) and replay the snapshot
before releasing the lock in subscribe, so the two critical sections fully
serialize and each chunk reaches a subscriber exactly once.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 19:39:40 +05:30
Pritom14 4d90d59bf0 feat(backend): wire terminal manager into the daemon at /mux
Construct the tmux runtime and a terminal.Manager fed by the CDC
broadcaster, and hand it to httpd.New so the /mux WebSocket surface goes
live. httpd.New now runs after the CDC substrate so the broadcaster exists
when the manager subscribes; the listener still binds before running.json
is written, preserving fail-fast on port conflict. The manager is closed on
shutdown alongside the CDC pipeline and lifecycle stack.
2026-05-31 19:23:35 +05:30
Pritom14 edcc631037 feat(httpd): mount terminal-streaming WebSocket at /mux
Add the /mux route: httpd performs the WebSocket upgrade (coder/websocket)
and adapts the connection to terminal.wsConn via wsjson, then hands it to
terminal.Manager.Serve. httpd owns only the upgrade and transport
adaptation; all stream logic stays in internal/terminal.

The route is mounted outside the per-request Timeout middleware (the
connection is long-lived) and is omitted entirely when no manager is wired,
so the daemon degrades to no terminal surface rather than failing. New/
NewRouter take the manager; main.go passes nil until commit 3 wires it.

mux_test.go drives the real upgrade + wsjson + Serve + creack/pty path with
a throwaway shell command, so it needs no tmux.
2026-05-31 19:23:35 +05:30
Pritom14 721b8b34a2 feat(terminal): PTY-attach terminal streaming feature package
Add internal/terminal: a transport-agnostic feature package that attaches
a PTY to a session's tmux pane and multiplexes the byte stream to WebSocket
clients, plus a session-state channel fed by the CDC broadcaster.

The PTY is reached through a small PTYSource interface (satisfied by the
tmux runtime adapter) and spawned via an injectable spawnFunc, so fan-out,
the 50KB replay ring, and re-attach resilience all test without a real
process, tmux, or network. A tmux-guarded integration test exercises the
real creack/pty path end-to-end.

Raw PTY bytes never touch the CDC change_log; only the sessions channel is
CDC-fed. Windows PTY spawning is stubbed pending a ConPTY path.
2026-05-31 19:23:35 +05:30
harshitsinghbhandari 1a9a9ec67e test(integration): check ok/err before patching session row
Greptile P1: the patch-then-Update path in TestRestoreRoundTrip was
discarding GetSession's ok/err. A missed row would have handed UpdateSession
a zero-value SessionRecord (ID==""), which matches zero rows and returns
nil — Phase B then fails with the misleading "agent session id lost across
restart" instead of the real cause.

Fixed at the patch site (the only write path that could swallow the error)
and at the two read-then-assert sites in TestDetectingPersistsAcrossRestart
for consistency. Downstream assertions there would already fail loudly, but
the explicit ok/err check makes the failure mode unambiguous.

All 219 tests still pass under -race.
2026-05-31 18:54:55 +05:30
harshitsinghbhandari 6552e1eee0 test(integration): address greptile review
- Idempotent close + t.Cleanup so a mid-test t.Fatalf can't leak the
  SQLite handle for the rest of the binary run. Restart-style tests still
  call close() explicitly between phases; the cleanup hook becomes a no-op
  once that runs.
- Drop the hardcoded "mer-1" assertion in TestHappyPath; assert the
  structural invariant (project-scoped, non-empty id) instead, so the
  test does not couple to the {project}-{counter} generation detail.
- Realign the test storeAdapter's PRFactsForSession + WritePR bodies to
  be line-for-line identical to backend/lifecycle_wiring.go's production
  adapter (extracted prState helper, separated err vs empty-rows checks),
  so a future divergence shows up as a diff at review time. The proper
  fix (extract to internal/storeutil) remains out of scope per the
  brief's "do NOT redesign anything".

All 219 tests still pass under -race.
2026-05-31 18:46:31 +05:30