Run Project — Codex Router
SkillDev toolsCodex-native router for executing HQ PRD stories. Default inline dispatches into pooled per-worker lanes claimed through conduct-pool.sh, typically 3-4 live lanes (preflight explorer, story worker, regression gate), never more than CONDUCT_POOL_CAP (default 8), regardless of how many stories the PR
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Run Project — Codex Router skill
What this skill tells your AI
The instructions your AI receives, as published by indigoai-us/hq-core in .claude/skills/run-project/SKILL.md and read by ahel’s review.
Codex does not use Claude Code's Task, Plan sub-agents, ExitPlanMode, /checkpoint, or /compact primitives. The default path needs none of them: every story is a detached lane. Where a host has no engine CLI and falls back in-session, Codex's spawn_agent / wait_agent map the per-story boundary to a worker agent and the read-only preflight to an explorer agent.
Live children: typical 3–4, worst case CONDUCT_POOL_CAP (default 8). Every
agent this skill dispatches is a pooled lane claimed through
core/scripts/conduct-pool.sh and dispatched as a detached process per
.claude/skills/_shared/lane-dispatch-protocol.md, so the count is set by the
pool, not by the number of stories — a 40-story PRD opens no more lanes than a
4-story one.
Story coordinators claim story:{worker-id}; the phases inside them claim the
bare worker id, so a story can never block on a lane it is holding itself.
--interactive runs in the parent and claims none.
User's input: $ARGUMENTS
Script Resolution
Resolve the shell orchestrator before any shell delegation:
- Prefer
core/scripts/run-project.shif it exists. - Otherwise use
.claude/scripts/run-project.sh. - If neither exists, stop with a clear error.
Store the chosen path as {run_project_script}. In this HQ workspace today, the expected path is .claude/scripts/run-project.sh.
Work Mesh Live — trusted bind (do this first)
Before any other tool call that touches project work, bind the session per
.claude/skills/_shared/work-mesh-live-bind.md (US-011):
bash core/scripts/work-mesh-live-bind-trusted.sh \
--company "{co}" --project "{project}" --task "{task}"
Omit --task when unknown. This writes workspace/sessions/<sid>/meta.yaml
and reconciles with observation.trustedContext (no --trusted CLI flag).
Step 1 — Parse Arguments
Extract from $ARGUMENTS:
{project}— project name, required unless--statusor--help--status— show orchestrator status, then stop--dry-run— show story order, then stop--resume— pass through to the chosen execution path--inline— the default: one detached lane per story, pooled (Step 3). The name is historical; it means story-delegated, as opposed to--interactive's parent-driven editing. It has not meant "in the parent session" since stories became lanes.--interactiveor--session-mode— parent-driven Codex execution--ralph-mode— the same pooled lane loop as default, run unattended: skip the preflight approval, auto-advance through every story without between-story pauses, and report once at the end--in-place— (ralph) skip feature-branch pre-creation; work on the current checkout--timeout N— (ralph) per-story wall-clock budget in minutes before a story is markedblocked: TIMEOUT. This is the waiter'sdeadlinefile (dispatch protocol §4-§5), not the runner'stimeoutSecs— that one only warns and never kills, so it cannot bound anything on its own.--resumecontinues from the next incomplete story (read fromstate.json)
If no execution mode is supplied, route to --inline. This is the default because it preserves the Ralph story loop and keeps implementation context out of the parent session — each story runs in its own detached lane, so the parent holds coordination state only.
Use --interactive only when the user asks to steer edits directly in the parent session; it is the one mode that does not dispatch lanes at all. Use --ralph-mode for long unattended runs where you do not want to be prompted between stories — it executes the identical worker-authoritative lane loop, just without the pauses.
Recommendation posture: always recommend --inline. It runs continuously — no pause between stories — except when either condition holds:
- The PRD has more than 10 incomplete stories → pause at the preflight plan for one approval, then run continuously.
- A story requires session mode (its notes or classification demand parent-driven interactive steering) → pause before that story only, run it interactively, then resume continuous flow.
Everything else auto-continues exactly like ralph-mode. Failed or blocked stories still stop the queue.
Note on the legacy detached orchestrator — not the same thing as a lane. Earlier versions of this skill launched a detached shell subprocess (
nohup bash run-project.sh … --engine claude) that ran one headlessclaude -pbuilder per story, outside the pool and outside the worker system. That path is retired:run-project.sh's execution loop is frozen and kept only for--status,--dry-run, and--help; it no longer runs stories, and it has noclaudebuilder. The--engine/--builder,--swarm,--tmux,--codex-autofix, and--no-monitorflags belonged to it and are not accepted.Today's lanes are a different mechanism and share none of that: they are pooled, capped by
CONDUCT_POOL_CAP, worker-authoritative, engine-chosen from the dispatch protocol's roster, and each one runs/execute-taskrather than a bare headless builder. "Detached" describes both, which is why this note exists — do not read the retirement above as a claim that stories run in the parent session.
Step 2 — Status, Help, and Dry Run
Use {run_project_script}:
bash {run_project_script} --status
bash {run_project_script} --help
bash {run_project_script} --dry-run {project}
Display the important output to the user and stop.
Step 2.5 — Design-Lock Check (soft gate)
Before execution, read the project's prd.json and check metadata.designLocked.
- If
designLocked: true(or the project has no UI surface) — proceed silently. - If the PRD is UI-bearing (stories reference screens, pages, components, or
metadata.designRef/audiencesimply a user-facing interface) anddesignLockedis absent or false — surface a one-line soft warning: "Heads up — design isn't locked for{project}. Consider/storyboard {project}first to lock visuals and fold any design changes into the PRD before building. Proceed anyway? [Y/n]"
This is advisory, not a hard stop — many projects (CLIs, infra, libraries) have no visual surface and should proceed without friction. Honor an explicit "yes/proceed" and continue.
Step 3 — Default Execution: Pooled, Detached Lanes
Default mode is story-delegated, pooled, and detached. The parent session
plans and coordinates; each story runs in a lane claimed from the session pool —
one live lane per HQ worker id, reused across stories rather than a fresh child
per story — that invokes /execute-task {project}/{story-id} internally.
A lane is a detached OS process, not an in-session sub-agent. Dispatch
follows .claude/skills/_shared/lane-dispatch-protocol.md, the same mechanism
/conduct uses: a brief on disk, a detached workflow-runner.mjs process, and a
background waiter. Three things follow from that, and they are the reason this is
the default:
- Work in flight survives the parent session. Compaction, a restart, or an ended session no longer kills the running story — the previous in-session loop died with its parent, mid-story, with the slot still marked running and the work lost. Be precise about the scope: the story survives, the coordinator does not. The waiter, the validation and the dispatch of the next story are all parent-side, so a run that loses its session finishes the in-flight story and then waits for a new session to resume it (Step 5). It is resumable, not self-driving.
- A story can be corrected while it runs (dispatch protocol §6) instead of being killed and relaunched.
- A host with no in-session sub-agent primitive can still run a project.
Inside the lane, /execute-task maps its worker phases to that engine's own
sub-agents (spawn_agent under Codex, Task under Claude) per
.claude/skills/execute-task/SKILL.md. Those phases are children of the lane, not
of this session, and they claim their bare-worker-id slots from the same pool
— which is why the lane must carry HQ_SESSION_ID (dispatch protocol §4).
Resolve the engine once, before the first dispatch (dispatch protocol §2):
an engine the user named, else codex, else the first of grok/claude that
resolves. Record it in workspace/orchestrator/{project}/state.json and reuse it
for the preflight, every story, and the regression gate.
CONFIRM the engine, model and effort with the user before the first dispatch. Resolution above picks a default; it is not a decision the user has made. A run is many hours of lane time on whatever model this picks, and the wrong pick is only visible once the work comes back — so ask once, up front, and never silently default into a long run. Skip the question ONLY when the user named an engine or model in the invocation itself.
Ask with a single AskUserQuestion (text fallback per
core/policies/hq-codex-decision-gate-fallback.md) naming concrete options —
the resolved default first, labelled as such — and covering engine, model, and
reasoning effort together. Then record all three in state.json as engine,
engine_model, and engine_effort, and pass them on every lane dispatch.
Passing model and effort to a lane (dispatch protocol §4's agent() opts):
| Engine | Model | Effort |
|---|---|---|
codex | model: opt, else tier map (gpt-5.6-sol plan / gpt-5.6-terra exec) | effort: opt → -c model_reasoning_effort=… |
grok | model: opt, else grok-4.6 | effort: opt → --reasoning-effort |
claude | model: opt, else tier map (opus plan / sonnet exec) | not wired — workflow-runner.mjs skips effort for claude. Pass it yourself as extraArgs: ["--effort", "low"]; the installed CLI accepts low|medium|high|xhigh|max |
Do not assume effort reaches a claude lane through the normal opt. It does not,
and a run asked for at low effort will quietly execute at the CLI default.
Record the session id next to it, in the same write. Both the pool and the
run dirs are keyed by the session that created them: conduct-pool.sh resolves
the session from the environment, and the protocol mints run dirs under the
session id (dispatch protocol §3). A later session therefore looks in its
own pool, finds nothing, and concludes the run has no live lanes — while the
original slots are still running and the original lane is still committing.
That is how a story gets dispatched twice. So:
So write two fields, not one: engine, and session_id set to the output of
bash core/scripts/hq-session.sh current. Use jq and a temp file rather than
editing the JSON in place.
Write session_id before the first dispatch, and never overwrite it on a
resume — it names the session that owns the lanes, not the session reading the
file. Re-resolving per lane
gives a run whose stories ran on different models — results that are not
comparable and a failure you cannot attribute. /run-project takes no
--engine flag; that flag belonged to the retired subprocess loop.
Fallback — no engine CLI. If no codex/grok/claude CLI resolves, detached lanes are
unavailable on this host: say so plainly, then run the story loop in-session with
spawn_agent/wait_agent (or Task) exactly as described in 3b's fallback note.
Everything else in this step — the pool claim, the return contract, the retry, the
proof gates — is identical on both paths. Do not discover a missing engine once
per story.
3a. Preflight Plan
The preflight explorer is a named pool slot, not a throwaway. Claim and
validate it exactly as .claude/skills/_shared/pool-lane-protocol.md describes
— assign, honour exits 3 and 4, mkdir -p the slot dir, check owner.json
before the spawn/resume split, and record running immediately after launch — a
lane left claimed while work is live is one cancel may retire as undispatched
(pool protocol §4):
bash core/scripts/conduct-pool.sh assign --worker-id "explorer" --task "{project} preflight"
The ownership check is not optional here just because the id is a constant — it
is more necessary. explorer is the same lane id for every project and every
company, so a second /run-project in one session resumes the first one's
planning transcript unless the stamp is checked. On a company or project
mismatch: cancel (not recycle — nothing has been dispatched into this claim
yet), clear, re-stamp, dispatch cold. Then:
Dispatch it as a lane per .claude/skills/_shared/lane-dispatch-protocol.md
({caller} = run-project, {lane} = explorer — the protocol mints the run
dir; do not spell one here — {tier} = plan — this is analysis, not execution, and the
protocol's tier is a caller-supplied placeholder precisely so this lane does not
inherit exec). The brief:
Read and analyze the PRD for {project}. Resolve prd.json, identify incomplete
userStories, sort by dependencies then priority then array order, classify each
story using /execute-task rules, identify worker sequences, read applicable hard
policy rules, and return only a concise markdown implementation plan followed by
a JSON block with project, company, repoPath, ordered_stories, hard_policies,
quality_gates, and resume_from.
Wait on it with the protocol's background waiter, then read the plan out of
agent-1.result.json per protocol §7 — jq -r '.value', never lane.log, which
also carries the runner's narration. On the no-engine fallback, the same brief goes to
spawn_agent({agent_type: "explorer", reasoning_effort: "low", …}) +
wait_agent(...) instead.
Display the plan. If the queue has ≤10 incomplete stories and none require session mode, proceed directly into the story loop without an approval stop. Otherwise (>10 stories, or session-mode stories present) ask the user to approve, adjust, switch to --interactive, switch to --ralph-mode, or stop. If structured question tooling is unavailable, use the plain-text fallback required by core/policies/hq-codex-decision-gate-fallback.md.
3b. Story Loop
For each approved incomplete story:
-
Announce story ID, title, and planned worker sequence.
-
Perform only lightweight parent orchestration: branch setup, state file update, and best-effort Linear sync.
-
Claim the story worker's lane, then dispatch into it. Classify the story to an HQ worker id first (same classification
/execute-taskuses), then:bash core/scripts/conduct-pool.sh assign \ --worker-id "story:{worker-id}" --task "{project}/{story-id}"The
story:prefix is not cosmetic. This wrapper does no domain work; it runs/execute-task, whose phases claim the bare worker id in their ownassign. A wrapper holdingbackend-devwould send its ownapi_developmentphase to exit 4 — waiting for a lane the wrapper itself is holding, with the wrapper waiting on that phase. Neither ever finishes. Coordinator lanes live in thestory:namespace; phase lanes use the bare id; they can never collide. The delimiter is a colon, not a slash:conduct-pool.shrestricts worker ids to[A-Za-z0-9._:-], sostory/backend-devexits 1 and no coordinator lane is claimed at all.Then follow
.claude/skills/_shared/pool-lane-protocol.mdfor the slot — the exit codes (3 and 4 both mean wait), themkdir -pandowner.jsoncheck before the spawn/resume split — and.claude/skills/_shared/lane-dispatch-protocol.mdfor the dispatch itself.record --status runningagainst the run-dir basename immediately after launch; a lane leftclaimedwhile work is live is onecancelmay retire as undispatched (pool protocol §4).record … --status idlethe moment the lane exits — before reading its JSON and before deciding whether to retry. Step 3b.4's one retry on malformed JSON re-dispatches throughassign, so a lane released only after the JSON validates sends exactly that retry to exit 4, waiting on a coordinator that has already finished.{caller}=run-project,{lane}={story-id}— the protocol mints the run dir, session-scoped and collision-free; do not spell a path here. Pool lane id:story:{worker-id}.{tier}=exec.deadline= now + the per-story budget (--timeout Nminutes, default 20). The brief is the prompt below.{"action":"spawn",...}means dispatch a fresh lane with the full prompt.{"action":"resume",...}means that coordinator already has an idle lane holding everything it learned on earlier stories of this class. A lane is a process that has already exited — there is nothing to reattach to — so do not treat this branch as unimplementable and do not leave it. A coordinator that cannot act on aresumeleaves the slot stuck and every later claim for that worker exits 4. Use the pool protocol's disk-backed restart: dispatch a new lane into the same slot, with the full prompt below prefixed byYou are resuming your own lane. Every story you already ran in this session is recorded in {slot dir}/handoffs.jsonl — read it first and do not redo anything it shows as done. That file belongs to this session and this company only; if it is absent, start from the brief.then
recordthe new run-dir basename against the same slot. The cap counts slots, not restarts. Append each validated story JSON to that file so the next restart can see it.Two stories that classify to the same worker id share one coordinator lane and therefore serialize on it. That is the intended behaviour, not a stall — their phases would have serialized on the bare worker lane anyway.
The brief written to the lane's brief.md (on the no-engine fallback, the same
text is the message: of spawn_agent({agent_type: "worker", reasoning_effort: "low", …}) followed by wait_agent(...)):
Execute story {project}/{story-id} by running /execute-task {project}/{story-id}.
You are not alone in the codebase. Own only this story and its declared files;
do not revert edits made by others; adapt to existing changes you encounter.
When /execute-task asks for a sub-agent, use your engine's own runtime adapter so
the nested HQ worker phases run as isolated agents inside this lane. They claim
their phase slots from the same session pool this lane was granted from.
Commit your story work before returning. If your runtime returns an integration
patch instead of a parent-visible commit, say so in notes and list every changed
path.
RETURN CONTRACT: json
Return ONLY this JSON object — no prose, no markdown fences, nothing before or after:
{
"status": "passed" | "failed" | "blocked",
"story_id": "{story-id}",
"commits": ["<short-sha>", ...],
"files_changed": <int>,
"back_pressure": {
"tests": "pass" | "fail" | "skip",
"lint": "pass" | "fail" | "skip",
"typecheck": "pass" | "fail" | "skip",
"build": "pass" | "fail" | "skip"
},
"workers_run": ["architect", "backend-dev", ...],
"evidence": ["repo-path:src/lib/foo.ts", "path:companies/{co}/projects/{project}/report.md", "url:https://...", ...],
"notes": "<1-2 sentence summary; include blocker description if status != passed>"
}
"evidence" lists what you actually produced, in these forms: path:<hq-relative> ·
repo-path:<relative to the repo> · branch:<name> · url:<https://…>. Commits go
in "commits" and count as evidence automatically. List only things that exist —
every item is verified on disk/git before the story is marked done, and a claim
that does not exist fails the story. An empty list is allowed.
Arm the protocol's background waiter on the lane. The story's JSON arrives in the
run dir, not as a tool return, and not in lane.log — read it per protocol
§7:
raw="$(jq -r '.value' "{run dir}/agent-1.result.json")" # unwrap the runner envelope
printf '%s' "$raw" | jq -e '.status, .workers_run, .evidence'
lane.log is the runner's whole stdout — narration plus
JSON.stringify(result), and because the story worker is schema-less that
result is a JSON string holding the story JSON. jq -e . against lane.log
therefore passes while .status, .workers_run, and .evidence all come
back empty, so step 4 would wave through a story it never actually read and
step 5's worker-proof gate would reject a worker that did run its phases. Two
jq calls, on agent-1.result.json, not one on the log.
- Validate
$raw— the unwrapped value, not the log — as JSON withjq -e .(or equivalent). If invalid, retry exactly once with this stricter prompt addition:Your previous reply was not valid JSON. Emit ONLY the JSON object specified above. No prose, no fences, no trailing newline.If still invalid, mark the storyblockedwith reasonINVALID_RETURN_FORMAT, surface to user, do NOT advance to the next story. (Enforced by ralph-orchestrator-context-discipline.) - Enforce the worker proof gate: passed stories must include at least one real HQ worker ID in
workers_run; reject placeholder-only values likecodex,worker,general-purpose, orcommit. - Verify commits are parent-visible with
git log --oneline -n {len(commits)}. If the worker produced an integration patch instead of a visible commit, review/integrate it in the parent and create the story commit before continuing. - Verify the worker's evidence and mark the story. Run
bash core/scripts/verify-story-deliverables.sh --prd <prd.json> --story <id> [--repo <repoPath>] --evidence-json '<reply.evidence>' --commits-json '<reply.commits>' --write. Exit 0 records the verified references on the story asevidence[](audit trail) and you may writepasses: true. Exit 3 names what does not exist — a claimed path/branch/commit/URL that is missing, or a deliverable the PRD declared that was never produced — and the story is NOT done: do not write the flag, treat it like a failed back-pressure check. A worker that returns no evidence and a PRD that declares none is allowed (the story is marked done but unverified); this gate is deliberately not strict. Markpasses: trueonly after status ispassed, worker proof passes, back-pressure is acceptable, commit verification succeeds, and this evidence check exits 0. - Update
workspace/orchestrator/{project}/state.json. Record the story's run dir on the story entry as you dispatch it, not after it returns — a run that loses its session mid-story is exactly the case where the path is needed, and it is the only record of which lane belongs to which story. - Narrate one line per story to the user:
[{story_id}] {status} · {files_changed} files · {first_commit_short_sha}. Anything longer goes toworkspace/threads/journal/<date>/<story-id>.md, not the parent transcript. - Auto-continue to the next incomplete story until the queue is empty or a story is
failed/blocked(then surface and stop). Pause only per the recommendation-posture exceptions in Step 1: >10 incomplete stories (single preflight approval) or a story that requires session mode (pause before that story only).
3c. Regression Gates
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 85
- Forks
- 16
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
run-project- Source
- github.com/indigoai-us/hq-core