Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [master]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm

- name: Install
run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test
133 changes: 95 additions & 38 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# cursor-glados — agent guide

GLaDOS finds open PRs with a pending review request for the authenticated user (GitHub Search), clones each PR locally, runs a Cursor SDK agent review, and posts the result back to GitHub (summary + inline comments, approve or request changes). The notifications CLI also clears the GitHub notification inbox.
GLaDOS reviews open PRs with a pending review request for the authenticated user, and replies when humans respond on its review-comment threads.

1. **Full review (Phase B):** GitHub Search finds `review-requested:@me` PRs → clone → Cursor SDK local agent → post summary + inline comments (`APPROVE` or `REQUEST_CHANGES`).
2. **Thread replies (Phase A):** On every PR notification wake-up (and before each full review) → inspect GLaDOS review threads → agree (reply + resolve) or disagree (reply, leave open). Settled agreements are never re-raised on later reviews of that PR.

TypeScript, ESM (`"type": "module"`), Node ≥ 22.13. Run scripts with `tsx`.

Expand All @@ -10,94 +13,148 @@ TypeScript, ESM (`"type": "module"`), Node ≥ 22.13. Run scripts with `tsx`.
export GLADOS_TOKEN='ghp_...' # GitHub PAT: search + notifications + repo + pull_requests
export CURSOR_API_KEY='cursor_...'

npm run notifications # review open PRs with pending review requests, clear inbox
npm run notifications # Phase A + Phase B for pending reviews; Phase A for PR notifications; clear inbox
npm run notifications -- --all # include read notifications when listing inbox
npm run smoke # local Cursor SDK smoke test (cwd = this repo)
npm run typecheck
npm test # unit tests (node:test via tsx)
npm run typecheck # TypeScript check (CI "lint")
```

CI (`.github/workflows/ci.yml`) runs `npm ci`, `typecheck`, and `test` on PRs and pushes to `master`.

`@connectrpc/connect-node` is required at runtime by `@cursor/sdk` but is not bundled — keep it in `package.json`.

## Layout

Only `src/cli/` contains runnable entrypoints. Everything else is library code imported by CLI or other modules.
Only `src/cli/` contains runnable entrypoints. Everything else is library code.

```
src/
cli/
notifications.ts # entrypoint: review pending PRs, then list + clear notifications
notifications.ts # entrypoint: review-requested PRs, then PR notification wake-ups, inbox cleanup
smoke.ts # entrypoint: one-shot local Agent.prompt smoke test

types.ts # NotificationThread, PullRequestRef

git/
workspace.ts # clone repo to temp dir, fetch + checkout PR branch
workspace.ts # clone to /tmp/glados-*/repository, PR lock, withPrWorkspace()

github/
notifications.ts # listNotifications(), markNotificationDone()
pr.ts # listReviewRequestedPullRequests(), isReviewRequestedForUser(), subjectUrlToWebUrl()
pr.ts # search review-requested PRs, pending-reviewer check, notification → PR ref
diff.ts # getCommentableLines() — RIGHT-side lines that accept comments
reviews.ts # postGithubReview() — validate vs diff, then createReview
threads.ts # GraphQL: list/get review threads, reply, resolve

review/
process.ts # processPrReview() — orchestrates full review flow
agent.ts # runAgentReview() — Cursor SDK Agent.prompt on local cwd
payload.ts # prompt, JSON parse, GitHub review formatting, personality hook
process.ts # processPrReview() — Phase A then Phase B wiring only
thread-process.ts # processPrThreadReplies() / processReviewThreads() — Phase A orchestration
agent.ts # runAgentReview() / runThreadReplies() — Cursor SDK + sandbox/env isolation
payload.ts # review prompt, parse, GitHub formatting, personality, strip control markers
threads.ts # pure: awaiting/settled classification, Phase A prompt/parse, suppress settled findings
```

## Review flow
Co-located `*.test.ts` files use Node’s built-in test runner (`npm test`).

## Flow

```
cli/notifications.ts
→ parallel:
github/pr.listReviewRequestedPullRequests() # search: is:open is:pr review-requested:@me
github/pr.listReviewRequestedPullRequests() # is:open is:pr review-requested:@me
github/notifications.listNotifications()
→ review/process.processPrReview() (per PR from search)
→ github/pr.isReviewRequestedForUser() # skip if not on pending reviewer list
→ git/workspace.preparePrWorkspace() # /tmp/glados-*/<repo>
→ review/agent.runAgentReview() # local Agent.prompt
→ review/payload.buildGithubReview() # APPROVE vs REQUEST_CHANGES
→ github/reviews.postGithubReview()
→ rm temp workspace
→ github/notifications.markNotificationDone() (per inbox thread — inbox cleanup only)
→ per review-requested PR:
review/process.processPrReview()
→ withPrLock
→ isReviewRequestedForUser() # skip if not on pending list
→ withPrWorkspace → /tmp/glados-*/repository
→ thread-process.processReviewThreads() # Phase A
→ agent.runAgentReview(+ settled context) # Phase B
→ suppressSettledFindings()
→ payload.buildGithubReview() → reviews.postGithubReview()
→ per PullRequest notification (wake-up only; GitHub coalesces PR activity):
thread-process.processPrThreadReplies() # Phase A only; clone only if a reply is needed
markNotificationDone() only if Phase A complete
→ other notifications: markNotificationDone()
```

**Review queue:** `listReviewRequestedPullRequests()` uses GitHub Search, not the notifications API. Search reflects pending review state directly; the inbox is cleared separately and does not drive which PRs get reviewed.
**Review queue:** Search drives which PRs get a full review. Notifications do **not** choose the review queue; every `PullRequest` notification is a Phase A wake-up to re-inspect thread state (do not rely on `latest_comment_url`).

**Duplicate protection:** before cloning for a full review, `isReviewRequestedForUser()` checks pending reviewers. After you submit a review you drop off that list until re-requested.

**Per-PR lock:** `withPrLock()` serializes overlapping local CLI runs so two processes cannot reply/review the same PR at once.

## Phase A — thread replies

**Awaiting:** root comment by GLaDOS, unresolved, not settled, and there is an unacknowledged human reply (anyone) after GLaDOS’s last acknowledgment.

**Agent:** `runThreadReplies()` with thread history + local checkout → JSON `{ replies: [{ threadId, decision: "agree"|"disagree", body }] }` — exactly one decision per awaiting thread.

**Duplicate protection:** before cloning, `isReviewRequestedForUser()` calls `GET .../pulls/{n}/requested_reviewers`. GitHub only lists users with a **pending** review request — once you submit a review you drop off; if someone re-requests you, you're back. Review only when the GLADOS user is on that list.
**Actions:**
- Agree → controlled reply (`<!-- glados:reply-to:… -->` + `<!-- glados:agree -->`) → `resolveReviewThread` when `viewerCanResolve`.
- Disagree → controlled reply with acknowledgment marker only; leave open. Back-and-forth continues indefinitely whenever someone replies after GLaDOS.
- Agreed but unresolved → retry resolve only (no new agent reply). If `viewerCanResolve` is false, leave Phase A incomplete so the notification is retained for later retry; still treat the issue as settled for Phase B.

**Tests:** GLaDOS does not run the test suite or install deps to execute tests. The PR's CI runs tests; the agent reviews test *code* by reading files only. This is enforced in `buildReviewPrompt()`.
**Race safety:** re-fetch the target thread immediately before each write; compare full comment id/body/`updatedAt` snapshots. After posting, reconcile — if a human raced in, the thread stays awaiting.

**Approve vs request changes:** `critical` or `high` findings → `REQUEST_CHANGES`; otherwise `APPROVE`.
**Markers:** appended only by `formatReplyBody()` / controlled code. Strip from all model-authored review text via `stripGladosControlMarkers()`. Root findings never count as agreements.

**Inline comments:** findings with `path` + `line` become review comments (`side: RIGHT`). The agent reviews the whole repo, so it can cite lines outside the diff — but GitHub only accepts RIGHT-side comments on lines present in the PR diff, and one bad anchor 422s the *entire* inline batch. So before posting, `github/diff.ts` → `getCommentableLines()` parses the PR diff hunks and `reviews.ts` filters comments against it: anchorable lines post inline, the rest are demoted into the review body (`appendCommentsToBody`). Findings without `path`/`line` go in the body too. A body-only 422 fallback remains as a last resort.
## Phase B — full review

Inject settled findings (original body + agreement reason) into `buildReviewPrompt()`. After parse, `suppressSettledFindings()` removes exact text repeats and regenerates the summary if anything was dropped. Open/disagreed threads do **not** suppress new findings.

**Approve vs request changes:** `critical` or `high` → `REQUEST_CHANGES`; else `APPROVE`.

**Inline comments:** `path` + `line` → RIGHT-side review comments. Anchors outside the PR diff are demoted into the body (`getCommentableLines` / `appendCommentsToBody`); one bad anchor would 422 the whole batch.

**Agent must not run tests/builds/installers** — review by reading files only (CI runs tests). Same rule for Phase A.

## Agent isolation (local Cursor SDK)

| Measure | Why |
|---------|-----|
| Checkout always under `…/repository` | Repo name must not become workspace policy (e.g. a repo named `.cursor`) |
| Agent `cwd` = temp parent of checkout | Repo `.cursor/sandbox.json` is review data, not active policy |
| Disposable `HOME` / XDG / `TMP*` under the temp workspace | No host `~/.ssh` or ambient `~/.cursor` |
| Environment allowlist | No ambient tokens/DSNs for agent child processes |
| `sandboxOptions: { enabled: true }` | Constrain tool execution |
| `settingSources: []` | No ambient Cursor settings layers |
| Git auth: `http.https://github.com/.extraHeader` + `GIT_LFS_SKIP_SMUDGE=1` | Scope PAT to GitHub; avoid LFS smudge/token leak |
| Auth not persisted in `.git/config` | Agent must not read the PAT from the clone |

`CURSOR_API_KEY` is passed explicitly to `Agent.prompt`; it is not left in the sanitized env.

## Key extension points

| What | Where |
|------|--------|
| Reviewer instructions / JSON schema | `review/payload.ts` → `buildReviewPrompt()` |
| Severity levels (single source of truth) | `review/payload.ts` → `SEVERITIES` const + `Severity` type |
| GLaDOS voice before posting | `review/payload.ts` → `applyPersonality()` |
| Clone/checkout behavior | `git/workspace.ts` |
| GitHub API (search, notifications, PR helpers, post review) | `github/` |
| Orchestration only — no business logic | `review/process.ts` |
| Severity levels | `review/payload.ts` → `SEVERITIES` + `Severity` |
| GLaDOS voice | `review/payload.ts` → `applyPersonality()` (post-time rewrite) |
| Thread reply prompt / classification / suppress | `review/threads.ts` |
| Phase A orchestration | `review/thread-process.ts` |
| Phase B wiring | `review/process.ts` (no business logic) |
| Clone / lock / cleanup | `git/workspace.ts` |
| GitHub I/O | `github/` |

Tune **review prompt**, **thread-reply prompt**, and **personality** independently.

Tune the **review prompt** and **personality** independently: prompt asks for structured JSON; `applyPersonality()` rewrites text at post time.
Design detail: `docs/superpowers/specs/2026-08-05-review-thread-replies-design.md`.

## Conventions

- **ESM imports** use `.js` extensions in TypeScript source (`import x from "./foo.js"`).
- **New runnable scripts** go in `src/cli/` only. Wire them in `package.json` scripts.
- **New library code** goes in the matching domain folder (`github/`, `git/`, `review/`), not a generic `utils/`.
- **Keep modules small:** `agent.ts` = SDK only, `payload.ts` = pure data/prompt/formatting, `process.ts` = wiring.
- **Minimize scope** on changes — match existing style, no over-abstraction.
- **ESM imports** use `.js` extensions in TypeScript source.
- **New runnable scripts** go in `src/cli/` only; wire in `package.json`.
- **New library code** goes in `github/`, `git/`, or `review/` — not a generic `utils/`.
- **Keep modules small:** `agent.ts` = SDK + isolation; `payload.ts` / `threads.ts` = pure data; `process.ts` / `thread-process.ts` = wiring.
- **Tests** live next to the code they cover (`*.test.ts`); add cases when changing classification, markers, locks, or suppression.
- **Minimize scope** — match existing style, no over-abstraction.

## Environment

| Variable | Used for |
|----------|----------|
| `GLADOS_TOKEN` | GitHub API (search, notifications, clone auth, post reviews) |
| `GLADOS_TOKEN` | GitHub API (search, notifications, clone auth, reviews, thread reply/resolve) |
| `CURSOR_API_KEY` | Cursor SDK local agent runs |

`GLADOS_TOKEN` needs access to arbitrary repos that send review requests (`repo` scope or equivalent fine-grained permissions).
`GLADOS_TOKEN` needs access to arbitrary repos that send review requests (`repo` scope or equivalent). Resolving conversations additionally requires being the PR author **or** having write access on the repo — when that fails, agreement still settles for Phase B but Phase A stays incomplete for retry.
116 changes: 116 additions & 0 deletions docs/superpowers/specs/2026-08-05-review-thread-replies-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Review thread replies — design

GLaDOS must respond when humans reply on its review comment threads: agree (short reply + resolve) or disagree (explain on the thread). Settled agreements are never re-raised on later reviews of the same PR. Disagreements can continue indefinitely whenever someone replies after GLaDOS.

## Decisions

| Topic | Choice |
|-------|--------|
| Triggers | Reply notifications **and** full re-review |
| Who counts | Anyone who replies on the thread |
| After agree | Resolve thread; permanently suppress that issue for the PR |
| Notification, no pending review | Phase A only (reply/resolve); no new review |
| Open disagreements on re-review | May restate as new findings **and** continue the old thread |
| Architecture | Two-phase agent (threads, then optional full review) |
| Memory | GitHub thread state only (no local DB) |

## Flow

```
cli/notifications.ts
├─ review-requested PRs → processPrReview()
│ 1. preparePrWorkspace()
│ 2. processReviewThreads() # Phase A
│ 3. runAgentReview(+ settled) # Phase B
│ 4. postGithubReview()
└─ inbox: reply on a GLaDOS review thread
→ processReviewThreads() only
→ markNotificationDone() only after successful processing
```

## Phase A — thread replies

**Awaiting thread:** Root review comment authored by GLaDOS, thread unresolved, last comment not by GLaDOS.

**Agent:** Given thread history + current code (local checkout), return per-thread `{ threadId, decision: "agree"|"disagree", body }`.

Comment history is untrusted prompt data. Agent runs use a disposable HOME and
temp directory, a fixed parent workspace (so repository `.cursor` files cannot
become active policy), an environment allowlist, and Cursor sandboxing. The
agent may read code only; tests, builds, installers, package managers, and
repository scripts are forbidden.

**Actions:**
- Agree → post short reply, resolve thread (GraphQL `resolveReviewThread`).
- Disagree → post reply explaining why; leave unresolved.
- Every reply carries a hidden acknowledgment of the exact human comment it
evaluated. If another human comment races before GLaDOS posts, the thread
remains awaiting and is reconsidered on the next pass.
- If an agree marker exists but the thread is still unresolved, retry resolution
without asking the agent or posting another reply.

Thread and comment connections must both be fully paginated so long-running
conversations are never truncated.

**Personality:** Same GLaDOS voice as review comments (via existing personality hook / prompt).

## Phase B — full review (review-requested only)

Inject **settled** threads into the review prompt: original finding text,
path/line, and the marked GLaDOS agreement explaining why it was settled. Rule:
do not re-raise those issues for this PR, even if resolution failed or the
thread was later unresolved (no revive if code changes). Exact repeated
findings are also removed after parsing; if removal occurs, regenerate the
summary so it cannot repeat the settled blocker.

Open / disagreed threads are **not** suppressors; new findings on the same topic are allowed.

## Modules

| Module | Responsibility |
|--------|----------------|
| `github/threads.ts` | List PR review threads (GraphQL), create thread reply, resolve thread; identify authenticated user |
| `review/threads.ts` | Pure: awaiting vs settled classification; prompt/parse for Phase A; format settled context for Phase B |
| `review/agent.ts` | `runThreadReplies()` for Phase A |
| `review/thread-process.ts` | Orchestrate Phase A thread reads, replies, resolution and retries |
| `review/process.ts` | Run Phase A, pass settled context into Phase B, post full review |
| `cli/notifications.ts` | Treat every PR notification as a fresh Phase A wake-up |

Keep `process.ts` as wiring only; GitHub I/O in `github/`; prompt/parse and
classification in `review/threads.ts`.

## GitHub API notes

- Threads + resolve: GraphQL (`pullRequest.reviewThreads`, `resolveReviewThread`).
- Replies: GraphQL `addPullRequestReviewThreadReply`, using the thread node ID.
This avoids deprecated/overflow-prone numeric comment IDs.
- Settled detection: agree replies include a hidden marker
(`<!-- glados:agree -->`) plus an acknowledgment marker appended only by
controlled code. The agreement is valid only if no unacknowledged human
comment preceded it. Root findings and model-provided markers do not count.
A valid marker permanently settles the issue for this PR even if the thread
is later unresolved; `isResolved` controls only whether GLaDOS needs to retry
the resolve mutation.
- GitHub coalesces PR notifications, so every `PullRequest` notification is
treated only as a wake-up to inspect complete thread state; do not rely on
`subject.latest_comment_url`.
- Before replying, re-fetch complete thread histories and reject a batch if any
comment was added or edited while the agent evaluated it. Re-fetch the target
thread again immediately before each write.
- Serialize each PR with a local process lock so overlapping CLI runs cannot
post duplicate or contradictory replies.
- Resolution is best-effort when `viewerCanResolve` is false (GitHub requires
the PR author or repository write access). The agreement remains settled and
later full reviews continue instead of becoming blocked, but Phase A remains
incomplete and its notification is retained so resolution is retried if
permissions later change.
- A failed Phase A leaves its notification unmarked so a later CLI run retries
it.

## Out of scope

- Real-time streaming / webhooks (CLI poll remains the runner)
- Local durable store of findings
- Resolving threads GLaDOS did not agree on
- Changing APPROVE / REQUEST_CHANGES rules beyond “don’t re-raise settled items”
Loading
Loading