Skip to content

Commit bed1740

Browse files
salma-remyxclaude
andcommitted
fix(outrider two-tier): make drafter/refiner install actually run on target repos
Addresses review feedback on #47: * Drafter `uses: ./` rewrite (blocking). The @v1 drafter references the action locally; on a customer repo that resolves to their root (no action.yml). _render_drafter_workflow now rewrites `uses: ./` → `uses: remyxai/outrider@v1`, guarded like the interest-id rewrite. * Runner declares the refiner's dispatch inputs (blocking). The refiner dispatches outrider.yml with mode/publish/start-from-ref/lead-content/ staged-synthesis, which GitHub rejected as unknown workflow_dispatch keys. _render_local_workflow now declares + forwards all five; defaults match the action's own so scheduled/manual runs are unchanged. * Document the fork schedule caveat (note). Two-tier PR body + plan output now note that `schedule:` triggers don't self-run on forks; wires up the previously-unused PR_TITLE_TWO_TIER. Also fixes _fetch_outrider_template passing a stray leading "api" arg to _gh_api_json (which already prepends `gh api`), which produced `gh api api repos/...` and broke every two-tier fetch. Refiner over-selection (note 4) is already resolved upstream in remyxai/outrider (drafter-known-branch enumeration) and propagates via the live @v1 fetch — no CLI change needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 243ce3d commit bed1740

2 files changed

Lines changed: 140 additions & 4 deletions

File tree

remyxai/cli/outrider_local.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,31 @@ def _render_local_workflow(
307307
description: 'Optional exact arxiv_id (e.g. 2402.02347v3). Bypasses selection and implements this specific paper. Use for reproducible re-runs.'
308308
required: false
309309
default: ''
310+
# The five inputs below let outrider-weekly-refine.yml (the refiner)
311+
# dispatch this runner: it pins the picked draft branch (start-from-ref),
312+
# pipes a gap analysis in (lead-content), turns on staged synthesis, and
313+
# selects mode/publish. Defaults match the action's own, so scheduled and
314+
# manual runs are unchanged.
315+
mode:
316+
description: 'Run mode. recommend (default) runs the full scout→implement flow; the refiner dispatches recommend against a pinned paper + start-from-ref.'
317+
required: false
318+
default: 'recommend'
319+
publish:
320+
description: 'pr (default) opens a PR/Issue; branch produces a fork branch without opening one (drafter behavior).'
321+
required: false
322+
default: 'pr'
323+
start-from-ref:
324+
description: 'Optional base branch to build on top of (the refiner passes the picked drafter branch here). Empty = start from the default branch.'
325+
required: false
326+
default: ''
327+
lead-content:
328+
description: 'Optional inline markdown (e.g. a gap analysis) fed to the agent as leading context. Used by the refiner dispatch.'
329+
required: false
330+
default: ''
331+
staged-synthesis:
332+
description: 'Enable the multi-pass staged-synthesis flow (the refiner sets true). Empty/false = single-pass.'
333+
required: false
334+
default: 'false'
310335
claude-timeout:
311336
description: 'Wall-clock seconds for the Claude Code agent calls (preflight + implementation). Raise for very large monorepos; lower to cap cost.'
312337
required: false
@@ -362,6 +387,14 @@ def _render_local_workflow(
362387
search-method: ${{{{ inputs.search-method }}}}
363388
pin-arxiv: ${{{{ inputs.pin-arxiv }}}}
364389
claude-timeout: ${{{{ inputs.claude-timeout }}}}
390+
# Forwarded so outrider-weekly-refine.yml can dispatch a refinement
391+
# run (mode + start-from-ref + lead-content + staged-synthesis) and
392+
# so the drafter/manual runs can pick pr vs branch publishing.
393+
mode: ${{{{ inputs.mode }}}}
394+
publish: ${{{{ inputs.publish }}}}
395+
start-from-ref: ${{{{ inputs.start-from-ref }}}}
396+
lead-content: ${{{{ inputs.lead-content }}}}
397+
staged-synthesis: ${{{{ inputs.staged-synthesis }}}}
365398
# Maps provider name → base URL. Adding more providers here
366399
# extends the table (Bedrock / Vertex / on-prem); leave
367400
# empty on the default Anthropic path.
@@ -394,6 +427,13 @@ def _render_local_workflow(
394427
# We rewrite that specific string to the customer's interest-id at render time.
395428
_OUTRIDER_SELF_INTEREST_ID = "29ca03e7-454d-446c-9941-32c96c53d95d"
396429

430+
# The outrider repo's templates reference the action locally (``uses: ./``),
431+
# which only resolves from inside the outrider repo itself. On a customer
432+
# install that path points at the target repo's root (no action.yml there),
433+
# so we rewrite it to the published action ref at render time.
434+
_LOCAL_ACTION_USES = "uses: ./"
435+
_PUBLISHED_ACTION_USES = f"uses: {_OUTRIDER_TEMPLATE_REPO}@{_OUTRIDER_TEMPLATE_REF}"
436+
397437

398438
def _fetch_outrider_template(path: str) -> str:
399439
"""Fetch a workflow-template file from remyxai/outrider@v1 via `gh api`.
@@ -404,7 +444,7 @@ def _fetch_outrider_template(path: str) -> str:
404444
import base64
405445
try:
406446
payload = _gh_api_json([
407-
"api", f"repos/{_OUTRIDER_TEMPLATE_REPO}/contents/{path}?ref={_OUTRIDER_TEMPLATE_REF}",
447+
f"repos/{_OUTRIDER_TEMPLATE_REPO}/contents/{path}?ref={_OUTRIDER_TEMPLATE_REF}",
408448
])
409449
except Exception as e:
410450
raise click.ClickException(
@@ -421,7 +461,8 @@ def _fetch_outrider_template(path: str) -> str:
421461

422462
def _render_drafter_workflow(interest_id: str) -> str:
423463
"""Drafter template — fetched live from remyxai/outrider@v1 with the
424-
self-test interest-id rewritten to the customer's."""
464+
self-test interest-id rewritten to the customer's and the local action
465+
reference (``uses: ./``) rewritten to the published action ref."""
425466
raw = _fetch_outrider_template(_DRAFTER_TEMPLATE_PATH)
426467
if _OUTRIDER_SELF_INTEREST_ID not in raw:
427468
raise click.ClickException(
@@ -430,7 +471,16 @@ def _render_drafter_workflow(interest_id: str) -> str:
430471
f"({_OUTRIDER_SELF_INTEREST_ID}); template format may have changed. "
431472
f"CLI needs an update."
432473
)
433-
return raw.replace(_OUTRIDER_SELF_INTEREST_ID, interest_id)
474+
if _LOCAL_ACTION_USES not in raw:
475+
raise click.ClickException(
476+
f"drafter template on {_OUTRIDER_TEMPLATE_REPO}@{_OUTRIDER_TEMPLATE_REF} "
477+
f"no longer references the action via '{_LOCAL_ACTION_USES}'; template "
478+
f"format may have changed. CLI needs an update."
479+
)
480+
return (
481+
raw.replace(_OUTRIDER_SELF_INTEREST_ID, interest_id)
482+
.replace(_LOCAL_ACTION_USES, _PUBLISHED_ACTION_USES)
483+
)
434484

435485

436486
def _render_refiner_workflow() -> str:
@@ -506,6 +556,9 @@ def handle_outrider_setup_local(
506556
click.echo(f" {DRAFTER_WORKFLOW_PATH} (daily drafter, Haiku 4.5, publish=branch)")
507557
click.echo(f" {REFINER_WORKFLOW_PATH} (weekly refiner, picks + gap-gens + dispatches Opus)")
508558
click.echo(" (three files on a branch → one PR — templates fetched live from remyxai/outrider@v1)")
559+
click.secho(" - Note: forks don't run scheduled workflows — the daily/weekly "
560+
"crons need an external dispatcher on a fork (workflow_dispatch is unaffected).",
561+
fg="yellow")
509562
else:
510563
click.echo(f" - Writes: {WORKFLOW_PATH} on a branch + opens a PR")
511564
click.echo("")
@@ -597,8 +650,22 @@ def handle_outrider_setup_local(
597650
f"Research interest: `{resolved_interest}`\n\n"
598651
f"Generated by `remyxai outrider setup-local`."
599652
)
653+
if two_tier:
654+
body += (
655+
"\n\n**Two-tier setup** — installs a daily drafter "
656+
"(`outrider-daily.yml`) and a weekly refiner "
657+
"(`outrider-weekly-refine.yml`) alongside the manual-dispatch "
658+
"runner.\n\n"
659+
"> **Note — forks:** GitHub disables/deprioritizes `schedule:` "
660+
"triggers on forked repos, so the drafter's daily cron and the "
661+
"refiner's weekly cron will not self-run on a fork. "
662+
"`workflow_dispatch` (manual / `gh workflow run`) works either "
663+
"way; drive the cadence from an external dispatcher if this repo "
664+
"is a fork."
665+
)
600666
pr_url, pr_number = _gh_open_pr(
601-
resolved_repo, branch_name, default_branch, PR_TITLE, body,
667+
resolved_repo, branch_name, default_branch,
668+
PR_TITLE_TWO_TIER if two_tier else PR_TITLE, body,
602669
draft=(mode != "auto"),
603670
)
604671
click.echo(f"✓ Opened PR: {pr_url}")

tests/cli/test_outrider_local.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,75 @@ def test_render_forwards_workflow_dispatch_inputs_to_action():
117117
)
118118

119119

120+
def test_render_declares_and_forwards_refiner_dispatch_inputs():
121+
"""The runner must declare + forward every input the refiner
122+
(outrider-weekly-refine.yml) dispatches — mode, publish, start-from-ref,
123+
lead-content, staged-synthesis — or GitHub rejects the dispatch as an
124+
unknown workflow_dispatch key and no refinement PR opens."""
125+
wf = outrider_local._render_local_workflow("uuid")
126+
for name in ("mode", "publish", "start-from-ref", "lead-content", "staged-synthesis"):
127+
assert f" {name}:" in wf, f"missing input declaration: {name}"
128+
assert f"{name}: ${{{{ inputs.{name} }}}}" in wf, f"missing forwarding for {name}"
129+
# Defaults match the action's own so scheduled/manual runs are unchanged.
130+
assert "default: 'recommend'" in wf # mode
131+
assert "default: 'pr'" in wf # publish
132+
133+
134+
# ─── two-tier template rendering (fetched from remyxai/outrider@v1) ──────────
135+
136+
_FAKE_DRAFTER_TEMPLATE = (
137+
"name: Outrider daily\n"
138+
"on:\n schedule:\n - cron: '0 6 * * *'\n"
139+
"jobs:\n scout:\n steps:\n"
140+
" - uses: actions/checkout@v4\n"
141+
" - uses: ./\n"
142+
" with:\n"
143+
" interest-id: '29ca03e7-454d-446c-9941-32c96c53d95d'\n"
144+
" publish: branch\n"
145+
)
146+
147+
148+
def test_render_drafter_rewrites_local_action_and_interest_id():
149+
"""The drafter fetched from @v1 uses `uses: ./` (resolves only inside the
150+
outrider repo) and a self-test interest-id. Both must be rewritten so the
151+
installed drafter targets the published action + customer's interest."""
152+
with patch.object(outrider_local, "_fetch_outrider_template",
153+
return_value=_FAKE_DRAFTER_TEMPLATE):
154+
rendered = outrider_local._render_drafter_workflow("cust-uuid")
155+
assert "uses: ./" not in rendered
156+
assert "uses: remyxai/outrider@v1" in rendered
157+
assert "interest-id: 'cust-uuid'" in rendered
158+
assert outrider_local._OUTRIDER_SELF_INTEREST_ID not in rendered
159+
160+
161+
def test_render_drafter_guards_missing_local_action_ref():
162+
"""If the template stops referencing the action via `uses: ./`, fail loud
163+
rather than silently shipping an un-rewritten drafter."""
164+
broken = _FAKE_DRAFTER_TEMPLATE.replace("uses: ./", "uses: remyxai/outrider@v1")
165+
with patch.object(outrider_local, "_fetch_outrider_template", return_value=broken):
166+
with pytest.raises(click.ClickException, match="no longer references the action"):
167+
outrider_local._render_drafter_workflow("cust-uuid")
168+
169+
170+
def test_fetch_outrider_template_calls_gh_api_without_double_prefix(monkeypatch):
171+
"""Regression: _fetch_outrider_template must pass the contents path directly
172+
to _gh_api_json (which already prepends `gh api`), not a leading 'api' arg
173+
that would produce `gh api api repos/...`."""
174+
import base64
175+
captured = {}
176+
177+
def fake_gh_api_json(args):
178+
captured["args"] = args
179+
return {"content": base64.b64encode(b"hello").decode()}
180+
181+
monkeypatch.setattr(outrider_local, "_gh_api_json", fake_gh_api_json)
182+
out = outrider_local._fetch_outrider_template(".github/workflows/outrider-daily.yml")
183+
assert out == "hello"
184+
assert captured["args"][0] != "api"
185+
assert captured["args"][0].startswith("repos/remyxai/outrider/contents/")
186+
assert "ref=v1" in captured["args"][0]
187+
188+
120189
# ─── gh secret stdin invariant ───────────────────────────────────────────────
121190

122191
def test_gh_set_secret_value_via_stdin(monkeypatch):

0 commit comments

Comments
 (0)