Commit Graph

1607 Commits

Author SHA1 Message Date
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
prateek c3eeecb686
merge fork, 25 May - 2 June (#2086)
* feat(core): add prs[] array to Session for multi-PR support — Phase 1 metadata layer

* feat(core): update lifecycle-manager for multi-PR per session — Phase 2

* feat(web): add prs[] to DashboardSession and wire up multi-PR display — Phase 3

* feat(plugins): append to prs metadata field on gh pr create — Phase 4

* fix: address all code review issues — PR number, enrichment pipeline, claimPR append, stale PRs, V1 compat, SCM calls, dead code, isDraft, session validation

* fix: address Greptile review — state machine aggregation, URL dedup, React keys, review comments, typecheck

* fix: remove useless assignment in lifecycle-manager — lint error

* fix: add missing prs field to Session objects in cli and plugin test files

* fix: add prs field to integration-tests helpers and fix V1 flat-file fallback in agent-claude-code

* fix: use stack approach for claimPR prs field — claimed PR becomes primary

* feat(core,web): multi-PR per session — enrichment, status dots, bug fixes (#1821)

- Session metadata now stores a comma-separated prs list; all PR URLs
  parsed into session.prs[] on load, backwards-compatible with single-PR
- Enrichment loop hydrates every PR in the list each poll cycle via the
  existing batch GraphQL call
- Fixed closed-PR filter reading from session lifecycle state instead of
  per-PR enrichment cache, which misclassified secondary PRs
- Fixed isDraft never propagating from enrichment cache to dashboard.pr
  and dashboard.prs[i]
- Fixed prs[0] and dashboard.pr being separate objects — enrichment on
  one now syncs to the other
- SessionCard PR badges now show a colored status dot per PR: green (CI
  passing/merged), yellow (CI pending), red (CI failing/changes
  requested), grey (unenriched/draft/closed)
- Corrected color token mapping: passing uses --color-status-merge
  (green), pending uses --color-status-pending (yellow)

* fix(core): add missing prs field to code-review-manager test fixture

* feat(web): per-PR rows with colored chips, titles, and click-to-switch on session card

- Replace single-PR badges with one row per PR when session has multiple PRs
- Each row shows: status-colored chip, PR title (truncated), diff size
- Chip colors: green (CI passing/merged), yellow (CI pending), red (CI
  failing/changes requested), grey (closed/draft/unenriched)
- Repo initials badge (e.g. AO, CM) shown per row when PRs span multiple repos
- Clicking a row selects it — description, alerts, and action buttons in the
  card body all switch to that PR's context
- Clicking the chip opens GitHub; clicking title/diff area selects without
  navigating away
- Single-PR sessions are completely unchanged

* fix(web): address multi-PR card review — effectivePR for merge button, color-mix chips, left-border selection

* fix(web): derive prs from pr in makeSession test helper

* fix(core): fall back to raw[pr] when prs absent in claimPR

* feat(web): PR switcher in session detail header for multi-PR sessions

* fix(core): treat ciStatus/reviewDecision 'none' as passing in multi-PR aggregation

* fix(core): stale-session cleanup checks all prs before killing multi-PR session

* fix(core): all-none reviewDecision aggregates to none, not approved

* fix(core): prs accumulation in Node hook, bump WRAPPER_VERSION, clear stale enrichment on claimPR

* fix(core,web): webhook matching for secondary PRs, restore lifecycle PR number fallback

* fix(web): merge endpoint resolves target PR from session.prs for secondary PRs

* fix(core,web): clamp selectedPRIndex, guard multi-PR partial cache miss, clear prs on takeover

* fix(core,web): scope webhook secondary PR match by owner/repo, handle JSON metadata in Node hook

* fix(web): guard merge route PR lookup by owner/repo to prevent number collision

* fix(web): forward owner/repo from merge button to activate PR disambiguation guard

* fix(web): propagate owner/repo through BottomSheet and AttentionZone merge triggers

* test(web): update onMerge assertion to expect owner/repo arguments

* fix(core,web): resolve 5 multi-PR logical gaps — conflict detection, partial cache miss, isDraft aggregation, event context prs, AttentionZone labels

* fix(core,cli,plugins): add prs field to NotificationEventContext fixtures across all packages

* fix(web): add owner/repo guard to primary PR branch in webhook session match

* fix: handle unknown update versions and flat interactive config

* ci: pin GitHub Actions to SHAs

* fix: support new orchestrator from flat config

* ci: skip dependency review on unsupported forks

* revert: run dependency review on all PRs

* docs(design): add dashboard design-language exploration + mockups

Capture the design-language direction explored for the AO dashboard as
live HTML mockups plus rationale, under docs/design/.

- dashboard-language.md: the language — blue=orchestrator / orange=agents
  identity, rationed semantic color, Schibsted Grotesk + JetBrains Mono,
  unified status system, layout patterns, and how it diverges from DESIGN.md.
- mockups/kanban.html: canonical fleet board (frameless lifecycle columns).
- mockups/session.html: canonical session detail (framed xterm + pluggable
  inspector rail: Summary / Changes / Browser; review-comment "Address").
- mockups/{concepts,refined,*-icons}.html: exploration / icon comparisons.
- mockups/mascot.png: the blue conductor mascot.

Reference mockups only (CDN fonts + inline styles); production must use the
Tailwind tokens in globals.css per C-02/C-07.

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

* feat(web): migrate dashboard to mission-control design language (supersedes DESIGN.md)

Replace the "Warm Terminal" system with the "Mission Control" language from
the merged design exploration (docs/design/dashboard-language.md + the kanban
& session mockups). One coherent system, dark control-room aesthetic, rationed
color (blue = orchestrator/you, orange = working agent, amber = needs-input,
red = failing/stuck, green = mergeable, else grayscale).

Tokens & fonts
- globals.css @theme/.dark: add literal palette (--bg #0a0b0d, --bg-side,
  --card #15171b = only bordered surface, --term #0c0d10, --line/--line-2,
  text ramp --t1..--t4, --blue/--orange/--amber/--red/--green) and re-point the
  existing --color-* semantic tokens to it (no token renames — alias + migrate).
  Flat surfaces, no warm gradients/glows. Dark theme preserved.
- Self-host Schibsted Grotesk (UI) + JetBrains Mono (machine) via
  next/font/local — no external font CDN. Geist Sans removed.

Status system (one spectrum, used everywhere)
- lib/status-spec.ts (getStatusSpec) + <StatusBadge> render the kanban card
  badge, sidebar dot, and session topbar pill from a single source; working dot
  breathes (CSS-only @keyframes breathe).

Fleet board (kanban.html)
- Frameless tinted columns Working -> Needs you -> In review -> Ready to merge
  with per-column semantic top-glow; the card is the only bordered surface
  (hairline ring, no status left-rail). Sidebar-shows-all-projects + SSE 5s
  unchanged.

Session detail (session.html)
- Framed xterm terminal: theme object tied to tokens (orange cursor, blue
  selection, token-mapped ANSI) — terminal content unstyled.
- New pluggable inspector rail <SessionInspector>: Summary (PR -> review
  comments -> Activity -> Overview) / Changes / Browser. Review comments keep
  the soft-blue Address action (askAgentToFix passing comment + file:line).
- Topbar: shared status pill, Kill, solid-blue Orchestrator primary; the PR
  popover is now mobile-only (desktop has the inspector). Blue mascot mark.

Tests: add status-spec / StatusBadge / SessionInspector / AppMark tests; update
column-label, theme, layout-metadata and PR-location assertions; mock
next/font/local in vitest. typecheck / test / lint / build all green.

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

* wip(web): compact card footer + cleanup before mockup-fidelity pass

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

* feat(web): mockup-faithful redesign pass — sidebar brand, resizable panels, cleaner chrome

Addresses review feedback to follow docs/design/mockups/{kanban,session}.html closely.

Sidebar (ProjectSidebar + mc-sidebar.css)
- Brand + blue mascot mark moved to the TOP of the sidebar (was in the topbar).
- Cool sidebar background (var(--color-bg-sidebar) #08090b) — removed the warm tint.
- Removed the yellow session-count chip (plain mono count) and the Review button.
- Per-project action icons now reveal on hover only.
- Consolidated the footer's four buttons into a single Settings gear + popover
  (show killed / show done / show session id / theme).
- Right edge is drag-resizable.

Board (Dashboard + AttentionZone + SessionCard + mc-board.css)
- Topbar brand removed; "Board" header + mockup subtitle; blue primary CTA.
- Frameless tinted columns with per-column glow; compact informational cards
  (status badge · id · hover terminal, 2-line title, branch w/ git icon, thin
  PR/CI footer). Removed inline quick-reply/alerts/CI-chips/merge+review buttons
  from cards (actions live on the session page); kept hover kill + terminal link.
- Split Done card into SessionCard.parts.tsx (≤400 lines).

Session detail (SessionDetail* + SessionInspector + TerminalControls + mc-session.css)
- Topbar: ‹ Kanban back button, title with branch to its RIGHT (git icon, no odd
  truncation), shared status pill, Kill, blue Orchestrator. Brand removed from the
  session shell header too.
- Terminal header: removed the "Connected" status text; matches the mockup
  (Terminal label · id · zoom · fullscreen). Terminal flush (no stray left margin).
- Terminal head and inspector tab bar are both 47px so their rules align.
- Inspector: PR → review comments → Activity timeline → Overview; left edge
  drag-resizable. Split header helpers into SessionDetailHeader.parts.tsx.

Shared infra
- useResizable hook (pointer drag → CSS var on :root, localStorage-persisted,
  no inline style); .resize-handle styles; per-screen mc-*.css loaded after
  globals.css in layout.tsx; sidebar/inspector widths read --ao-sidebar-w /
  --ao-inspector-w with sensible min/max + double-click reset.

typecheck / web tests (1005) / lint (0 errors) / build all green; resize verified.

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

* fix(web): sidebar/topbar polish from review feedback

- Notification bell icon 13→17px (was tiny inside its 34px button).
- Sidebar project rows: count shows at rest in the right slot; per-project
  action icons are now an absolutely-positioned hover-reveal group (out of
  flow) so the project name keeps full width and the row stays single-height
  (fixes the inflated rows / vertically-stacked icons).
- Session status dot nudged 1px to vertically center against the mono label.
- Orchestrator session page no longer renders the inspector rail (full-width
  terminal — nothing to inspect).

typecheck / web tests (1005) / lint (0 errors) green.

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

* fix(web): notification icon size + sidebar/topbar review fixes

- Notification bell on the session topbar was squeezed to a 6px sliver: the
  text-button padding rule applied to the icon-only button inside its fixed
  34px width. Give the bell its own 34px square / zero-padding rule → 17px icon.
- Remove the redundant sidebar toggle from the session topbar on desktop
  (mobile keeps the drawer hamburger); the sidebar owns collapse/expand.
- Collapsed sidebar now shows an expand button so it can be reopened.
- Trim sidebar padding + session-list indent so project/agent rows get more
  width.

typecheck / web tests (1005) / lint (0 errors) green; collapse↔expand + icon
size verified.

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

* fix(web): sidebar session names truncate with ellipsis; rename pencil no longer overlays

- The session label was an inline <span>, so text-overflow:ellipsis never
  applied — long names hard-clipped. Make it display:block so it ellipsizes.
- The rename pencil now absolutely positions at the row's right edge; at rest
  the label gets the full row width, and on hover the link gains right-padding
  so the text re-truncates and the pencil sits in the reserved space instead of
  covering the name.

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

* fix(web): project name reserves space for hover action icons (no overlap)

On hover the per-project action icons reveal over the right slot; the toggle now
gains right-padding so the project name re-truncates with ellipsis instead of
running underneath the icons — same pattern as the session rows.

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

* fix(web): remove kanban topbar toggle + redesign empty state to mission-control

- The board (kanban) topbar still rendered a desktop sidebar toggle; make it
  mobile-only (the sidebar owns collapse/expand), matching the session topbar.
- Empty state (Skeleton EmptyState): ghost columns now use the live board's
  labels (Working / Needs you / In review / Ready to merge), 4 columns, and the
  frameless tinted-trough look — removing the old dashed/warm-tinted columns and
  the stale 5-category set. Orchestrator glyph recolored to the blue accent via
  tokens (was hardcoded orange).

typecheck / web tests (1005) / lint (0 errors) green.

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

* fix(web): show project name in titlebar, not the raw project id hash

The board topbar special-cased projects named 'Agent Orchestrator' to display
the project id (e.g. agent-orchestrator_649ba24578) — unreadable gibberish.
Always show the human-readable project name instead.

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

* fix(core,web): detectPR branch guard, multi-PR prLabel aggregation, reset selectedPRIndex on session change

* fix(web): render terminal with xterm WebGL renderer (fixes broken box-drawing)

The dashboard terminal used xterm's default DOM renderer, which draws each
row as a separate DOM line and cannot tile box-drawing / block glyphs across
cells. Agent TUIs (Claude Code's bordered panels, the "N shell command"
expansion) rendered with broken/gappy frames and a grey blob — independent of
font or lineHeight (verified: even a complete font at lineHeight 1.0 breaks in
the DOM renderer).

Load xterm's WebGL renderer (@xterm/addon-webgl), which GPU-draws box/block
glyphs into each cell so frames connect and shaded regions stay solid. Loaded
rAF-deferred after open() with a DOM fallback on context-loss / when WebGL is
unavailable (headless, blocklisted GPU). This matches how superset renders
the same agent TUIs.

The WebGL addon only ships for the xterm 6.1.0-beta train, so bump
@xterm/xterm 6.0.0 -> 6.1.0-beta.256 and the addons to the matched beta set
(the same pair superset runs in production).

Also drops the earlier no-op fontWeight/fontWeightBold/letterSpacing change —
those values equalled xterm's defaults and had no visible effect.

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

* fix(web): enforce dark-mode contrast floor so agent blocks aren't grey blobs

Claude Code's expanded "Ran N shell command" block is painted on an ANSI
white background (ESC[47m). The terminal theme sets `white` ≈ `foreground`
(#c5ccd3), and dark mode used `minimumContrastRatio: 1` (no floor), so the
block's text was the same colour as its background — rendering as an
unreadable grey blob (only independently-coloured links/emoji showed through).

Raise the dark-mode floor to 4.5 (WCAG AA), matching the rationale already
used for light mode (7). xterm then auto-adjusts only the foregrounds that
fail the floor, leaving all other agent colours untouched. Verified in an
isolated harness with the real theme: white-on-white block goes from
invisible to legible.

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

* fix(web): load Unicode 11 addon so emoji/wide-char widths match tmux

Agent TUIs and tmux lay out tables treating emoji like / as 2 cells
wide (modern Unicode). xterm defaults to Unicode 6 width tables, where
those are 1 cell — so the grid shifts a column after every emoji, breaking
table borders and leaving stray text fragments (visible only in the web
terminal; a direct tmux attach via a modern terminal renders it fine).

Load @xterm/addon-unicode11 and set unicode.activeVersion = "11" so xterm's
width tables agree with tmux/the agent. Same beta train as the other addons.

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

* fix(web): rescale overlapping glyphs so out-of-font chars don't overlap text

JetBrains Mono is subset to Latin glyphs, so characters it lacks (arrows
→ ← ↔, CJK, emoji, powerline) are drawn from a fallback font whose advance
can exceed our monospace cell — the glyph then bleeds into the next cell and
overlaps the following character. Set rescaleOverlappingGlyphs so xterm shrinks
any glyph wider than its cell back to fit. Fixes the whole class of out-of-font
overflow, not just arrows.

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

* fix(web): ship full-coverage JetBrains Mono (restore arrows/box/blocks)

The self-hosted JetBrains Mono was subset to ~229 Latin glyphs, dropping
arrows (→ ← ↔), box-drawing, block elements and other symbols agents emit.
Those fell back to a system font at the wrong cell width — overlapping
(pre-rescale) or shrunk/blurry (post-rescale). Replace it with the full
JetBrains Mono variable face (OFL-1.1, wght 100–800, ~111 KB woff2, same
metrics) so those glyphs render in-font, full-size and crisp at the correct
monospace width. rescaleOverlappingGlyphs stays as a safety net for the few
glyphs even the full face lacks (CJK, emoji, powerline PUA).

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

---------

Co-authored-by: Laraib-1629 <laraibahmed73@gmail.com>
Co-authored-by: suraj-markup <sk9261712674@gmail.com>
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Priyanshu Choudhary <57816400+Priyanchew@users.noreply.github.com>
2026-06-02 05:10:55 +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 80f46719d9
Merge pull request #61 from aoagents/chore/golangci-lint
chore(ci): add golangci-lint with a strong ruleset + fix CI Go version
2026-06-01 05:02:02 +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 c4bbbf73c4
Merge pull request #63 from aoagents/revert-58-feat/55
Revert "feat: add notifier delivery runtime"
2026-06-01 04:48:25 +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
prateek 70718f84f6
Merge pull request #60 from aoagents/refactor/collapse-pr-row-types
refactor(backend): collapse duplicate PR row types into one domain definition
2026-06-01 04:18:37 +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
prateek 721888f146
Merge pull request #53 from aoagents/feat/cli-foundation
feat(backend): add Cobra CLI foundation
2026-06-01 03:38:28 +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 4f13dd1b83 test(cli): add -v/--verbose mode that prints each command and full output
run-local.sh -v (or AO_SMOKE_VERBOSE=1) makes smoke.sh echo every command and
its complete output, indented, alongside the PASS/FAIL — for auditing exactly
what the suite runs and what the CLI returns. Default output is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 03:04:54 +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 a9e83011f1 docs(test): correct --init rationale (suite uses a fabricated dead PID)
The stale-daemon assertion does not depend on container PID-1 reaping — it
writes a fabricated dead PID rather than killing a real process. --init is
still run so the real daemon spawned by the `start` test (detached via setsid)
is reaped after `stop` instead of lingering as a zombie. Reword the README,
Dockerfile, and workflow comments to say that accurately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 02:37:51 +05:30
itrytoohard 3680ac5474 test(cli): add end-to-end smoke test + Docker/CI harness
Adds a fresh-machine, install→use→verify E2E test for the `ao` CLI and wires
it into CI. The suite drives the real binary (start/status/doctor/stop + the
daemon-control HTTP surface) against fully isolated state — its own temp
run-file, data dir, and an auto-picked free loopback port — so it never
collides with a developer's real AO install or daemon.

- test/cli/smoke.sh: 40 assertions covering install resolution, version/help
  (daemon hidden), doctor text+json (and that it does NOT migrate SQLite),
  status stopped/stale/ready, start fresh+idempotent, daemon-created store,
  /healthz identity, the /shutdown CSRF + DNS-rebinding guard (403 + survives),
  graceful/stale/idempotent stop, run-file ownership cleanup, exit codes
  (2 usage / 1 runtime), and completion for all four shells. It deliberately
  ignores an inherited AO_PORT and self-allocates a free port for isolation.
- test/cli/Dockerfile: models installing ao on a fresh machine — builds the
  binary, drops it on PATH in a clean Debian image with only runtime deps
  (git/tmux/curl), runs the suite as a non-root user.
- test/cli/run-local.sh: build-from-source + native run convenience wrapper.
- .github/workflows/cli-e2e.yml: two tiers — `native` runs the suite on a
  ubuntu+macos runner matrix (the real VMs, to cover the unix setsid detach and
  macOS os.UserConfigDir paths a Linux container can't), and `container` runs
  the fresh-machine Docker image with --init (real PID-1 reaper so the
  stale-daemon assertion is reliable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 02:33:56 +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
Adil Shaikh 83d1ea1e88
Merge pull request #57 from aoagents/feat/54
feat: add durable notification foundation
2026-06-01 00:58:14 +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
Priyanshu Choudhary e55012793c
fix(web): allow Windows add-folder browsing outside home (#1785)
* fix(web): allow Windows add-folder browsing outside home

Allow the dashboard file browser to navigate Windows drive roots while keeping non-Windows browsing home-contained and restricted folders blocked. Add an editable path field, Windows drive selector, current-folder selection metadata, and regression coverage for typed paths and drive navigation.

* fix(web): address add-folder review feedback

Use the shared Windows platform helper in browse API tests and preserve drive-rooted paths when building Windows breadcrumbs.

* perf(web): make filesystem browse API non-blocking

* refactor(web): extract useDirectoryBrowser hook

* refactor(web): extract DirectoryBrowser component

* refactor(web): slim AddProjectModal to a shell

* feat(web): add breadcrumb navigation to folder picker

* feat(web): show folder icons and git-repo badges in picker

* feat(web): add keyboard typeahead to folder picker

* feat(web): add recent folders to picker sidebar

* feat(web): restyle drive picker to match toolbar

* test(web): fix directory browser lint import

* feat(web): refine add-project folder picker UX

- Keyboard nav (arrows/typeahead/Enter) now works when focus is on the
  modal dialog, not only after clicking into the browser pane
- Announce git-repo status to screen readers via folder-row aria-label
- Square the drive picker to match the toolbar buttons
- Redraw the broken refresh icon as a clean circular arrow
- Drop the redundant current-path line below the breadcrumb
- Remove the Recent sidebar; reclaim its width for the folder list
- Add a selectable "this folder" row so the current directory - and
  git repos with no subfolders - can be added
- Reveal a descend chevron on row hover/selection
- Place Project ID and Project Name on one two-column row

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

* fix(web): add Home to location dropdown, stop auto-selecting on descend

Two folder-picker UX bugs:

1. Once on a Windows drive (C:, D:), there was no way back to ~ from the
   dropdown — only drives were listed. Replace the "Drive" placeholder
   with a real "Home" option (value ~) so the dropdown always offers
   every root. The select's value reflects browsePath, so it reads
   "Home" at ~ and the drive letter inside a drive. Renamed the aria
   label from "Drive" to "Location" to match.

2. Double-clicking a folder to descend was silently re-selecting the
   folder you'd just navigated into — if it wasn't a git repo, the
   modal flashed a red "not a git repository" warning for every
   non-repo folder a user passed through. Same flaw fired on
   breadcrumb clicks, drive switches, and history nav.

   Root cause: browse() defaulted selectedPath to the navigation
   target. Changed the default to "" — selection is now only ever
   set by explicit user intent (clicking a row, the "this folder"
   row, or pressing Enter on a typed location). Callers that
   genuinely seed a selection (reset, refresh, location-input
   submit) already pass selectedPath explicitly, so they're
   unaffected. Also dropped the now-redundant selectedPath from
   the drive-switch call.

Tests: 29/29 passing. Added two tests for the Home option (it's
present and picking it routes to ~; the select value tracks
browsePath) and one for the descend-doesn't-select behavior.
Updated two pre-existing tests that asserted the old auto-select
contract with comments explaining the new one.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 21:48:20 +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