Skip to content

Commit b085a77

Browse files
smellslikemlclaude
andauthored
outrider CLI: --bulk-repos, --no-cron on setup-local, install-paths docs (REMYX-147) (#43)
Three CLI improvements driven by Wave 1-3 onboarding pain: - `--bulk-repos PATH` on `outrider init` + `outrider setup-local`: a TSV-driven loop (`owner/name<TAB>uuid` per row). Per-row errors are captured and reported in a summary at the end — one failure does not abort the remaining rows. `--pace SECONDS` tunes inter-row spacing (default 3s). Mutually exclusive with --repo / --interest / --auto-interest. - `--no-cron` on `setup-local`: workflow renders with the schedule block commented-out (not removed). Re-enable later by uncommenting three lines — no need to re-run setup-local. Engine-side `init --no-cron` is not yet wired; deferred to a follow-up that needs engine cooperation. - New docs/install-paths.md covers init vs setup-local trade-offs, the App-install dependency (and what happens if you write the workflow manually), `--no-cron`, and the `--bulk-repos` workflow. README gets a one-sentence pointer to the doc. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0d5a8a8 commit b085a77

6 files changed

Lines changed: 515 additions & 11 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ Once Outrider is installed, `remyxai outrider trigger` dispatches an ad-hoc run
134134
135135
If the Remyx GitHub App isn't installed on the target repo yet, the command surfaces the install link.
136136

137+
For the no-App `setup-local` path, the `--no-cron` switch, and `--bulk-repos` onboarding across many repos at once, see [docs/install-paths.md](docs/install-paths.md).
138+
137139
## Command reference
138140

139141
Run any command with `--help` for full flag listings and examples.

docs/install-paths.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
type: howto
3+
description: Two paths to install Outrider on a repo, when to use each, and how to onboard many repos at once.
4+
tags: [outrider, install, github-app, setup-local, bulk-repos]
5+
---
6+
7+
# Installing Outrider on a repo
8+
9+
There are two CLI paths to install Outrider on a target repo, plus a bulk path for onboarding many repos in one shot.
10+
11+
12+
## `outrider init` — the default
13+
14+
Drives the Remyx engine to register the **Remyx GitHub App** (`remyx-ai[bot]`) on the target, write the workflow, set the repo's Actions secrets, and open a bot-authored setup PR. In `auto` mode it also merges the PR and fires the first run.
15+
16+
```bash
17+
remyxai outrider init --repo owner/name --auto-interest
18+
```
19+
20+
The App is the load-bearing piece. At runtime, the workflow asks the engine to mint a short-lived `remyx-ai[bot]` token so the action's PRs and Issues are authored by the bot — not `github-actions[bot]`. **Without the App installed on the target, that token-mint silently returns empty and the action falls back to the workflow's built-in `GITHUB_TOKEN`** — the run still succeeds but the artifacts are anonymous and skip the convention-pass enrichments that key off the bot author. Always prefer this path when the org can grant the App.
21+
22+
If the App isn't installed yet, the command surfaces the install link — accept it once and the engine handles the rest.
23+
24+
25+
## `outrider setup-local` — when the App can't be granted
26+
27+
For enterprises that can't grant a third-party App yet (pending security review, restricted org policy):
28+
29+
```bash
30+
remyxai outrider setup-local --repo owner/name --auto-interest
31+
```
32+
33+
Uses your own authenticated `gh` CLI to set the repo secrets, write the workflow, and (in `auto` mode) merge the setup PR. No Remyx App, nothing new to security-review. The only Remyx dependency is the `REMYX_API_KEY` the workflow uses at runtime.
34+
35+
The action then opens its PRs / Issues with the repo's built-in `GITHUB_TOKEN` (authored by `github-actions[bot]` rather than `remyx-ai[bot]`).
36+
37+
### `--no-cron`
38+
39+
By default the workflow ships with a scheduled cron at 14:00 UTC Mondays. For trials, cost control, or any "I'll dispatch manually" use case:
40+
41+
```bash
42+
remyxai outrider setup-local --repo owner/name --interest <uuid> --no-cron
43+
```
44+
45+
The schedule block is rendered commented-out (not removed entirely), so re-enabling later means uncommenting three lines — no need to re-run setup-local.
46+
47+
Engine-side `outrider init --no-cron` is not yet supported; for now, prefer `setup-local --no-cron` if you need that knob.
48+
49+
50+
## Don't write the workflow file manually
51+
52+
Committing `.github/workflows/outrider.yml` by hand (without one of the two CLI paths above) skips the App-install / secrets-set steps. The action will deploy and run, but:
53+
54+
- The bot-token mint silently fails → PRs are `github-actions[bot]`-authored
55+
- The `REMYX_API_KEY` / `ANTHROPIC_API_KEY` Actions secrets must be set manually
56+
57+
If you've already done this and your runs are producing 0 artifacts, run `remyxai outrider init --repo owner/name --interest <uuid>` after the fact — it's idempotent for the secrets-set step and surfaces the App install link if needed.
58+
59+
60+
## `--bulk-repos` — many repos in one shot
61+
62+
Onboarding a portfolio of forks or installing across a team's repos:
63+
64+
```bash
65+
remyxai outrider init --bulk-repos repos.tsv --mode review --yes
66+
```
67+
68+
`repos.tsv` is a tab-separated mapping of repo → ResearchInterest UUID, one per line. Blank lines and `#`-prefixed comments are allowed:
69+
70+
```
71+
# wave-1 forks
72+
smellslikeml/agents 649828cc-11ec-4ce5-9549-303d1da6f1ce
73+
smellslikeml/helicone 734a06d2-84c0-4b3f-bb5f-d0ab476b36a9
74+
75+
# wave-2
76+
smellslikeml/promptfoo 3ede9f1b-ac4b-4468-9718-7314acd85fce
77+
```
78+
79+
Per-repo errors are captured and reported at the end — one failure does not abort the remaining rows. Use `--pace SECONDS` to tune the inter-repo pacing (default 3s).
80+
81+
`--bulk-repos` is also supported on `setup-local` with the same TSV format.
82+
83+
`--bulk-repos` is mutually exclusive with `--repo` / `--interest` / `--auto-interest` (the TSV is the source of truth for the per-row pair).

remyxai/cli/commands.py

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
handle_interests_toggle,
2626
)
2727
from remyxai.cli.outrider_actions import (
28+
_parse_bulk_repos_tsv,
29+
_run_bulk,
2830
handle_outrider_init,
2931
handle_outrider_trigger,
3032
)
@@ -595,13 +597,22 @@ def outrider():
595597
"Don't block polling for the App install or provisioning to "
596598
"finish; print next steps and return."
597599
))
600+
@click.option("--bulk-repos", "bulk_repos", type=click.Path(), default=None,
601+
help=(
602+
"Path to a TSV mapping repos to ResearchInterest UUIDs "
603+
"(\"owner/name<TAB>uuid\"). Loops `init` over each row "
604+
"sequentially. Mutually exclusive with --repo / --interest "
605+
"/ --auto-interest."
606+
))
607+
@click.option("--pace", "pace_s", type=int, default=3, show_default=True,
608+
help="Seconds to wait between repos in --bulk-repos mode.")
598609
@click.option("--dry-run", is_flag=True, default=False,
599610
help="Print the plan and exit without making any changes.")
600611
@click.option("--yes", "-y", "skip_confirm", is_flag=True, default=False,
601612
help="Skip the confirmation prompt (default is opt-in).")
602613
def outrider_init(
603614
repo, interest_id, auto_interest, mode, anthropic_key,
604-
no_wait, dry_run, skip_confirm,
615+
no_wait, bulk_repos, pace_s, dry_run, skip_confirm,
605616
):
606617
"""
607618
Set up Outrider on a GitHub repo.
@@ -623,7 +634,30 @@ def outrider_init(
623634
remyxai outrider init --repo remyxai/RepoRanger --auto-interest
624635
625636
remyxai outrider init --repo owner/name --interest <uuid> --mode review
626-
"""
637+
638+
remyxai outrider init --bulk-repos repos.tsv --mode review --yes
639+
"""
640+
if bulk_repos:
641+
if repo or interest_id or auto_interest:
642+
raise click.UsageError(
643+
"--bulk-repos is mutually exclusive with "
644+
"--repo / --interest / --auto-interest."
645+
)
646+
rows = _parse_bulk_repos_tsv(bulk_repos)
647+
_run_bulk(
648+
handle_outrider_init,
649+
rows,
650+
common_kwargs=dict(
651+
auto_interest=False,
652+
mode=mode,
653+
anthropic_key=anthropic_key,
654+
skip_confirm=skip_confirm,
655+
dry_run=dry_run,
656+
no_wait=no_wait,
657+
),
658+
pace_s=pace_s,
659+
)
660+
return
627661
handle_outrider_init(
628662
repo=repo,
629663
interest_id=interest_id,
@@ -652,13 +686,29 @@ def outrider_init(
652686
@click.option("--anthropic-key", "anthropic_key", default=None,
653687
help="Anthropic API key to set as the ANTHROPIC_API_KEY repo "
654688
"secret. Falls back to $ANTHROPIC_API_KEY, then prompts.")
689+
@click.option("--no-cron", "no_cron", is_flag=True, default=False,
690+
help=(
691+
"Render the workflow with the scheduled cron commented "
692+
"out. Manual `workflow_dispatch` / `remyxai outrider "
693+
"trigger` only. Re-enable later by uncommenting the "
694+
"schedule block."
695+
))
696+
@click.option("--bulk-repos", "bulk_repos", type=click.Path(), default=None,
697+
help=(
698+
"Path to a TSV mapping repos to ResearchInterest UUIDs "
699+
"(\"owner/name<TAB>uuid\"). Loops `setup-local` over each "
700+
"row sequentially. Mutually exclusive with --repo / "
701+
"--interest / --auto-interest."
702+
))
703+
@click.option("--pace", "pace_s", type=int, default=3, show_default=True,
704+
help="Seconds to wait between repos in --bulk-repos mode.")
655705
@click.option("--dry-run", is_flag=True, default=False,
656706
help="Print the plan + rendered workflow and exit; no changes.")
657707
@click.option("--yes", "-y", "skip_confirm", is_flag=True, default=False,
658708
help="Skip the confirmation prompt (default is opt-in).")
659709
def outrider_setup_local(
660710
repo, interest_id, auto_interest, mode, anthropic_key,
661-
dry_run, skip_confirm,
711+
no_cron, bulk_repos, pace_s, dry_run, skip_confirm,
662712
):
663713
"""
664714
Set up Outrider WITHOUT the Remyx GitHub App.
@@ -681,7 +731,31 @@ def outrider_setup_local(
681731
682732
remyxai outrider setup-local --repo owner/name --interest <uuid> \\
683733
--mode review
684-
"""
734+
735+
remyxai outrider setup-local --bulk-repos repos.tsv --mode review --yes \\
736+
--no-cron
737+
"""
738+
if bulk_repos:
739+
if repo or interest_id or auto_interest:
740+
raise click.UsageError(
741+
"--bulk-repos is mutually exclusive with "
742+
"--repo / --interest / --auto-interest."
743+
)
744+
rows = _parse_bulk_repos_tsv(bulk_repos)
745+
_run_bulk(
746+
handle_outrider_setup_local,
747+
rows,
748+
common_kwargs=dict(
749+
auto_interest=False,
750+
mode=mode,
751+
anthropic_key=anthropic_key,
752+
skip_confirm=skip_confirm,
753+
dry_run=dry_run,
754+
no_cron=no_cron,
755+
),
756+
pace_s=pace_s,
757+
)
758+
return
685759
handle_outrider_setup_local(
686760
repo=repo,
687761
interest_id=interest_id,
@@ -690,6 +764,7 @@ def outrider_setup_local(
690764
anthropic_key=anthropic_key,
691765
skip_confirm=skip_confirm,
692766
dry_run=dry_run,
767+
no_cron=no_cron,
693768
)
694769

695770

remyxai/cli/outrider_actions.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,103 @@ def handle_outrider_init(
391391
)
392392

393393

394+
# ─── bulk-repos onboarding ────────────────────────────────────────────────
395+
396+
def _parse_bulk_repos_tsv(path: str) -> list:
397+
"""Read a TSV mapping repos to ResearchInterest UUIDs.
398+
399+
Format: one row per repo, two tab-separated columns:
400+
401+
owner/name<TAB>interest-uuid
402+
403+
Blank lines and ``#``-prefixed comments are skipped. Lines with the
404+
wrong column count or a malformed UUID are surfaced with their line
405+
number so the caller can fix-then-retry without partial state.
406+
407+
Returns: list of ``(repo, interest_uuid)`` tuples in file order.
408+
"""
409+
if not os.path.exists(path):
410+
raise click.UsageError(f"--bulk-repos file not found: {path}")
411+
rows = []
412+
errors = []
413+
with open(path) as fh:
414+
for line_no, raw in enumerate(fh, start=1):
415+
line = raw.rstrip("\n").rstrip("\r")
416+
stripped = line.strip()
417+
if not stripped or stripped.startswith("#"):
418+
continue
419+
parts = line.split("\t")
420+
if len(parts) != 2:
421+
errors.append(
422+
f"line {line_no}: expected 2 tab-separated columns "
423+
f"(repo<TAB>interest_uuid), got {len(parts)}: {line!r}"
424+
)
425+
continue
426+
repo_raw, uuid_raw = parts[0].strip(), parts[1].strip()
427+
repo = _normalize_repo(repo_raw)
428+
if not repo:
429+
errors.append(
430+
f"line {line_no}: not a valid GitHub repo: {repo_raw!r}"
431+
)
432+
continue
433+
if not UUID_RE.match(uuid_raw):
434+
errors.append(
435+
f"line {line_no}: not a valid UUID: {uuid_raw!r}"
436+
)
437+
continue
438+
rows.append((repo, uuid_raw))
439+
if errors:
440+
raise click.UsageError(
441+
"--bulk-repos parse errors:\n " + "\n ".join(errors)
442+
)
443+
if not rows:
444+
raise click.UsageError(
445+
f"--bulk-repos file {path!r} contains no installable rows."
446+
)
447+
return rows
448+
449+
450+
def _run_bulk(
451+
handler, rows, common_kwargs, pace_s=3,
452+
echo=click.echo,
453+
):
454+
"""Run ``handler(repo=..., interest_id=..., **common_kwargs)`` per row.
455+
456+
Per-row exceptions are captured and reported in a summary at the end —
457+
one failure does not abort the remaining rows, since the most common
458+
error class (an already-installed fork, a permission edge case) is
459+
independent across repos.
460+
461+
Returns: list of ``(repo, status)`` tuples where status is ``"ok"`` or
462+
the exception message. Caller can post-filter for retry.
463+
"""
464+
import time
465+
466+
results = []
467+
for i, (repo, uuid) in enumerate(rows, start=1):
468+
echo(f"\n── [{i}/{len(rows)}] {repo} ──")
469+
try:
470+
handler(repo=repo, interest_id=uuid, **common_kwargs)
471+
results.append((repo, "ok"))
472+
except click.ClickException as e:
473+
echo(f" ✗ {e.message}")
474+
results.append((repo, e.message))
475+
except Exception as e:
476+
echo(f" ✗ {type(e).__name__}: {e}")
477+
results.append((repo, f"{type(e).__name__}: {e}"))
478+
if i < len(rows) and pace_s > 0:
479+
time.sleep(pace_s)
480+
481+
# Summary
482+
ok = [r for r, s in results if s == "ok"]
483+
failed = [(r, s) for r, s in results if s != "ok"]
484+
echo("")
485+
click.secho(f"== summary: {len(ok)}/{len(rows)} ok ==", bold=True)
486+
for repo, msg in failed:
487+
echo(f" ✗ {repo}: {msg}")
488+
return results
489+
490+
394491
# ─── outrider trigger ─────────────────────────────────────────────────────
395492

396493
WORKFLOW_FILENAME = "outrider.yml"

remyxai/cli/outrider_local.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,23 @@ def _gh_dispatch(repo: str, branch: str) -> bool:
205205

206206
# ─── workflow rendering (inline; no Remyx App / bot-token step) ─────────────
207207

208-
def _render_local_workflow(interest_id: str) -> str:
208+
def _render_local_workflow(interest_id: str, no_cron: bool = False) -> str:
209209
# No github-token input → the action uses this repo's built-in
210210
# GITHUB_TOKEN, which setup-local authorizes to open PRs.
211+
#
212+
# When ``no_cron=True``, the schedule block is rendered commented-out
213+
# (not omitted) so the user can re-enable scheduled runs later by
214+
# uncommenting three lines, without re-running setup-local.
215+
if no_cron:
216+
schedule_block = (
217+
" # schedule:\n"
218+
" # - cron: '0 14 * * 1' # Mondays 14:00 UTC; uncomment to enable\n"
219+
)
220+
else:
221+
schedule_block = (
222+
" schedule:\n"
223+
" - cron: '0 14 * * 1' # Mondays 14:00 UTC; pick any cadence\n"
224+
)
211225
return f"""name: Outrider
212226
213227
# Generated by `remyxai outrider setup-local` (no Remyx GitHub App).
@@ -216,9 +230,7 @@ def _render_local_workflow(interest_id: str) -> str:
216230
# https://github.com/remyxai/outrider
217231
218232
on:
219-
schedule:
220-
- cron: '0 14 * * 1' # Mondays 14:00 UTC; pick any cadence
221-
workflow_dispatch:
233+
{schedule_block} workflow_dispatch:
222234
223235
jobs:
224236
recommend:
@@ -246,7 +258,7 @@ def _render_local_workflow(interest_id: str) -> str:
246258

247259
def handle_outrider_setup_local(
248260
repo, interest_id, auto_interest, mode,
249-
anthropic_key, skip_confirm, dry_run,
261+
anthropic_key, skip_confirm, dry_run, no_cron=False,
250262
):
251263
"""Self-provision Outrider with the user's own gh token (no Remyx App)."""
252264
import os
@@ -293,7 +305,7 @@ def handle_outrider_setup_local(
293305

294306
if dry_run:
295307
click.echo("--- rendered workflow ---")
296-
click.echo(_render_local_workflow("<interest-id>"))
308+
click.echo(_render_local_workflow("<interest-id>", no_cron=no_cron))
297309
click.secho("dry-run: no changes made.", fg="yellow")
298310
return
299311

@@ -333,7 +345,7 @@ def handle_outrider_setup_local(
333345
branch_created = True
334346
click.echo(f"✓ Created branch {branch_name}")
335347

336-
workflow = _render_local_workflow(resolved_interest)
348+
workflow = _render_local_workflow(resolved_interest, no_cron=no_cron)
337349
_gh_put_file(resolved_repo, branch_name, WORKFLOW_PATH, workflow,
338350
"Install Outrider (self-provisioned via remyxai CLI)")
339351
click.echo(f"✓ Wrote {WORKFLOW_PATH}")

0 commit comments

Comments
 (0)