Commit Graph

209 Commits

Author SHA1 Message Date
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
harshitsinghbhandari ee0af288a4 chore: gofmt integration test file
The Check formatting step failed on the stub method alignment in
stubAgent's three method declarations. Re-run of gofmt aligns the column
gutter on those signatures; no behavioural change.
2026-05-31 18:37:06 +05:30
harshitsinghbhandari ad1c4dacec test(integration): LCM+SM live-fire against real SQLite store
Adds backend/internal/integration with five end-to-end tests that hydrate the
real lifecycle.Manager + session.Manager against a tmp SQLite store and
exercise the full pipeline through the DB triggers and the CDC poller:

- TestHappyPath_Spawn_PR_Kill — spawn -> SCM PR observation (open + CI
  passing) -> kill; asserts canonical row, pr row, and change_log event
  types (session_created/_updated, pr_created, pr_check_recorded).
- TestRestoreRoundTrip_PreservesMetadata — spawn, kill, close store, reopen
  same DB path, hydrate fresh LCM/SM, Restore(); asserts AgentSessionID and
  the rest of SessionMetadata survive across the daemon restart.
- TestCIFailureAndRecovery_NudgeThenClears — failing CI observation drives
  the CI-failed reaction nudge with the log tail injected; passing CI
  observation switches to approved-and-green human notify; pr_checks history
  reads back the failure (the brake's source of truth).
- TestDetectingPersistsAcrossRestart — failed probe parks the session in
  detecting with detecting_* columns populated, round-trips across a
  close/reopen, alive probe clears the quarantine memory.
- TestCDCPollerReceivesAllStages — drives the real cdc.Poller; asserts the
  trigger pipeline emits each expected event_type and seq is monotonic.

Wiring gap fixed (minimal): goose v3 keeps baseFS/logger/dialect as
package-level globals, so two concurrent sqlite.Open() calls — uncommon in
production but normal under -race with t.Parallel() — race on
goose.SetBaseFS/SetLogger/SetDialect inside migrate(). Added a process-level
sync.Mutex around the migrate() call. ~11 lines, no signature changes.

Scope notes (the task brief assumed a fancier architecture than what
actually shipped in PR #37):
- No outbox / consumer_offsets / janitor exist on main — the change_log
  table IS the durable, ordered source of truth (see cdc/event.go), so the
  brief's janitor-watermark step is skipped.
- No reaction_trackers table / ReactionStore port — trackers are in-memory
  per lifecycle/reactions.go; persistence-round-trip there is N/A.
- No revision column / Upsert(rec, eventType) — write-mutex serialises and
  change_log.seq orders, so the assertions land on event_type + seq, not on
  a per-row revision counter.

All 219 tests pass under -race across 18 packages. lifecycle/fakes_test.go
is untouched; existing unit tests still drive the in-memory fake.
2026-05-31 18:34:51 +05:30
Dhruv Sharma 0672dbb266
Merge pull request #37 from aoagents/feat/sqlite-storage-cdc
feat(backend): SQLite storage layer + CDC pipeline
2026-05-31 17:13:00 +05:30
prateek 70aab5eb26 feat(backend): atomic PR-observation write + CDC on check status updates
Addresses review on PR-observation persistence:

- pr_checks now has an AFTER UPDATE CDC trigger (guarded on status change), so a
  check flipping in_progress->failed on the same commit emits change_log instead
  of updating silently. Restores symmetry with the sessions/pr triggers.
- writePR persists scalar facts + checks + comments in ONE transaction via
  Store.WritePRObservation, so a mid-write failure can't leave the pr row (and
  its CDC event) committed while checks/comments are partial. Collapses the
  PRWriter port's three write methods into one WritePR.
- db.go: record why modernc.org/sqlite (pure-Go, CGO-free static binary) at the
  import site.

Regression tests for both the update-trigger (emit on change, suppress no-op
re-poll) and the transactional write. go test -race ./... green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:02:47 +05:30
prateek 0dbd304e58 fix(backend): drain CDC/lifecycle goroutines without deadlocking on non-signal exit
lcStack.Stop()/cdcPipe.Stop() block on done channels that close only when ctx
is cancelled, but the deferred stop() that cancels ctx ran last (LIFO) — so any
non-signal exit (e.g. a listener bind error) hung the daemon forever. Cancel ctx
first, then drain, explicitly after srv.Run instead of via defer. Also refresh
the startup comments that still described the removed outbox/JSONL/janitor flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 16:14:02 +05:30
prateek 0a69b8429e docs(config): drop stale CDC-JSONL mention in resolveDataDir
CDC is trigger-driven in the SQLite DB now; there is no JSONL log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 07:18:22 +05:30
prateek cdf55ebef5 feat(backend): port lifecycle lane onto the new storage+CDC model
Reworks the LCM, reactions, session manager, reaper, and boot wiring onto
the redesigned domain model — collapsing the runtime axis to is_alive,
moving PR facts to the pr table (read back as PRFacts), replacing the
free-form SessionReason with a typed terminal-only TerminationReason, and
dropping Revision/EventType/durable reaction-trackers (CDC is trigger-driven,
escalation budgets are in-memory).

- ports: SessionStore + PRWriter interfaces; PRObservation/RuntimeFacts/
  ActivitySignal DTOs; drop LifecycleStore/EventType/ReactionStore.
- lifecycle: single-writer reducer over is_alive; ApplyPRObservation writes
  the pr tables and reacts; CI-fix-loop brake derived from pr_checks history;
  review comments injected into the agent regardless of author (no bot
  detection); merge auto-terminates with pr_merged.
- session: store-assigned "{project}-{n}" ids; folded metadata; status
  derived from PRFacts on read.
- reaper: reports the four-valued probe vocabulary unchanged.
- boot: trigger -> poller -> broadcaster; storeAdapter bridges *sqlite.Store.

Lane shrinks 6218 -> 2803 LOC. go build/vet/test -race green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 07:16:06 +05:30
prateek e5c4fd6ffd feat(storage,cdc): minimal 6-table schema + trigger-driven CDC (storage layer)
Reworks the storage + CDC layer to the simplified design agreed in review:

Schema (one clean migration, 0001): projects, sessions, pr, pr_checks,
pr_comment, change_log. sessions.id is a single string key "{project}-{num}"
(mer-1); operational metadata folded into sessions; is_alive replaces the
runtime axis; no revision (the per-session write mutex serializes, change_log.seq
orders). pr keyed by URL (1 session : many PRs). pr_checks is CI run history
(one row per check per commit) — the CI-fix-loop brake is a LIMIT 3 query, no
counter stored. change_log carries a required project_id FK + nullable session_id.

CDC is DB-native: AFTER INSERT/UPDATE triggers on sessions/pr/pr_checks append
to change_log atomically with the change (json_object payloads). The old durable
outbox/JSONL/janitor pipeline is gone; the cdc package is now a Poller that reads
change_log and fans events out through the in-memory Broadcaster (hardened with
recover()). Clients catch up via the log from their own offset (SSE Last-Event-ID).

Storage uses a single writer connection + a reader pool (read-your-writes for the
triggers' subqueries; concurrent reads). sqlc-generated typed queries.

Tests (-race): CRUD, per-project id assignment, the loop-brake query, concurrent
creates, triggers populating change_log; CDC end-to-end through the real store,
concurrent goroutine delivery, broadcaster panic-isolation.

NOTE: scoped to storage + CDC. The lifecycle-engine consumers (decide, lifecycle,
session, reaper, main wiring) still reference the old domain axes and need a
follow-up integration pass to compile against the new model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:42:59 +05:30
prateek ba47212802 perf(storage): allow concurrent reads; serialize writes via a mutex
SetMaxOpenConns(1) forced every read (List/Get/GetPR/...) to queue behind
the single connection, so the dashboard's reads contended with the LCM's
writes. WAL already supports many concurrent readers, so raise the pool to 8
and instead serialize *writes* with a Store.writeMu. That keeps WAL's
single-writer rule and the revision-CAS read-then-write atomic regardless of
pool size, while reads now run in parallel across the pool.

Every write method takes writeMu (Upsert, PatchMetadata, UpsertPR/DeletePR,
the pr_check/pr_comment Replace* via inTx, the CDC outbox/offset writes,
project writes, reaction-tracker writes); reads take nothing. Added
TestConcurrentReadsAndWrites (16 writers + 16 readers) which passes under
-race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:51:18 +05:30
prateek b0e4fffa62 test(cdc): add full-stack E2E tests through the real store + snapshot source
The cdc integration test covers the synchronous Drain/Poll happy path but
(1) resyncs from a fake snapshot and (2) never runs the publisher/consumer as
the concurrent goroutines the daemon actually uses. Add two E2E tests in the
composition-root package that wire the real sqlite.Store, outboxAdapter,
Publisher, JSONL log, Consumer, Broadcaster and the REAL snapshotSource
(store.ListAll):

- RealSnapshotResyncThroughRotation: forces a rotation and asserts the
  consumer rebuilds from the sessions table, delivering the persisted record
  payload, with the offset landing at the change_log head.
- ConcurrentPublisherConsumer: runs both as goroutines on their tickers and
  asserts every write is delivered exactly once, in order, offset at head
  (also exercises the broadcaster hand-off under -race).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:39:19 +05:30
prateek 4ce90448e2 refactor(storage): make session metadata + PR facts typed and structured
The first storage cut modelled two side tables as free-form blobs. This
replaces both with opinionated, statically-typed schema so what a session
can carry is fixed by the schema, not by convention.

session_metadata: was a (session_id, key, value) KV bag with six
convention-only keys. Now a 1:1 table of named, typed columns. The domain
currency is a typed domain.SessionMetadata struct (was map[string]string),
threaded through ports.LifecycleStore, the LCM, the Session Manager and the
reaper, so an unknown key is a compile error rather than a silently-dropped
write. PatchMetadata keeps its non-destructive merge ("empty = leave
unchanged"). The off-canonical invariant is now enforced at the type level
via json:"-" on SessionRecord.Metadata, removing the manual `Metadata = nil`
scrub the change_log/snapshot paths had to remember; the Meta* string-key
constants are deleted.

pr_enrichment -> pr (+ pr_check, pr_comment): the scalar facts are now
typed columns with CHECK-constrained enums (review_decision, mergeability,
ci_state) and integer CI counts instead of opaque TEXT. The two list facts
the old `pending_comments`/ci_summary strings smuggled are normalized into
child tables (pr_check, pr_comment) that cascade from pr. The store exposes
UpsertPR/GetPR plus atomic ReplacePRChecks/ReplacePRComments + List.

Both tables remain off the canonical CDC path. sqlc regenerated; migrations
0001/0002 revised in place (nothing released). gofmt/vet clean; go test
-race green; daemon smoke-boots and creates the new schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:33:13 +05:30
harshitsinghbhandari a0020e06be refactor(tracker): address code-review findings
Six fixes from the second code review pass — none load-bearing but all
improve the contract honesty or prevent future churn. Tests pass with
-race (49 in package, 299 across the backend).

1. Preflight: atomic.Bool fast-path before the mutex so cached-success
   calls don't contend on the lock. Double-checked locking on the
   mutex side so concurrent first-callers still serialize the single
   GET /user request.

2. Preflight godoc: tighten to say it verifies token validity, not
   repo authorization — Get/List against a specific repo may still
   return ErrAuthFailed after a green Preflight if the token lacks
   the scope or the repo isn't visible.

3. domain.ListFilter.Limit godoc: explicitly note that exceeding the
   provider per-page max is SILENTLY capped (no error, no truncation
   signal) and that auto-pagination is deferred to #35.

4. Extract issueFromGH helper. Get and List were duplicating the
   identical ghIssue -> domain.Issue projection; consolidating now
   prevents a 3-way merge when #40 (Comment/Transition) lands.

5. parseGitHubRepo: reject whitespace and # in both owner and repo
   segments. Leading dots stay legal (the "owner/.github" repo
   convention). New test cases cover the rejections plus a positive
   guard for the leading-dot case.

6. fakeGH test helper: lock the handlers map on both read and write,
   so future tests registering handlers from goroutines won't trip
   -race.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:32:45 +05:30
harshitsinghbhandari d6cd245833 feat(tracker): add List + Preflight to the port and GitHub adapter
Brings the read-side surface up to parity with the legacy TS impl's
useful read methods. Closes a gap flagged during scope review.

Port additions:
  - List(ctx, repo, filter) ([]Issue, error)
  - Preflight(ctx) error

Domain additions: TrackerRepo, ListStateFilter (open/closed/""=all),
ListFilter (State, Labels, Assignee, Limit).

GitHub adapter:
  - List hits GET /repos/{o}/{r}/issues with query-encoded filter.
    Defaults: state=all, per_page=30; per_page is capped at 100.
    PRs are filtered out client-side (GitHub conflates them with
    issues on that endpoint).
  - Preflight hits GET /user. Success is cached for the Tracker's
    lifetime via sync.Mutex + bool; failures are intentionally NOT
    cached so a transient startup glitch is recoverable.
  - New ErrAuthFailed sentinel. classifyError now maps 401, and 403
    without rate-limit signals, to ErrAuthFailed instead of an opaque
    error — so Preflight callers can distinguish bad-token from other
    failures.

Pagination beyond the first page is intentionally out of scope for v1
(see doc.go); the observer/polling work in #35 will own that.

Tests: 43 pass (was 25). Adds Preflight cache + recovery, List query
encoding, PR filtering, repo parser rejection, and ErrAuthFailed
classification.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 22:04:58 +05:30
harshitsinghbhandari 6e4ec499fb refactor(tracker): drop Comment + Transition; v1 is read-only Get
Scope correction: mirroring agent lifecycle onto the tracker (status
comments, label/state updates) is not wanted in the current rewrite.
That work is now tracked as issue #40 and will land once we decide on
the opt-out knob, label setup, and Linear's workflow-state fit.

Removes from the port and the GitHub adapter:
  - Comment(ctx, id, body) and ErrEmptyBody
  - Transition(ctx, id, state) and ErrUnknownState
  - planForState / transitionPlan and the forward state mapping
  - reasonComplete constant (only the Get reverse mapping is kept)
  - 11 tests + the transitionCall normalization helpers

Kept (still load-bearing for Get):
  - All 5 NormalizedIssueState values — Get reports them faithfully
    when a repo carries the in-progress / in-review labels.
  - The reverse mapping in mapStateFromGitHub.
  - RateLimitError with ResetAt + RetryAfter (#35 will use it).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:53:44 +05:30
Pritom14 23b8fe43cf feat(backend): add projects and pr_enrichment tables to SQLite store
Migration 0002 adds two tables off the canonical CDC path:

- projects: durable registry of managed repos (the twin of the old YAML
  config). Soft-deletable via archived_at so a session's project_id always
  resolves; ListProjects returns active rows only, GetProject resolves any.
- pr_enrichment: per-session cache of rich SCM facts (CI summary, review
  decision, mergeability, pending comments, CI log tail) that do not live
  in the canonical lifecycle. 1:1 with a session, cascades on session delete.

Both are written outside the LCM write path: no revision bump, no
change_log/outbox event. Store methods mirror the reaction_trackers adapter
pattern with storage-local row structs.
2026-05-30 21:53:14 +05:30
Pritom14 f5bc4c7b8c feat(backend): SQLite storage layer + CDC pipeline, LCM/reaper wiring
Add the two real outbound adapters that replace the in-memory fakeStore:
internal/storage/sqlite (persistence satisfying ports.LifecycleStore) and
internal/cdc (transactional-outbox publisher, JSONL delivery, durable
consumer). Wire them into main.go alongside the Lifecycle Manager and reaper
so the write path is live end-to-end: LCM.Upsert -> store -> outbox -> JSONL
-> broadcaster.

Storage (internal/storage/sqlite):
- modernc.org/sqlite (pure Go, no CGO) for clean cross-compile; goose
  embedded migrations; sqlc-generated typed queries under gen/.
- Atomic Upsert: session row + change_log + outbox written in one tx.
- revision is an optimistic-concurrency (CAS) check: insert requires
  revision 0 and persists 1; update requires loaded revision == stored and
  bumps +1; zero rows affected returns a revision-mismatch error.
- Metadata is an opaque map in session_metadata, off the CDC path.
- Durable reaction_trackers (fixes the in-memory-only escalation budget that
  re-fired human pages on restart).

CDC (internal/cdc):
- Publisher drains the outbox to a JSONL log; size-based rotation with a
  reset marker.
- Consumer tails via byte cursor, detects rotation (os.SameFile), resyncs
  from a full-state snapshot on gaps, and tracks a durable consumer_offsets
  cursor.
- Janitor reclaims acknowledged outbox rows.
- Broadcaster is the in-process fan-out port the FE transport will subscribe
  to (WS/SSE wiring deferred).

Composition root (main.go + *_wiring.go):
- startCDC stands up publisher/consumer/janitor + broadcaster.
- startLifecycle constructs the LCM, makes escalation budgets durable via
  WithReactionStore, teaches it to enumerate sessions via WithSessionLister,
  and starts the reaper.
- Notifier, AgentMessenger, and the reaper's runtime registry are TEMPORARY
  no-op/empty stubs (lifecycle_wiring.go) with TODO markers; see the PR
  description for how to fill them in.

Tests: contract-parity, revision CAS, outbox atomicity, CDC ordering and
idempotency, rotation/resync, janitor vacuum, reaction durability across a
simulated restart, and composition-root adapters. gofmt/build/vet clean and
go test -race ./... green.
2026-05-30 16:02:07 +05:30
harshitsinghbhandari e5919c7998 feat(tracker): Tracker port + GitHub adapter
Reference implementation; GitLab and Linear follow in separate PRs.
Issue observer loop (poll + ApplyTrackerFacts) is deferred to #35.

Three-layer split mirrors the SCM layout adil is landing in PR #28:
- domain/tracker.go   — value types (TrackerProvider, TrackerID,
  NormalizedIssueState, Issue)
- ports/tracker.go    — the Tracker interface
- adapters/tracker/github/ — REST-backed adapter

v1 is write-mostly: Get, Comment, Transition. No cache, no inflight
dedup, no polling. State mapping is documented in the package doc and
exercised by table-driven tests against an httptest fake — no real
GitHub traffic from CI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:11:15 +05:30
harshitsinghbhandari 1eaaa4ce1d fix(observe): broaden reaper poll set + always report probe fact
Address blocker found in self-review (B1 + I1):

- Manager.RunningSessions previously filtered to runtime.State == RuntimeAlive,
  but a session enters Detecting with runtime axis = RuntimeProbeFailed (failed
  probe path, decide_bridge.go:72) or RuntimeMissing (detectingLC in
  manager_test.go:539). The filter silently parked every Detecting session, so
  the recovery path proved by manager_test.go:59 ("healthy probe recovers
  liveness-owned detecting -> working") and the terminal path proved by
  manager_test.go:79 ("dead+dead with no recent activity concludes killed")
  were both unreachable through the reaper. Broaden the predicate to "session
  is not in a terminal state" (mirrors the LCM's existing isTerminal helper)
  and document the wider semantics.

- reaper.probeOne now reports every probe result — including alive — back to
  the LCM as ApplyRuntimeObservation facts. The previous skip-alive
  optimization was a layering violation: the reaper has no business deciding
  what counts as a no-op. The LCM's ApplyRuntimeObservation already diffs
  against canonical and only Upserts on actual change, so steady-state alive
  stays cheap. With the broadened poll set, an alive probe for a Detecting
  session IS the recovery fact.

- Add unit tests for Manager.RunningSessions covering: nil-lister no-op, lister
  error propagation, and the full canonical state matrix (working/idle/
  needs_input/detecting-probefailed/detecting-missing/not_started included;
  terminated/done excluded).

- Update reaper tests: alive case now asserts the alive fact is reported; new
  "detecting session: alive probe reported so LCM can recover from quarantine"
  case locks in the recovery path; multi-runtime case now asserts both runtime
  facts flow through.

- Bump "session in poll set without handle metadata" log from Debug to Warn —
  it is an anomaly (OnSpawnCompleted should have written both keys), not a
  routine event.

- Document WithSessionLister must be called before any reaper attached to the
  Manager starts running (it is a bare field read; concurrent re-injection is
  meaningless anyway).
2026-05-29 22:18:54 +05:30
harshitsinghbhandari 11475fbc72 feat(observe): reaper for liveness probe + TickEscalations heartbeat (#9)
The reaper sits OUTSIDE the LCM's per-session serial loop. On every tick it:
1. Fires lcm.TickEscalations(now) — the duration-based escalation heartbeat
   a non-polling LCM cannot wake itself to drive.
2. Asks lcm.RunningSessions for the snapshot of sessions whose runtime axis is
   alive, then calls runtime.IsAlive(handle) per session via a RuntimeRegistry
   that dispatches by RuntimeHandle.RuntimeName (so a single reaper covers
   tmux + zellij side by side).
3. Reports any non-alive result back as a fact via ApplyRuntimeObservation —
   dead -> RuntimeProbeDead, probe error -> RuntimeProbeFailed (never
   collapsed to alive: failed probe ≠ dead, but it ≠ alive either). Steady-
   state alive is skipped so we don't churn the LCM with no-op load/diff work.

The reaper REPORTS facts; the LCM owns DECIDE (anti-flap Detecting quarantine,
terminal-session rules). The reaper never writes.

Open-question resolution: add RunningSessions(ctx) to ports.LifecycleManager
(option a). The Manager implements it via an injectable session lister
(Manager.WithSessionLister) so the LCM itself does not require a new
LifecycleStore method — Tom's store contract is untouched, daemon wiring (lane
#10) will inject the production lister at startup.

Scope: reaper goroutine + the minimum LCM seam. No activity ingest, no FS
watcher, no daemon wiring, no new schema fields, no store changes.
2026-05-29 22:10:29 +05:30
harshitsinghbhandari 650ecdf08a fix: address LCM/SM review blockers R1, RA, R11, RB
Four narrowly-scoped fixes against the LCM + Session Manager lane
from an external review of the current backend state. R2 (failed-restore
lifecycle stranding) is intentionally deferred to PR #15, which already
closes it via the new OnSpawnInitiated path; R3 also stays on that PR.

- R1 (BLOCKER): Manager.Spawn never persisted AgentSessionID, so
  Manager.Restore's hard-required metadata key was always missing and
  every restore failed. Persist the assembled launch prompt as
  MetaPrompt at spawn time and add a fresh-launch fallback to Restore
  that uses Agent.GetLaunchCommand with the seeded prompt when the
  captured agent session id is absent (the id-capture hook is a separate
  path that may never have run). Restore still fails fast when neither
  the id nor a prompt is on hand — there is nothing to relaunch from.

- RA (BLOCKER): adapters/workspace/gitworktree/commands.go's
  worktreeRemoveForceArgs passed --force, which deletes uncommitted
  agent work. Renamed to worktreeRemoveArgs and dropped --force so the
  post-prune "still registered" guard in Workspace.Destroy surfaces the
  refusal to Manager.Cleanup, which routes the session to Skipped
  instead of destroying in-progress changes.

- R11 (SHOULD-FIX): reactions.go's two Notifier.Notify call sites
  (executeReaction's notify and escalate) built OrchestratorEvent
  without ProjectID. Captured projectID on the transition (via a
  store.Get in mutate) and on reactionTracker (so TickEscalations can
  still populate it on duration-based escalations), and threaded it
  through executeReaction/sendToAgent/escalate.

- RB (SHOULD-FIX): gitworktree.Workspace.managedPath used filepath.Join
  which cleans .. segments before validateManagedPath ran, so
  session=\"../other\" stayed inside managedRoot while breaking
  per-project isolation. validateConfig now rejects path separators and
  the . / .. components on ProjectID and SessionID at the source.

go build ./..., go vet ./..., and go test -race ./... all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 17:57:46 +05:30
Harshit Singh Bhandari 2b4af15605
Merge pull request #15 from aoagents/fix/lcm-writer-contract
fix: reconcile LCM writer contract
2026-05-29 17:00:46 +05:30
harshitsinghbhandari 1376b01933 fix: align writer contract with schema-2 2026-05-29 16:13:14 +05:30
harshitsinghbhandari fb7dbbbb42 fix: guard spawn initiation and restore metadata 2026-05-29 15:43:58 +05:30
neversettle 59a654afea
feat(backend): HTTP daemon skeleton (Phase 1a) — #10 (#14)
* 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.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 10:02:53 +05:30
harshitsinghbhandari 1fee0164e2 fix: restore revision monotonicity 2026-05-28 19:46:54 +05:30
harshitsinghbhandari fcb3aec988 fix: handle restore rollback and spawn id collisions 2026-05-28 17:25:02 +05:30
harshitsinghbhandari e66988ab77 fix: reconcile lifecycle writer contract 2026-05-28 17:16:26 +05:30
Harshit Singh Bhandari 7afa332ade
Merge pull request #16 from aoagents/fix/scm-observer-seam
fix: handle SCM observer seam facts
2026-05-28 17:14:42 +05:30
harshitsinghbhandari 1e715ecfa9 style: gofmt zellij runtime adapter 2026-05-28 16:20:13 +05:30
harshitsinghbhandari db6975eb71 fix: tighten zellij runtime attach and launch 2026-05-28 16:18:27 +05:30
harshitsinghbhandari 586931b3ff feat: add zellij runtime adapter 2026-05-27 23:05:08 +05:30
harshitsinghbhandari f0766ebd8f fix: handle SCM observer seam facts 2026-05-27 22:19:07 +05:30
Harshit Singh Bhandari bbaf6650e3
Merge pull request #13 from aoagents/feat/draft-pr-state
feat: handle draft PR lifecycle state
2026-05-27 20:37:42 +05:30
harshitsinghbhandari 09a82d9206 fix: keep draft PR reactions active 2026-05-27 20:30:14 +05:30
harshitsinghbhandari 77c01daf42 feat: handle draft PR lifecycle state 2026-05-27 18:34:42 +05:30
Harshit Singh Bhandari f9bb4ce90d
Merge pull request #11 from aoagents/aa-8-tmux-worktree-adapters
feat: add tmux runtime adapter
2026-05-27 18:26:39 +05:30
harshitsinghbhandari 37f1fe269e fix: harden tmux runtime teardown and ids 2026-05-27 18:20:03 +05:30
harshitsinghbhandari b5a344ac07 fix: use exact tmux targets 2026-05-27 17:17:59 +05:30
harshitsinghbhandari 9df3fb59bf feat: add git worktree workspace adapter 2026-05-27 17:12:40 +05:30
harshitsinghbhandari f975ca24c9 feat: add tmux runtime adapter 2026-05-27 16:58:03 +05:30
harshitsinghbhandari f03c7c8c88 fix(session): harden teardown/restore safety + drop dead reaction flag
Address PR #2 Copilot review comments on the merged LCM+SM lane:

- session: validate runtime handle + workspace path before Kill/Cleanup
  teardown; refuse (ErrIncompleteTeardownMetadata) or skip rather than
  hand empty args to a real adapter's Destroy (unsafe delete).
- session: reject Restore unless the session is terminal
  (ErrNotRestorable) so a live session can't spawn a duplicate
  runtime/workspace.
- ports: document SpawnConfig.OpenTerminal as reserved/not yet honored.
- lifecycle: remove the unread reactionConfig.auto field; note
  approved-and-green is notify-only (human decides to merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:21:47 +05:30
harshitsinghbhandari 4c331d907e test(session): cover spawn orphan-to-errored route and restore runtime rollback (review follow-ups)
Adds an injectable OnSpawnCompleted failure to the recording LCM and two tests:
- Spawn: when OnSpawnCompleted fails, the seeded record is parked terminal/errored
  (via OnKillRequested(KillError)) and runtime+workspace are torn down.
- Restore: when OnSpawnCompleted fails post-create, the new runtime is destroyed
  while the workspace is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:00:44 +05:30
harshitsinghbhandari 162881d2bd fix(session): harden Restore — require agent session id, roll back runtime on post-create failure (PR #7 review)
- Restore now fails early with a clear error if MetaAgentSessionID is missing,
  rather than emitting an ambiguous "resume nothing" launch command (no stored
  prompt means a fresh-launch fallback isn't possible).
- On a post-runtime-create failure (reopen patch or OnSpawnCompleted), best-effort
  destroy the newly created runtime (never the workspace, which holds prior work)
  so we don't strand a live process while parking the session terminal.
- Added a test for Restore with a missing agent session id: errors early, touches
  no workspace/runtime, leaves the session terminal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:50:24 +05:30
harshitsinghbhandari 538210844e feat(session): implement Session Manager (spawn/kill/list/get/send/restore/cleanup)
Implements ports.SessionManager against fakes for the outbound ports. The SM is
the explicit-mutation half of the lane: it drives Runtime/Agent/Workspace, seeds
the initial lifecycle, and routes outcomes to the LCM (OnSpawnCompleted /
OnKillRequested). It never derives observed state and is the single producer of
the derived display status (attached on read, never persisted).

- Spawn: Workspace.Create -> Runtime.Create (AO_* identity env) -> Seed ->
  OnSpawnCompleted, with eager rollback of completed steps on failure.
- Kill: OnKillRequested first -> Runtime.Destroy -> Workspace.Destroy, honoring
  the worktree-remove safety (refusal surfaced, never forced).
- List/Get: derive status via DeriveLegacyStatus. Send: via AgentMessenger.
  Restore: re-seed (reopen) + relaunch via GetRestoreCommand. Cleanup: reclaim
  terminal sessions, skip worktrees holding uncommitted work.

Store-contract additions (co-owned with Tom's persistence layer, flagged for
review): LifecycleStore.Seed (explicit create-with-identity; OnSpawnCompleted
requires a seeded record) and LifecycleStore.Get (single record-with-identity
read; Load is lifecycle-only). Lifecycle test fake updated to satisfy both.

Tests route through the real LCM Manager (wrapped to record call order).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:43:40 +05:30
harshitsinghbhandari 5081cf794b fix(lifecycle): kill clears trackers, send-failure budget, inclusive escalation (Copilot review)
- OnKillRequested now clears the session's escalation trackers after a
  successful kill, so a later duration-based TickEscalations can't emit
  reaction.escalated for a dead session (dispatch is still skipped).
- sendToAgent rolls back the attempt (and firstAttemptAt when it set it) on a
  messenger.Send error, so undelivered messages don't march a reaction toward
  escalation — honoring "send failures retry next tick" (§4.3).
- Duration escalation now uses an inclusive boundary (>=) in both shouldEscalate
  and TickEscalations, so a 30m reaction escalates at exactly 30m instead of
  waiting for the next tick.

Tests: kill clears trackers + no post-kill escalation; repeated failed delivery
never escalates; duration escalation fires at exactly escalateAfter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:11:00 +05:30
harshitsinghbhandari b2161d5582 fix(lifecycle): clear ci-failed tracker on recovery/incident-over (PR #6 review)
Address review finding #1: the persistent ci-failed tracker leaked and could
stale-silence a future regression. It was only cleared when leaving the
ci-failed reaction AND incidentOver held at that moment — so a recovery to
another open-PR state (ci-failed -> approved -> merged) never cleared it.

- react() now clears ALL of a session's trackers when the state REACHED is
  incident-over (PR resolved / session terminal) OR a genuine recovery
  (approved/mergeable, which the open-PR ladder guarantees means CI is no
  longer failing). Keyed on the state reached, not the one left, since the
  recovery transition is typically review_pending->approved (empty beforeKey).
- Persistent ci-failed still survives the ambiguous review_pending limbo, so
  fail->pending->fail keeps one shared budget (§4.2).
- Document the out-of-lock react() dispatch caveat for the daemon integration
  step (review #2) and the intentionally-skipped agent-stuck 10m threshold.

Tests: re-arm after a genuine recovery (regression re-nudges, not silenced);
all session trackers cleared once the incident is over.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:26:45 +05:30
harshitsinghbhandari 8b8da8e6a4 feat(lifecycle): ACT layer — reaction table + escalation engine (split B)
Add the ACT half of the LCM: map persisted status transitions to reactions
(send-to-agent / notify / auto-merge) and drive escalation.

- reactions.go: the §4.2 default reaction table, reactionEventFor (mirrors
  DeriveLegacyStatus for the ACT layer), in-memory per-(session,reaction)
  escalation trackers, the react() dispatch chokepoint, and a real
  TickEscalations for duration-based escalations the synchronous LCM can't
  wake itself for. auto-merge action exists but is off by default;
  bugbot-comments/merge-conflicts are configured but dormant (no decide-core
  producer yet).
- manager.go: mutate now returns a transition; each Apply* path fires the
  mapped reaction after persist via the single synchronous react() seam.
  OnKillRequested intentionally does not react (explicit kill != inferred
  event). Split-A load->decide->diff->persist behavior is unchanged.

ci-failed budget is persistent across fail->pending->fail oscillation;
non-persistent trackers reset when the status leaves the triggering state.
Escalation silences further auto-dispatch until the condition clears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:17:51 +05:30
harshitsinghbhandari 9eb5348604 feat(lifecycle): activity resolves detecting + review polish
Address Harshit's PR #5 review (approve w/ design confirms + polish):

- #1 (design decision): a valid activity signal is proof of life, so it now
  resolves a detecting session — writes the activity-mapped session state and
  clears the quarantine memory. Scoped to detecting only; a liveness-escalated
  stuck stays the probe pipeline's to resolve. Terminal still never reopens.
- #2: document why a merged/closed PR parks the session axis even over an
  activity-owned needs_input/blocked (a merge is a milestone), unlike the
  open-PR path that defers to activity.
- #3: map plain idle activity to a neutral session reason instead of the
  misleading research_complete (kept for ready, which implies completion).
- #6: cover all three kill kinds (manual/cleanup/error), the open-PR review
  branches (changes_requested/mergeable/review_pending), and the neutral idle
  reason. Coverage 86.5% -> 88.6%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 01:49:28 +05:30
harshitsinghbhandari 3945b10f20 fix(lifecycle): address PR #5 review
- shouldWriteSessionRuntime: never resurrect a terminal session; an
  observation may refresh the runtime axis but must touch neither the
  session axis nor the detecting memory (gated in ApplyRuntimeObservation).
- OnSpawnCompleted: error on an unseeded session instead of fabricating a
  partial record (SM must seed first — a missing seed is a contract violation).
- OnKillRequested: no-op on an unknown/already-gone session (benign race)
  instead of fabricating a terminal record.
- keyedMutex: reference-count entries and evict on last release so the lock
  map stays bounded in a long-running daemon.
- runtimeSubstateFromFacts: map RuntimeProbeIndeterminate to RuntimeUnknown
  with a neutral reason, distinct from the probe_error of a failed probe.

Adds tests for terminal non-resurrection, unseeded spawn-completed error,
and unknown-session kill no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 01:37:14 +05:30
harshitsinghbhandari 1420bb9493 feat(lifecycle): implement LCM Apply* pipeline (split A)
Implements ports.LifecycleManager as a synchronous observe->decide->persist
reducer. Every entrypoint runs the shared pipeline under a per-session lock:
load canonical -> run the matching pure decider -> diff into a sparse
merge-patch -> persist. Never polls, never writes the display status.

- ApplyRuntimeObservation -> probe decider; always writes the runtime axis.
- ApplySCMObservation -> open-PR / terminal-PR deciders (failed fetch is a
  no-op: failed probe != "no PR"). Open PRs write only the PR axis.
- ApplyActivitySignal -> updates the activity axis + maps onto the session
  axis; only valid-confidence signals are authoritative.
- OnSpawnCompleted -> runtime alive + handles to metadata; session stays
  not_started (display: spawning).
- OnKillRequested -> SM's explicit terminal-write authority.
- TickEscalations -> no-op stub (reaction/escalation engine is split B).

Composition rule (#1): liveness owns the runtime + death axis; activity owns
the working/idle/waiting axis. A healthy probe verdict writes the session axis
only to recover a liveness-owned state, so it never clobbers an activity-owned
needs_input/blocked. Activity is the mirror: it stays off the death axis.

Detecting clear (#3): a non-detecting probe verdict clears stale detecting
memory so the next probe reads no phantom Prior.

Built/tested against in-memory fakes (LifecycleStore with full merge-patch +
ExpectedRevision, recording Notifier/AgentMessenger). Per-session
serialisation verified under -race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 01:26:15 +05:30
harshitsinghbhandari 918c5b4b3a feat(decide): populate open-PR Evidence; document decision zero-value contract
Address Copilot review:
- ResolveOpenPRDecision now sets a stable, timestamp-free Evidence summary
  "<condition> #<num> <url>" for every ladder outcome, consuming the
  previously-unused OpenPRInput.Number/URL identity inputs and making PR
  decisions traceable in logs. Covered by TestResolveOpenPRDecisionEvidence.
- Document LifecycleDecision's zero-value contract: an empty PRState/PRReason
  means "this decider does not address PR — leave unchanged", not PRNone. The
  LCM must map empty PR fields to a nil LifecyclePatch.PR; writing PRNone on a
  probe tick would clobber a live PR. (Pointers were considered but the empty
  sentinel is distinguishable from every valid state and consistent with the
  codebase's value-enum style; LifecyclePatch already owns nil-means-unchanged.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:38:34 +05:30
harshitsinghbhandari cdfcdd2def style(decide): gofmt the timestampPatterns doc block
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:26:46 +05:30
harshitsinghbhandari fbfbcd5f1b docs(decide): document each timestampPatterns regex with examples
Clarify the timestamp-stripping block flagged in review: spell out what
each of the three regexes matches (full ISO/RFC3339 datetime, bare
time-of-day, bare unix epoch) and why order matters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:26:33 +05:30
harshitsinghbhandari 4d8a20676a refactor(decide): split type definitions into types.go; clarify detecting helper
Address PR review (aa-1):
- Move the decider input/output type definitions (LifecycleDecision,
  ProbeInput, ProcessLiveness, OpenPRInput, DetectingInput) out of the
  execution file into a dedicated types.go, leaving decide.go for behaviour.
- Expand the detecting() helper doc to spell out that it adapts a probe
  verdict into the shared CreateDetectingDecision anti-flap path so each
  probe branch doesn't re-implement the quarantine counter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:25:44 +05:30
harshitsinghbhandari 9920c6daaa fix(decide): reach mergeable without formal approval; broaden consistency test
Address PR review (aa-1):
- ResolveOpenPRDecision now keys MERGEABLE on Mergeable alone (checked
  before Approved). Mergeability is the authoritative merge gate, so a PR
  on a no-required-review repo no longer falls through to PR_OPEN. The
  approved+mergeable and approved-only cases are unchanged.
- Broaden the derive-consistency test to cover the probe and terminal
  deciders too, not just the open-PR ladder.
- Document the HashEvidence epoch-stripping regex's breadth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:06:57 +05:30
harshitsinghbhandari 6e90734276 feat(decide): implement pure DECIDE core with exhaustive truth-table tests
Replace the stubbed deciders in domain/decide with real, total,
side-effect-free implementations:

- ResolveProbeDecision: kill-intent short-circuits to terminal; failed
  probes and probe disagreement route to detecting; only runtime-dead +
  process-dead + no-recent-activity concludes killed.
- ResolveOpenPRDecision: the PR pipeline ladder
  (ci_failing > changes_requested > approved+mergeable > approved >
  review_pending > idle-beyond > open).
- ResolveTerminalPRStateDecision: merged -> idle/merged_waiting_decision,
  closed -> idle.
- CreateDetectingDecision: anti-flap quarantine — unchanged-evidence
  counter with StartedAt preserved across the episode so the duration cap
  is a real wall-clock safety net; escalates to stuck at 3 ticks or 5m.
- HashEvidence: strips timestamps/epochs and collapses whitespace before
  hashing so restamped-but-unchanged signals compare equal.

Table tests cover every branch (100% statement coverage), including a
consistency check that the open-PR ladder's display Status matches
DeriveLegacyStatus over the canonical state it emits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:54:55 +05:30
harshitsinghbhandari e8f60d0b27 refactor(backend): address contract review on LCM+SM ports
Review feedback on PR #2:

- Add CanonicalSessionLifecycle.Revision (monotonic write counter) distinct
  from the schema Version; LifecyclePatch.ExpectedVersion -> ExpectedRevision
  now compares it, so optimistic locking actually works.
- LifecycleStore.List returns []domain.SessionRecord (persistence shape, no
  derived status); add SessionRecord and make Session embed it. Keeps the
  Session Manager the single producer of the derived display status.
- SpawnOutcome.RuntimeHandle is now the structured ports.RuntimeHandle, not a
  string, so Destroy/SendMessage get the handle without ad-hoc encoding.
- Agent.IsProcessRunning -> ProbeProcess returning ProcessProbe (liveness),
  not domain.ActivityState; the name no longer implies a boolean.
- Document LifecyclePatch Detecting vs ClearDetecting precedence (clear wins).
- Correct the DeriveLegacyStatus doc: hard session states outrank PR facts;
  "PR dominates" applies only to the soft idle/working states. Implementation
  was already correct (matches canonical AO); only the comment overstated it.
- Replace personal-name attributions in package/interface comments with
  role-based terms (SCM poller / persistence adapter / API layer).

go build / go vet / go test all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:55:27 +05:30
harshitsinghbhandari d630319f04 feat(backend): LCM + Session Manager contract package (domain + ports)
Contract-first boundary for the Lifecycle Manager + Session Manager lane.
Pure shapes only — types, interfaces, and the display-status derivation —
so adil (SCM poller), Tom (persistence), and aditi (API) can review and
build against a stable boundary before any behaviour lands.

domain/
  - CanonicalSessionLifecycle: the only persisted state (session/pr/runtime
    sub-states), with Activity + Detecting sub-states added as decider inputs
    that must survive between observations.
  - DeriveLegacyStatus: the sole producer of the derived display status
    (never persisted), with 11 table tests.
ports/
  - inbound: LifecycleManager (Apply* pipeline, per-session serialised) and
    SessionManager.
  - outbound: LifecycleStore (Tom), Notifier, AgentMessenger, and the
    Runtime/Agent/Workspace plugin ports (co-owned with the agents lane).
  - facts: SCMFacts / RuntimeFacts / ActivitySignal DTOs.
decide/ pure-core signatures + I/O types; bodies stubbed for the next PR.

Folds in four design-review fixes (documented in-code, pending team confirm):
  1. Activity + Detecting persisted so the pure decider has memory across calls.
  2. Per-session serialisation documented; LifecyclePatch.ExpectedVersion
     offers optimistic-locking as an alternative.
  3. LifecyclePatch is a sparse pointer-field merge-patch (+ ClearDetecting).
  4. SCMFacts gains Fetched (failed fetch != "PR closed") and per-comment
     IsBot (bot vs human route to different reactions).

go build / go vet / go test all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:00:31 +05:30
harshitsinghbhandari a1fb470005 chore: scaffold backend/ and frontend/ skeletons for rewrite
Initial buildable skeleton for the agent-orchestrator rewrite, splitting
the repo into a Go backend daemon and an Electron + TypeScript frontend.

- backend/: go.mod (Go 1.22) + main.go that compiles and prints a startup line
- frontend/: package.json, strict tsconfig.json, Electron main-process stub
- .gitignore for Node/Electron/Go/OS/editor/env artifacts
- README note describing the new two-folder structure

No app logic or architecture layering yet (routes/controllers/services/etc.
come in a later task). go build and tsc --noEmit both pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:22:36 +05:30