* feat(session): support multiple PRs per session
A session can now own several pull requests (a root plus stacked
children) instead of being capped at one. The SQLite schema was already
1-session->many-PR (pr.url PK, session_id a plain FK), so this is a
behavioural change across the observe -> persist -> derive -> react
pipeline, not a migration.
- observe: the SCM observer discovers every open PR whose source branch
matches a session branch or descends from it ("branch/..." stacking),
attributing each to the owning session; the longest matching branch
wins so a child session claims its own stacked PRs.
- derive: session status is a worst-wins aggregate over all owned PRs,
with a stack model (B is a child of A iff B.target == A.source and A is
open) exposed via prs[] on every session read DTO.
- react: per-PR reactions; a stacked child blocked by an open parent is
exempt from the rebase/merge-conflict nudge (only the bottom of the
stack is eligible), and the session completes only when no PR is open
and at least one merged.
- tests: unit coverage across stack/status/observer/lifecycle, a
real-SQLite ListPRFactsForSession test for the stacked-PR read path,
and a functional end-to-end integration test driving the real store +
lifecycle + observer through attribution, completion, and stacked-child
nudge suppression.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(scm): ignore fork heads in PR attribution and persist discovered siblings before completion
Branch-prefix attribution now requires a discovered PR's head branch to live
in the project repo. A fork PR can reuse a session's branch name while its
commits live in the fork, so the previous code could auto-claim foreign work.
Carry head repo full_name from the REST list response and skip any PR whose
head repo is not the base repo.
discoverNewPRs also writes each newly discovered PR as an open baseline row
before the refresh/lifecycle pass runs. A session can own several PRs, and a
terminal observation triggers a completion check that reads all of the
session's PRs from the store. Without the early write, an open sibling found
in the same poll was not yet durable and the session could terminate while
that PR was still open.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(session): surface actionable signals from blocked stacked children; clarify worker prompt
Status aggregation previously dropped any open PR blocked by an open parent,
hiding actionable child signals (failing CI, draft, requested changes,
unresolved comments) behind the parent's status. A blocked child still cannot
merge, so its readiness signals (mergeable/approved/review-pending/open) stay
suppressed, but its problem signals now contribute to the worst-wins aggregate.
The all-blocked fallback is preserved so a session never goes dark.
The worker multi-PR prompt said independent PRs could branch off the base
branch as usual, which conflicts with branch-prefix attribution. Clarify that
a PR may target the base branch, but its source branch must stay under the
session branch namespace for AO to track it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* 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>
* 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>