Skip to content

Commit 7475f79

Browse files
salma-remyxclaude
andcommitted
Rework outrider into two paths: engine/App init + no-App setup-local
Replaces the single gh-only `outrider init` with two flows that both end in the same Outrider Action but differ in who provisions it: - `outrider init` — thin client over the engine's provision-action endpoints (REMYX-60 / remyx#337). The Remyx GitHub App provisions the workflow, secrets, and a bot-authored setup PR server-side; --mode auto|review|off. New API clients (remyxai/api/github_app.py, integrations.py, and provision_action/poll/status in interests.py), with App-install and model-provider preflights. - `outrider setup-local` — self-provisions with the customer's own gh (no third-party App, for orgs under security review): sets the repo secrets, writes the workflow, opens (auto: merges + dispatches) the setup PR, and enables the repo's Actions-can-create-PRs setting so the built-in GITHUB_TOKEN opens recommendation PRs. Reversible-first ordering with branch/PR rollback; Contents-API sha handled on update. Neither path stores a GitHub token as a repo secret — only REMYX_API_KEY and ANTHROPIC_API_KEY. Also: drop the bundled workflow template + package_data (both paths render inline now); log_api_response treats all 2xx as success (provision-action returns 202). Validated live end-to-end against salma-remyx/FFMPerative-test: engine path -> remyx-ai[bot] recommendation PR; setup-local -> github-actions[bot] recommendation PR. 50 unit tests across tests/cli/test_outrider_actions.py, tests/cli/test_outrider_local.py, tests/api/test_outrider_api.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6014c8c commit 7475f79

13 files changed

Lines changed: 1530 additions & 809 deletions

README.md

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -179,26 +179,67 @@ result, time_elapsed = run_inference(model_name, prompt, server_url="localhost:8
179179
print(result)
180180
```
181181

182-
### Outrider
183-
Install [Outrider](https://github.com/remyxai/outrider) — the recommendation-driven implementation agent — into a GitHub repository. The command opens a PR that adds `.github/workflows/outrider.yml`, then writes `REMYX_API_KEY` and `ANTHROPIC_API_KEY` to the repo's Actions secrets.
182+
### Outrider — weekly arXiv → draft PR for your repo
183+
184+
[Outrider](https://github.com/remyxai/outrider) is a GitHub Action that, on a weekly schedule, finds the most implementable recent paper for your repository and opens a draft PR wiring it into a real call site. `remyxai outrider` sets it up for you.
185+
186+
There are two ways to install it — both end with the same Action running in your repo; they differ in **who provisions it**:
187+
188+
| | `outrider init` | `outrider setup-local` |
189+
|---|---|---|
190+
| **Best for** | Most users | Teams that can't grant a third-party GitHub App yet (e.g. a pending security review) |
191+
| **How it works** | The **Remyx GitHub App** provisions it server-side; PRs are authored by `remyx-ai[bot]` | The CLI uses **your own `gh`** to provision it; PRs authored by `github-actions[bot]` |
192+
| **You provide** | The Remyx App installed on the repo (the command links you to it) | An authenticated `gh` with admin on the repo |
193+
194+
Either way you'll need a **Remyx API key** (from engine.remyx.ai → Settings) and an **Anthropic API key** (from console.anthropic.com). Set the Remyx key once:
184195

185-
* cli command:
186196
```bash
187-
# Auto-detect repo from the current git remote
188-
$ remyxai outrider init --auto-interest
197+
export REMYXAI_API_KEY=... # from engine.remyx.ai → Settings
198+
```
189199

190-
# Or target a specific repo + interest
191-
$ remyxai outrider init --repo owner/repo --interest <uuid>
200+
#### Option A — `outrider init` (via the Remyx GitHub App) — recommended
192201

193-
# Preview without making any changes
194-
$ remyxai outrider init --auto-interest --dry-run
202+
```bash
203+
# Set up on a repo, auto-creating a research interest from it:
204+
$ remyxai outrider init --repo owner/name --auto-interest
205+
206+
# …or use an existing research interest (UUID from engine.remyx.ai):
207+
$ remyxai outrider init --repo owner/name --interest <uuid>
195208
```
196209

197-
Requirements:
198-
- `gh` (GitHub CLI), authenticated with `repo` + `admin:repo_hook` scopes (the latter is what GitHub requires for writing Actions secrets)
199-
- `REMYXAI_API_KEY` and `ANTHROPIC_API_KEY` in the local environment — these are read once and written to the target repo's Actions secrets via `gh secret set`
210+
If the Remyx App isn't installed on the repo yet, the command prints an install link and waits. Then the engine provisions the workflow, repo secrets, and a setup PR, and — in the default `auto` mode — merges it and starts the first run. Connect your Anthropic key once on the engine's Integrations page, or pass `--anthropic-key` (or set `ANTHROPIC_API_KEY`) and the CLI connects it for you.
211+
212+
`--mode`: `auto` (default — set it up and start the first run), `review` (open the setup PR for you to merge), `off` (just create the research interest).
213+
214+
#### Option B — `outrider setup-local` (no GitHub App)
215+
216+
For teams that can't install a third-party App yet. The CLI uses your own authenticated `gh` to do everything — nothing new to security-review.
217+
218+
```bash
219+
$ export ANTHROPIC_API_KEY=... # stored as a repo secret by the CLI
220+
221+
$ remyxai outrider setup-local --repo owner/name --auto-interest
222+
```
223+
224+
Using your `gh` credentials, the CLI sets the `REMYX_API_KEY` + `ANTHROPIC_API_KEY` repo secrets, writes `.github/workflows/outrider.yml`, opens a setup PR, and — in `auto` mode — merges it and dispatches the first run. So the Action can open its recommendation PRs, the CLI enables the repo's *"Allow GitHub Actions to create and approve pull requests"* setting; the Action then uses the repo's built-in `GITHUB_TOKEN` (no GitHub token is stored as a secret — only `REMYX_API_KEY` and `ANTHROPIC_API_KEY`).
225+
226+
Requires `gh` authenticated (`gh auth login`, or `$GITHUB_TOKEN` with `repo` + `workflow` scopes) and admin on the target repo.
227+
228+
#### Common options
229+
230+
- `--repo owner/name` — target repo (or a GitHub URL); defaults to the current directory's git remote.
231+
- `--interest <uuid>` / `--auto-interest` — use an existing research interest, or create one from the repo.
232+
- `--mode auto|review` (`init` also has `off`) — how far to take setup.
233+
- `--dry-run` — print the plan (and, for `setup-local`, the rendered workflow) and exit without making changes.
234+
- `-y` / `--yes` — skip the confirmation prompt.
235+
236+
Preview either path safely before committing to it:
237+
238+
```bash
239+
$ remyxai outrider setup-local --repo owner/name --auto-interest --dry-run
240+
```
200241

201-
**Credential model (v1):** the operator's own `REMYXAI_API_KEY` is copied into the target repo's Actions secrets. Anyone with write access to the repo's workflows can therefore consume Remyx credits attributed to that key. This is acceptable for repos you control end-to-end; for shared or third-party repos, prefer provisioning a scoped key in the Remyx dashboard before running `outrider init`. A first-class scoped-token flow is tracked in REMYX-76.
242+
**A note on credentials:** with `setup-local`, your `REMYXAI_API_KEY` is stored as the repo's `REMYX_API_KEY` secret so the Action can fetch recommendations — anyone with write access to the repo's workflows can consume Remyx credits on that key, so use it on repos you control. With `outrider init`, the Remyx App provisions a scoped key for you. In both paths your Anthropic key is stored as a repo secret to run the agent.
202243

203244
### User
204245

remyxai/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def get_headers(api_key=None):
6767

6868
def log_api_response(response):
6969
"""Log the response from the API based on the status code."""
70-
if response.status_code in [200, 201]:
70+
if 200 <= response.status_code < 300:
7171
logging.debug(
7272
f"API call successful: {response.url}, Status: {response.status_code}"
7373
)

remyxai/api/github_app.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
remyxai/api/github_app.py
3+
4+
Client calls for the Remyx GitHub App (remyx-ai[bot]) endpoints.
5+
Wraps /api/v1.0/github/app/* in engine/app/api/github_app_api.py.
6+
7+
The App is what lets the engine provision the Outrider recommendation
8+
workflow, set repo secrets, and open bot-authored PRs without the user
9+
granting a personal token. Installing the App is an interactive browser
10+
step; these endpoints let the CLI surface the install link and poll for
11+
completion.
12+
"""
13+
from __future__ import annotations
14+
15+
import logging
16+
import requests
17+
from typing import Any, Dict, Optional
18+
19+
from . import BASE_URL, HEADERS, get_headers, log_api_response
20+
21+
logger = logging.getLogger(__name__)
22+
23+
24+
def _h(api_key: Optional[str] = None) -> dict:
25+
return get_headers(api_key) if api_key else HEADERS
26+
27+
28+
def get_app_install_url(api_key: Optional[str] = None) -> Dict[str, Any]:
29+
"""Return the Remyx GitHub App install URL for the current user.
30+
31+
Calls GET /api/v1.0/github/app/install-url
32+
33+
Returns { install_url, state, app_slug, configured }. When the App
34+
isn't configured on the server, returns { configured: False, error }.
35+
"""
36+
r = requests.get(
37+
f"{BASE_URL}/github/app/install-url", headers=_h(api_key), timeout=30
38+
)
39+
log_api_response(r)
40+
if r.status_code == 503:
41+
return r.json() # {configured: False, error}
42+
r.raise_for_status()
43+
return r.json()
44+
45+
46+
def is_app_installed(repo: str, api_key: Optional[str] = None) -> bool:
47+
"""Whether the Remyx App is installed on `repo` (owner/name).
48+
49+
Calls GET /api/v1.0/github/app/installation?repo=owner/name
50+
"""
51+
r = requests.get(
52+
f"{BASE_URL}/github/app/installation",
53+
params={"repo": repo},
54+
headers=_h(api_key),
55+
timeout=30,
56+
)
57+
log_api_response(r)
58+
r.raise_for_status()
59+
return bool(r.json().get("installed"))

remyxai/api/integrations.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
remyxai/api/integrations.py
3+
4+
Client calls for the JWT integrations API.
5+
Wraps /api/v1.0/integrations/* in engine/app/api/integrations.py.
6+
7+
Used by the CLI to connect/check credential-based providers — notably
8+
the model provider (`claude_code` → Anthropic key) that Outrider needs
9+
the engine to push to the repo as a secret.
10+
"""
11+
from __future__ import annotations
12+
13+
import logging
14+
import requests
15+
from typing import Any, Dict, Optional
16+
17+
from . import BASE_URL, HEADERS, get_headers, log_api_response
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def _h(api_key: Optional[str] = None) -> dict:
23+
return get_headers(api_key) if api_key else HEADERS
24+
25+
26+
def get_integration_status(
27+
provider: str, api_key: Optional[str] = None
28+
) -> Dict[str, Any]:
29+
"""Connection status for a provider.
30+
31+
Calls GET /api/v1.0/integrations/<provider>/status
32+
Returns { connected: bool, connection?: {...} }.
33+
"""
34+
r = requests.get(
35+
f"{BASE_URL}/integrations/{provider}/status",
36+
headers=_h(api_key),
37+
timeout=30,
38+
)
39+
log_api_response(r)
40+
r.raise_for_status()
41+
return r.json()
42+
43+
44+
def connect_credential(
45+
provider: str,
46+
credentials: Dict[str, Any],
47+
api_key: Optional[str] = None,
48+
) -> Dict[str, Any]:
49+
"""Connect a credential-based provider (e.g. claude_code).
50+
51+
Calls POST /api/v1.0/integrations/<provider>/connect
52+
Body is the provider's credential dict, e.g. {"api_key": "sk-ant-..."}.
53+
The engine validates the key against the provider before storing it
54+
encrypted. Returns { connection, user_info } on success.
55+
"""
56+
r = requests.post(
57+
f"{BASE_URL}/integrations/{provider}/connect",
58+
json=credentials,
59+
headers=_h(api_key),
60+
timeout=30,
61+
)
62+
log_api_response(r)
63+
r.raise_for_status()
64+
return r.json()

remyxai/api/interests.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,93 @@ def toggle_interest(
164164
log_api_response(r)
165165
r.raise_for_status()
166166
return r.json()
167+
168+
169+
# ─── Outrider provisioning (REMYX-60) ──────────────────────────────────────
170+
171+
def provision_action(
172+
interest_id: str,
173+
repo_url: Optional[str] = None,
174+
auto_merge: bool = True,
175+
branch: Optional[str] = None,
176+
workflow_filename: Optional[str] = None,
177+
api_key: Optional[str] = None,
178+
) -> Dict[str, Any]:
179+
"""
180+
Provision the Outrider recommendation Action on a repo, server-side
181+
(the Remyx GitHub App sets secrets, writes the workflow, opens a
182+
bot-authored setup PR, and — when auto_merge — merges it and fires
183+
the first run).
184+
185+
Calls POST /api/v1.0/interests/<id>/provision-action
186+
187+
Args:
188+
repo_url: GitHub URL; defaults to the interest's source repo.
189+
auto_merge: merge the setup PR + dispatch the first run ("set it
190+
up for me"). False opens the PR for the user to review.
191+
192+
Returns 202 { task_id, status_url }. Poll with poll_provision_action.
193+
"""
194+
payload: Dict[str, Any] = {"auto_merge": auto_merge}
195+
if repo_url:
196+
payload["repo_url"] = repo_url
197+
if branch:
198+
payload["branch"] = branch
199+
if workflow_filename:
200+
payload["workflow_filename"] = workflow_filename
201+
202+
r = requests.post(
203+
f"{BASE_URL}/interests/{interest_id}/provision-action",
204+
json=payload,
205+
headers=_h(api_key),
206+
timeout=30,
207+
)
208+
log_api_response(r)
209+
r.raise_for_status()
210+
return r.json()
211+
212+
213+
def poll_provision_action(
214+
interest_id: str,
215+
task_id: str,
216+
api_key: Optional[str] = None,
217+
) -> Dict[str, Any]:
218+
"""
219+
Poll a provision-action task.
220+
221+
Calls GET /api/v1.0/interests/<id>/provision-action/<task_id>
222+
223+
Returns the task dict: { status, progress, message, result, error }.
224+
On completion, `result` carries pr_url, secret_set, merged, dispatched,
225+
model_key_missing, key_label, repo.
226+
"""
227+
r = requests.get(
228+
f"{BASE_URL}/interests/{interest_id}/provision-action/{task_id}",
229+
headers=_h(api_key),
230+
timeout=30,
231+
)
232+
log_api_response(r)
233+
r.raise_for_status()
234+
return r.json()
235+
236+
237+
def get_provision_status(
238+
interest_id: str,
239+
api_key: Optional[str] = None,
240+
) -> Dict[str, Any]:
241+
"""
242+
Latest provisioning state for an interest.
243+
244+
Calls GET /api/v1.0/interests/<id>/provision-action
245+
246+
Returns { provisioned: False } if none, else the provisioned-action
247+
record with provisioned: True.
248+
"""
249+
r = requests.get(
250+
f"{BASE_URL}/interests/{interest_id}/provision-action",
251+
headers=_h(api_key),
252+
timeout=30,
253+
)
254+
log_api_response(r)
255+
r.raise_for_status()
256+
return r.json()

0 commit comments

Comments
 (0)