Skip to content

Commit 0d5a8a8

Browse files
smellslikemlclaude
andauthored
Add remyxai outrider trigger command (#42)
* Add `remyxai outrider trigger` command (REMYX-148) A new subcommand that dispatches a one-shot Outrider run on a repo via workflow_dispatch — useful for kicking off ad-hoc method-targeted PRs without waiting for the next scheduled run. remyxai outrider trigger --repo owner/name \ --pin-method "knowledge distillation" Mirrors `outrider setup-local`: authenticates via the user's `gh` CLI, no Remyx engine round-trip and no Remyx App requirement. Pre-flight refuses to dispatch when the target repo doesn't have an Outrider workflow registered. The check probes the Actions API (not the Contents API) — the workflow file existing on disk isn't the same as GitHub having indexed it for dispatch. Surfaces a clear hint pointing at `remyxai outrider init` / `setup-local`. Pairs with the new `pin-method` input on the Outrider action. `--pin-method` and `--pin-arxiv` are mutually exclusive at both the CLI layer (click usage error) and the handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Use `gh workflow run` to dispatch (nests inputs.* correctly) The raw POST to /repos/{repo}/actions/workflows/{wf}/dispatches accepts only `ref` and `inputs` at the top level; any extra top-level key is rejected with HTTP 422 "X is not a permitted key". Passing `-f pin-method=X` to `gh api` puts the key at the top level, not under `inputs.*`, so even a workflow that correctly declares the input rejects the call. `gh workflow run` wraps the supplied `-f key=val` args under `inputs` automatically — the right tool for this job. Caught during the first end-to-end dogfood on smellslikeml/ag2: the workflow declared the inputs and the action accepted them, but GitHub rejected the dispatch before the action ever saw the request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Document the discover → trigger workflow in docs/ Adds docs/method-targeted-runs.md covering: how to find arxiv ids via `papers list` / `search query`, how to dispatch with `outrider trigger`, both `--pin-method` shapes (arxiv id and free-text), the pre-flight behavior, and how `--pin-method` differs from the legacy `--pin-arxiv`. README gets a one-sentence pointer to the doc + command-reference table additions for the new `outrider trigger` / `outrider setup-local` / `search query` commands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9a8113a commit 0d5a8a8

5 files changed

Lines changed: 544 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ Set with `--mode` (default `auto`):
112112
- `auto` — provision, merge the setup PR, and start the first run
113113
- `review` — provision and open the setup PR for you to review and merge
114114

115+
### Trigger a one-shot run
116+
117+
Once Outrider is installed, `remyxai outrider trigger` dispatches an ad-hoc run targeting a specific paper or method — see [docs/method-targeted-runs.md](docs/method-targeted-runs.md) for the discover → trigger workflow.
118+
115119
### Credentials
116120

117121
**You set up two things, once:**
@@ -149,6 +153,9 @@ Run any command with `--help` for full flag listings and examples.
149153
| `remyxai interests toggle <id>` | Flip active/inactive |
150154
| `remyxai interests delete <id>` | Remove an interest |
151155
| `remyxai outrider init` | Install Outrider on a GitHub repo via the Remyx App |
156+
| `remyxai outrider setup-local` | Install Outrider via your own `gh` (no Remyx App) |
157+
| `remyxai outrider trigger` | Dispatch a one-shot Outrider run; supports `--pin-method` / `--pin-arxiv` |
158+
| `remyxai search query <text>` | Search the engine's research-asset catalog |
152159
| `remyxai search list` | List recently added research assets (papers + Docker images) |
153160
| `remyxai search info <arxiv-id>` | Asset details |
154161

docs/method-targeted-runs.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
type: howto
3+
description: Discover a relevant paper or method, then dispatch a targeted Outrider run that implements it.
4+
tags: [outrider, cli, pin-method, pin-arxiv, workflow_dispatch]
5+
---
6+
7+
# Method-targeted Outrider runs
8+
9+
Once [Outrider](https://github.com/remyxai/outrider) is installed on a repo, you can dispatch a one-shot run targeting a specific paper or method — useful for kicking off an ad-hoc PR/Issue without waiting for the next scheduled run.
10+
11+
The flow is two steps: **discover** an arxiv id you want implemented, then **trigger** the action with that id pinned.
12+
13+
14+
## 1. Discover
15+
16+
### From the engine's recommendations for an interest
17+
18+
```bash
19+
remyxai papers list --interest <uuid> --period week -n 10
20+
```
21+
22+
Lists the engine's top recommendations for a Research Interest over the lookback window. Each entry shows the title, arxiv URL, and a relevance score. Note the arxiv id (e.g. `2410.20305v2`) of the candidate you want to implement.
23+
24+
`remyxai papers digest` groups recommendations by interest if you want a cross-cutting view.
25+
26+
### From a free-text search of the catalog
27+
28+
```bash
29+
remyxai search query "knowledge distillation" -n 5
30+
```
31+
32+
Searches the engine's research-asset catalog for matches against a method or topic. Useful when you have a method in mind but don't yet know the canonical paper.
33+
34+
`remyxai search info <arxiv-id>` returns the full asset details (license, code repo, abstract) if you want to vet a candidate before triggering.
35+
36+
37+
## 2. Trigger
38+
39+
```bash
40+
# Pin to a specific paper by arxiv id
41+
remyxai outrider trigger --repo owner/name --pin-method 2410.20305v2
42+
43+
# Or describe the method — the action resolves it to the top arxiv hit
44+
remyxai outrider trigger --repo owner/name --pin-method "knowledge distillation"
45+
```
46+
47+
`--pin-method` accepts either a literal arxiv id (`NNNN.NNNNN[vN]`) or a free-text method query.
48+
49+
- When the input matches the arxiv-id shape, the action uses **direct asset lookup** — the paper doesn't need to already be in the repo's candidate pool.
50+
- When the input is free text, the action runs the engine's search and pins to the top hit.
51+
52+
In both cases the LLM selection pass is bypassed and the resolved paper goes straight to the implementation phase (Phase A audit → Phase B convention pass → Phase C test gate).
53+
54+
### Pre-flight
55+
56+
The command refuses to dispatch on repos that haven't been initialized with Outrider. If Outrider isn't installed yet, you'll see:
57+
58+
```
59+
Outrider is not installed on owner/name. Install it first:
60+
remyxai outrider init --repo owner/name
61+
```
62+
63+
### --pin-arxiv (legacy)
64+
65+
`--pin-arxiv` is the older form: it pins to an arxiv id but requires the paper to already be present in the repo's candidate pool. `--pin-method` is a superset (it works on arxiv ids outside the pool, via direct asset lookup), so prefer it for new uses.
66+
67+
The two flags are mutually exclusive — setting both is a usage error.
68+
69+
### Other options
70+
71+
| Flag | Default | What it does |
72+
|---|---|---|
73+
| `--repo owner/name` | cwd's git remote | Target repo |
74+
| `--interest <uuid>` | the workflow's configured interest | Override the Research Interest for this run |
75+
| `--ref <branch>` | the repo's default branch | The git ref to dispatch against |

remyxai/cli/commands.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
handle_interests_delete,
2525
handle_interests_toggle,
2626
)
27-
from remyxai.cli.outrider_actions import handle_outrider_init
27+
from remyxai.cli.outrider_actions import (
28+
handle_outrider_init,
29+
handle_outrider_trigger,
30+
)
2831
from remyxai.cli.outrider_local import handle_outrider_setup_local
2932

3033

@@ -690,5 +693,60 @@ def outrider_setup_local(
690693
)
691694

692695

696+
@outrider.command("trigger")
697+
@click.option("--repo", "repo", default=None,
698+
help=(
699+
"Target repo as owner/name or a GitHub URL. Defaults to "
700+
"detecting from the current directory's git remote."
701+
))
702+
@click.option("--pin-method", "pin_method", default=None,
703+
help=(
704+
"Method/technique query (e.g. \"knowledge distillation\") "
705+
"or a literal arxiv_id. The action's selection pass is "
706+
"bypassed and the resolved paper is implemented directly. "
707+
"Mutually exclusive with --pin-arxiv."
708+
))
709+
@click.option("--pin-arxiv", "pin_arxiv", default=None,
710+
help=(
711+
"arxiv_id of a paper in the repo's candidate pool to "
712+
"implement directly. Use --pin-method for arxiv_ids not "
713+
"in the pool (it does a direct asset lookup)."
714+
))
715+
@click.option("--interest", "-i", "interest_id", default=None,
716+
help="Override the ResearchInterest UUID for this run.")
717+
@click.option("--ref", "ref", default=None,
718+
help="Git ref to dispatch on. Defaults to the repo's default "
719+
"branch.")
720+
def outrider_trigger(repo, pin_method, pin_arxiv, interest_id, ref):
721+
"""
722+
Dispatch a one-shot Outrider run on a repo via workflow_dispatch.
723+
724+
Useful for kicking off ad-hoc method-targeted PRs without waiting for
725+
the next scheduled run. The repo must already have an Outrider workflow
726+
installed (see `remyxai outrider init` / `setup-local`). Authenticates
727+
via your local `gh` CLI — no Remyx engine round-trip.
728+
729+
Examples:
730+
731+
# Implement a specific method on a target repo
732+
remyxai outrider trigger --repo owner/name \\
733+
--pin-method "knowledge distillation"
734+
735+
# Re-run a previously surfaced paper by arxiv_id (works even if it's
736+
# no longer in the candidate pool)
737+
remyxai outrider trigger --repo owner/name --pin-method 2410.20305v2
738+
739+
# Plain trigger — let the normal selection pass run
740+
remyxai outrider trigger --repo owner/name
741+
"""
742+
handle_outrider_trigger(
743+
repo=repo,
744+
pin_method=pin_method,
745+
pin_arxiv=pin_arxiv,
746+
interest_id=interest_id,
747+
ref=ref,
748+
)
749+
750+
693751
if __name__ == "__main__":
694752
cli()

remyxai/cli/outrider_actions.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,155 @@ def handle_outrider_init(
389389
" ⚠ No model provider key set — connect Claude Code so the first "
390390
"run can complete.", fg="yellow",
391391
)
392+
393+
394+
# ─── outrider trigger ─────────────────────────────────────────────────────
395+
396+
WORKFLOW_FILENAME = "outrider.yml"
397+
398+
399+
def _gh_default_branch(repo: str) -> Optional[str]:
400+
"""Return the repo's default branch via `gh api`, or None on failure."""
401+
try:
402+
out = subprocess.check_output(
403+
["gh", "api", f"/repos/{repo}", "--jq", ".default_branch"],
404+
text=True, stderr=subprocess.DEVNULL,
405+
).strip()
406+
except (subprocess.CalledProcessError, FileNotFoundError):
407+
return None
408+
return out or None
409+
410+
411+
def _outrider_workflow_exists(repo: str) -> bool:
412+
"""Return True iff the repo has an Outrider workflow registered.
413+
414+
Probes the Actions API rather than the Contents API: the Contents
415+
endpoint reports the file as soon as it's committed, but the Actions
416+
endpoint only knows about a workflow after GitHub has indexed it for
417+
dispatch — which is the precise capability we need for the trigger
418+
call to succeed.
419+
"""
420+
r = subprocess.run(
421+
["gh", "api", f"/repos/{repo}/actions/workflows/{WORKFLOW_FILENAME}",
422+
"--silent"],
423+
capture_output=True, text=True,
424+
)
425+
return r.returncode == 0
426+
427+
428+
def _gh_dispatch_outrider(repo, branch, inputs):
429+
"""Dispatch the Outrider workflow with the supplied inputs.
430+
431+
Uses ``gh workflow run`` (not raw ``gh api``) because workflow_dispatch
432+
expects inputs nested under ``inputs.*`` in the request body. The raw
433+
POST endpoint accepts ``ref`` and ``inputs`` at the top level and
434+
rejects any extra top-level keys with ``HTTP 422 "X is not a permitted
435+
key"``; ``gh workflow run`` handles that wrapping for us.
436+
437+
Returns (ok, stderr) so the caller can map errors to user-facing hints.
438+
"""
439+
args = [
440+
"gh", "workflow", "run", WORKFLOW_FILENAME,
441+
"--repo", repo, "--ref", branch,
442+
]
443+
for k, v in inputs.items():
444+
if v is None or v == "":
445+
continue
446+
args.extend(["-f", f"{k}={v}"])
447+
r = subprocess.run(args, capture_output=True, text=True)
448+
return (r.returncode == 0, (r.stderr or "").strip())
449+
450+
451+
def _gh_latest_run_url(repo, sleep=time.sleep):
452+
"""Best-effort lookup of the most recent outrider.yml run URL."""
453+
for _ in range(5):
454+
try:
455+
url = subprocess.check_output(
456+
["gh", "run", "list", "--repo", repo,
457+
"--workflow", WORKFLOW_FILENAME, "--limit", "1",
458+
"--json", "url", "--jq", ".[0].url"],
459+
text=True, stderr=subprocess.DEVNULL,
460+
).strip()
461+
if url:
462+
return url
463+
except (subprocess.CalledProcessError, FileNotFoundError):
464+
return None
465+
sleep(2)
466+
return None
467+
468+
469+
def handle_outrider_trigger(
470+
repo, pin_method, pin_arxiv, interest_id, ref,
471+
):
472+
"""Dispatch a one-shot Outrider run on a repo via workflow_dispatch.
473+
474+
Called from `commands.outrider_trigger`. Mirrors `outrider setup-local`
475+
by using the user's authenticated `gh` to POST the dispatch — no Remyx
476+
engine round-trip, no Remyx App requirement. The repo must already
477+
have an Outrider workflow installed (set up via `remyxai outrider init`
478+
or `setup-local`).
479+
"""
480+
# Inputs validation
481+
if pin_method and pin_arxiv:
482+
raise click.UsageError(
483+
"--pin-method and --pin-arxiv are mutually exclusive."
484+
)
485+
486+
# Repo resolution
487+
resolved_repo = _normalize_repo(repo) if repo else _detect_github_repo_from_cwd()
488+
if not resolved_repo:
489+
raise click.UsageError(
490+
"Could not determine target repo. Pass --repo owner/name or run "
491+
"from inside a GitHub-origin git checkout."
492+
)
493+
494+
# Pre-flight: refuse to dispatch on repos that aren't initialized.
495+
# Surfaces a clear install hint instead of a generic 404 from the
496+
# workflow_dispatch call below — the error is structurally about the
497+
# repo's setup state, not the dispatch attempt itself.
498+
if not _outrider_workflow_exists(resolved_repo):
499+
raise click.ClickException(
500+
f"Outrider is not installed on {resolved_repo}. Install it "
501+
f"first:\n"
502+
f" remyxai outrider init --repo {resolved_repo}\n"
503+
f"or, if your org can't grant the Remyx GitHub App yet:\n"
504+
f" remyxai outrider setup-local --repo {resolved_repo}"
505+
)
506+
507+
# Ref defaults to the repo's default branch (so trigger works without
508+
# the user knowing whether the repo's default is main, master, develop).
509+
branch = ref or _gh_default_branch(resolved_repo)
510+
if not branch:
511+
raise click.ClickException(
512+
f"Could not resolve a ref for {resolved_repo}. Pass --ref "
513+
f"explicitly (e.g. --ref main)."
514+
)
515+
516+
inputs = {
517+
"pin-method": pin_method or "",
518+
"pin-arxiv": pin_arxiv or "",
519+
"interest-id": interest_id or "",
520+
}
521+
522+
click.echo(f"Dispatching Outrider on {resolved_repo} (ref={branch})…")
523+
if pin_method:
524+
click.echo(f" pin-method: {pin_method!r}")
525+
if pin_arxiv:
526+
click.echo(f" pin-arxiv: {pin_arxiv!r}")
527+
if interest_id:
528+
click.echo(f" interest: {interest_id}")
529+
530+
ok, stderr = _gh_dispatch_outrider(resolved_repo, branch, inputs)
531+
if not ok:
532+
hint = ""
533+
if "403" in stderr or "permission" in stderr.lower():
534+
hint = ("\n Your gh token lacks the scope to dispatch workflows "
535+
"on this repo. Re-auth: `gh auth login --scopes workflow`.")
536+
raise click.ClickException(f"workflow dispatch failed: {stderr}{hint}")
537+
538+
click.secho("✓ Dispatched.", fg="green", bold=True)
539+
url = _gh_latest_run_url(resolved_repo)
540+
if url:
541+
click.echo(f" Run: {url}")
542+
else:
543+
click.echo(f" Run: https://github.com/{resolved_repo}/actions")

0 commit comments

Comments
 (0)