roadmap-orchestrator
One bounded roadmap-advance tick — dispatch ready packages, dual-gate PRs (CI + serialized local hardware test), auto-merge, write the daily standup. Cron-driven via Windows Task Scheduler (opencode r
SKILL.md.claude/skills/roadmap-orchestrator/SKILL.md
GitHub/roadmap-orchestrator [--dry-run]
Implements .astroray_plan/docs/2026-05-16-roadmap-orchestrator-design.md. One
invocation = one bounded tick. Recurrence comes from a /schedule cron routine,
never from looping in-session.
Step 0 — Guards
cdto the canonical repo path;git rev-parse --show-toplevelto confirm.git fetch origin; confirm onmain, up to date.- Stale-
.pydscan (reusepkg-shipStep 0 PowerShell scan). Abort tick on shadow.pyd. - Acquire the tick lock:
acquire_lock(".astroray_plan/.orchestrator.lock", 1500)returnsTrueif the lock was acquired (proceed) orFalseif a live (non-stale) lock is already held by a running tick. If it returnsFalse, exit now — do not run an overlapping tick. On--dry-run, skip lock acquisition entirely.
Step 1 — Compute the tick plan (decision engine)
Determine eligible_packages: from NEXT_STAGE_REPORT.md §2, the ready packages
(not research-only, deps merged, no open PR, no active worktree, dispatchable Track) —
this is exactly dispatch-next's eligibility logic; reuse it. Also exclude any
package already recorded in ledger.get("impl_dispatches", {}) with a non-terminal
outcome (e.g. "dispatched") — those are already in flight; do NOT re-dispatch them.
Count in_flight (active worktrees + running implementer agents).
Run:
python -m roadmap_orchestrator.cli \
--ledger .astroray_plan/.orchestrator-state.json \
--next-stage-report .astroray_plan/docs/NEXT_STAGE_REPORT.md \
--eligible <comma-joined eligible_packages> --in-flight <n> \
[--dry-run] (pass --gpu-lock-free only if the GPU lock is free)
GPU-lock-free is decided via lock_status(.astroray_plan/.orchestrator.gpu.lock, 5400).
Parse the JSON: plan.dispatch, plan.fixers, plan.hw_dispatch, plan.merges,
plan.hw_failed, standup_md.
If --dry-run: print the plan + standup_md, do nothing else, exit. (No lock,
no spawn, no merge, no build, no file write.)
Note: cli.py always emits the plan JSON and performs no side effects itself; the --dry-run flag is only echoed back as dry_run: true in the JSON and is the SKILL's own signal to stop after printing. All side effects (Step 2/3) are performed by this SKILL, never by the CLI.
Step 2 — Execute side effects (live only)
In this order, respecting caps already applied by the engine:
Dispatch each pkg in
plan.dispatchviadispatch-nextrouting (package-implementerin its own fresh worktree; Codex is supported again (2026-09-05) as a CLI second-opinion reviewer (Terra/Luna viacodex exec), never as an implementer or orchestrator, so legacy Track-E / Codex-paste-ready specs remain inert and route here too) with the NEXT_STAGE_REPORT §3 drop-in prompt verbatim. Record the dispatch BEFORE spawning:ledger.setdefault("impl_dispatches", {})[<pkg>] = {"outcome": "dispatched", "ts": <now>}thensave_ledger(...)— this is what stops the next tick from re-dispatching the same package. If an isolated worktree cannot be created, abort that dispatch (and set the entry outcome to"escalated:worktree"so it surfaces in the standup) — never fall back tomain(memoryparallel_agent_worktree_contamination). After spawning, re-checkgit rev-parse main== Step-0 value; if it moved, halt dispatch and write aCONTAMINATIONAction item.Fixers — for each
plan.fixers(each is{"pr": n, "kind": "rebase"|"ci"}):kind=="rebase"→ rebase-fixer on that PR's branch worktree (rebaseorigin/main, push);kind=="ci"→gate-failure-reviewer(which now produces a root-cause analysis + routes a freshpackage-implementerto draft a fix + requires a different-model sign-off on the proposed fix before push — seegate-failure-reviewer.md§ Deliverables). The fixer is not pushed for re-gate without a recordedindep_review:SIGN-OFF; aBLOCKverdict halts the push and writes an Action item to the standup. Thenrecord_action(ledger, <pr number>, "rebase_dispatched"|"ci_dispatched"|"indep_review:SIGN-OFF"|"indep_review:BLOCK")(signature:state.record_action(ledger, number, action)).Hardware gate (strictly serialized, asynchronous across ticks — design spec §2a).
plan.hw_dispatchis eithernullor a single PR number (int), not a dict. a. Read back a finished result first. Checklock_status(".astroray_plan/.orchestrator.gpu.lock", 5400). If the GPU lock is held and its dispatchedhardware-verifierjob for that PR has finished, callrecord_hw_result(ledger, <pr number>, <head_sha>, "PASS"|"FAIL", <numbers>, <artifact path>)(signature:state.record_hw_result(ledger, number, head_sha, result, numbers, artifact)), thenrelease_lock(".astroray_plan/.orchestrator.gpu.lock"). b. Dispatch a new job only if the slot is free. Ifplan.hw_dispatchis notnullAND the GPU lock is free: look up that PR'sheadRefOidandheadRefNamefromgh pr view <plan.hw_dispatch> --json headRefOid,headRefName; resolve the PR's worktree path (.claude/worktrees/<pkg>from the branch name, verified viagit worktree list);acquire_lock(".astroray_plan/.orchestrator.gpu.lock", 5400, meta={"sha": <headRefOid>}); if acquired, dispatch the local hardware build+test via theverifyskill /hardware-verifieron that PR's branch worktree, passing the worktree path +headRefOid+ PR number + spec path + newest binding as a background job — do NOT block the rest of this tick waiting for it (a CUDA build+render runs far longer than the 10-min tick cadence; its result is read back by a later tick via step (a)). The HW build runs the PR's own worktree at its head SHA via the vcvars-bootstrapping wrapper; nevermain; Step-0 stale-.pydhygiene and single-CUDA-job serialization are unchanged. Exactly one GPU/CUDA job ever — never start a second while this lock is held (memorycuda_verifier_concurrency). Never auto-run the full closeout sweep.Merges — for each
plan.merges: if the PR's package is non-HW-gated (Track A addon/orchestrator/engine plumbing / docs-with-code — no HW/render acceptance gate), dispatch one independent different-model pre-merge code review of the PR diff against the package spec's acceptance criteria before invokingpr-reviewer. Verdict isSIGN-OFForBLOCK:SIGN-OFF→ proceed topr-reviewer(which still runs its full checklist; the independent review is additive).BLOCK→ do not invokepr-reviewer; write a standup Action item (PR number, the integration/logic gap named by the reviewer) and route agate-failure-reviewer/ fresh-implementer pass. The PR is not merged this tick.
If the PR's package is HW/render-gated (has a HW/render acceptance gate in its spec — the empirical RTX visual gate is its real backstop), skip the independent review and proceed directly to
pr-reviewerexactly as today. Thepr-revieweragent's checklist self-escalates and STOPS on gate/license problems. It only auto-merges a PR that ismergeable+ CI all-pass; the engine already confirmed hardwarePASSbound to the current head SHA.Pure-docs PRs (diff touches only
*.mdand.astroray_plan/): the existingpr-reviewerdoc-only fast path is preserved; no independent review is dispatched (keeps the gate minimal).HW-failed / Action items — for each
plan.hw_failed: dispatchgate-failure-revieweron the local failure artifacts (which now produces a root-cause analysis + routes a freshpackage-implementerto draft a fix + requires a different-model sign-off on the proposed fix before push — see Step 2.2 andgate-failure-reviewer.md§ Deliverables). A HWFAILis never overridden automatically; the fix is not pushed for re-gate without a recordedindep_review:SIGN-OFF. Thenrecord_action(ledger, <pr number>, "gate_review_dispatched")to debounce re-dispatch (memoryorchestrator-hw-failed-no-debounce).
Step 3 — Standup + close out
finalize_previous(.astroray_plan/docs/standup, <today>).upsert_standup(.astroray_plan/docs/standup, <today>, plan, gpu_holder, hw_queue, ledger)(pass ledger to surface independent-review verdicts in standup).expire_closed(ledger, open_numbers)whereopen_numbersis a Pythonsetof int PR numbers from thegh pr list; thensave_ledger(".astroray_plan/.orchestrator-state.json", ledger).release_lock(.astroray_plan/.orchestrator.lock)(also release on every abort path).
Step 3 — Standup + close out
- Safe merged-worktree auto-GC (live path only; never under
--dry-run). Callgc_merged_worktrees(".claude/worktrees", ledger)(signature:state.gc_merged_worktrees(worktrees_dir, ledger) -> dict). Returns{"removed": [...], "escalations": [...]}. This safely removes worktrees + branches for PRs that are (a) MERGED on GitHub, (b) content inorigin/main(squash-aware), (c) zero uncommitted/unpushed changes. Anything not provably safe is escalated, never force-deleted. Design: pkg97 § Phase 1. finalize_previous(.astroray_plan/docs/standup, <today>).upsert_standup(.astroray_plan/docs/standup, <today>, plan, gpu_holder, hw_queue, merged_today, gc_report, ledger)wheremerged_todayis the list of PR numbers merged today (from_get_merged_today()) andgc_reportis the GC report from step 1.expire_closed(ledger, open_numbers)whereopen_numbersis a Pythonsetof int PR numbers from thegh pr list; thensave_ledger(".astroray_plan/.orchestrator-state.json", ledger).release_lock(.astroray_plan/.orchestrator.lock)(also release on every abort path).
Model routing (authority lives in agent frontmatter, not this body)
Per-agent model assignments live in the agent frontmatter — .opencode/agents/*.md
(opencode, primary) and .claude/agents/*.md (Claude Code, fallback) — plus the
delegate tiers in .claude/skills/delegate/config/tiers.json. Never hardcode a
model id here. The tick's own session runs on a cheap open-weight model; every
judgment-heavy step below carries its own agent + model via its frontmatter.
Independent sign-off (Steps 2.2, 2.4, 2.5): the independent review of implementer
output MUST be produced by a different lineage than the drafter. Dispatch the sign-off
agent (which runs claude -p — Claude Code, a different provider than the open-weight
drafter) with an adversarial prompt. The verdict is SIGN-OFF or BLOCK. Failing to
dispatch the sign-off agent silently defeats the rule. BLOCK halts the push (see
gate-failure-reviewer.md).
Outside-eye pre-pass (cheap, additive, non-authoritative): before the sign-off at each
of the three sites, run
python .claude/skills/delegate/scripts/delegate.py --tier verify --agent critic --prompt "..." on the same diff/spec. Its findings are LEADS, never a verdict — fold
them into the sign-off prompt (e.g. "a cheap first pass flagged: <findings>; weigh and
verify before your SIGN-OFF/BLOCK"). The critic pass never itself produces SIGN-OFF/BLOCK
and never blocks a merge on its own; if opencode/the model is unavailable, skip it
silently and proceed straight to the sign-off review — it's a bonus signal, not a
dependency.
Safety rails (non-negotiable — see design spec §5)
- One tick at a time (tick lock); one CUDA job at a time (GPU lock).
- Per-package isolated worktree or abort — never
main.mainmutated only bypr-reviewer. - The HW build runs the PR's own worktree at its head SHA via the vcvars-bootstrapping wrapper; never
main; Step-0 stale-.pydhygiene and single-CUDA-job serialization are unchanged. - Implementer cap 2 / fixer cap 1 (enforced in the engine).
- Auto-merge requires BOTH CI all-pass AND head-SHA-bound hardware
PASS. A HWFAILblocks merge + escalates. - A gate-failure fix (CI or HW) is never pushed for re-gate without a recorded different-model SIGN-OFF; BLOCK halts the push. (pkg98)
- Non-HW-gated packages get one independent different-model pre-merge code review before
pr-reviewer; HW-gated packages skip it (the empirical HW visual gate already covers them). (pkg98) --dry-run= zero side effects.- Worktree/branch GC is merged-only, never force-delete on doubt. Only PRs with
state == "MERGED"AND content inorigin/main(squash-aware) AND clean worktree are removed. No staleness/age heuristic. Escalate all ambiguous cases as Action items (pkg97 § Phase 1 hard invariant).
Recurrence (Windows Task Scheduler, not /schedule)
opencode has no built-in cron. Recurrence is a Windows Task Scheduler task
(Astroray-RoadmapOrchestrator) that runs scripts/orchestrator_tick.ps1 -Driver opencode
every 10 minutes, which pins the environment + PATH and invokes
opencode run -m opencode-go/deepseek-v4-pro "Run the roadmap-orchestrator skill — one bounded tick.". The tick lock makes exact firing time immaterial.
Pause/stop: schtasks /change /tn Astroray-RoadmapOrchestrator /disable (or
/enable). Disable it during team-overnight runs to avoid a second driver.
The standup is updated every tick and finalized on day rollover, so no separate daily cron is needed.