---
name: claudex-workflow
description: Run multi-agent work in Claudex via the Workflow tool with gpt models (luna/sol/terra). Use when the user asks for a dyn-wf, a dynamic workflow, parallel subagents, or names a gpt model for coding or review.
---

# Claudex Workflows

audience: AI coding agents first. BLUF, imperative.

This session is **Claudex**, not stock Claude Code. `Workflow`'s `agent()` accepts **gpt models**. The `model` enum in the tool schema (`sonnet|opus|haiku|fable`) is INCOMPLETE — it does not list what Claudex actually resolves. Pass the gpt name anyway.

Verified: a live run's per-agent `agent-*.meta.json` recorded `{"agentType":"workflow-subagent","model":"gpt-5.6-luna"}` and `"gpt-5.6-sol"` from bare `model: 'gpt-5.6-luna'` / `'gpt-5.6-sol'`. **Confirm it resolved on your own run** — read `model` in `<transcriptDir>/agent-*.meta.json`. A wrong name may fall back to the session model silently rather than error.

## Routing — pick the executor FIRST

| User says | Executor |
|---|---|
| "dyn-wf", "dynamic workflow", "parallel subagents", "fan out agents" | **`Workflow` tool.** This skill. |
| "/run-plan", "run the plan" | `cd <repoRoot> && factory adw_plan_build_test_quality docs/plans/<file>.md` |
| names a gpt model for the workers | **`Workflow` tool** — it takes per-agent `model`/`effort` |

## Models

Set `model` + `effort` per `agent()` call:

```js
const CODER    = { model: 'gpt-5.6-luna', effort: 'max' }
const REVIEWER = { model: 'gpt-5.6-sol',  effort: 'low' }
```

Families: `gpt-5.6-luna` · `gpt-5.6-sol` · `gpt-5.6-terra`.

`Workflow` accepts `low`, `medium`, `high`, `xhigh`, and `max` effort. `max` is allowed on `gpt-5.6-luna` only — never on another model. Pass the model family and `effort` to `agent()`.

**Omit `model` only when the user expressed no preference** — the agent then inherits the session model.

Role defaults when the user names none: general coding `luna`/max · review `sol`/low · auth and money paths `sol`/medium. Canonical: memory `feedback_codex_role_models`.

**`sol` is for code review only** — an agent that reads a diff and renders a verdict. A **verification** agent that runs commands and reports what it observed (curl a URL, grep the HTML, check an HTTP code, tail a log, smoke a deploy) is mechanical, not judgment: give it `{ model: 'gpt-5.6-luna', effort: 'low' }`. Naming the constant `REVIEWER` does not make the work review. Canonical: memory `feedback_sol_is_for_code_review_not_smoke`.

## Small coder model ⇒ decompose harder

A small model (`luna`) needs the task pre-chewed. One task = one file = one contract = one acceptance command. Never hand it "build the settings feature".

## The wave pattern

Group tasks into waves. Same wave ⇒ **strictly disjoint file sets** — same-wave agents run concurrently against the SAME working tree, so two agents on one file corrupt each other. Later wave ⇒ real dependency only.

```js
export const meta = {
  name: 'feature-slug',
  description: 'one line, shown in the permission dialog',
  phases: [{ title: 'Wave 1 — model' }, { title: 'Wave 2 — API' }],
}

const CODER    = { model: 'gpt-5.6-luna', effort: 'max' }
const REVIEWER = { model: 'gpt-5.6-sol',  effort: 'low' }
const REPO = '/abs/path/to/repo/root'

const WAVES = [
  { phase: 'Wave 1 — model', tasks: [1, 2, 3] },
  { phase: 'Wave 2 — API',   tasks: [4, 5] },
]

async function runTask(n, phase) {
  const result = await agent(implementPrompt(n), { label: `impl:t${n}`, phase, schema: RESULT, ...CODER })
  const verdict = await agent(reviewPrompt(n, result), { label: `review:t${n}`, phase, schema: VERDICT, ...REVIEWER })
  if (verdict?.approved === false && verdict.blocking_findings?.length) {
    const fixed = await agent(fixPrompt(n, verdict), { label: `fix:t${n}`, phase, schema: RESULT, ...CODER })
    return { task: n, result: fixed, verdict: await agent(reviewPrompt(n, fixed), { label: `reverify:t${n}`, phase, schema: VERDICT, ...REVIEWER }) }
  }
  return { task: n, result, verdict }
}

for (const wave of WAVES) {
  phase(wave.phase)
  const done = (await parallel(wave.tasks.map((n) => () => runTask(n, wave.phase)))).filter(Boolean)
  const bad = done.filter((o) => o.result?.acceptance_passed !== true || o.verdict?.approved !== true)
  if (bad.length || done.length !== wave.tasks.length) {
    return { halted_at: wave.phase, unresolved: bad }   // later waves depend on this one — HALT
  }
}
```

**Barrier between waves is CORRECT here** — wave N+1 consumes wave N's code. Do NOT `pipeline()` across waves.

## Prompt contract per agent

Every implement prompt MUST carry:

1. Absolute repo root, and that every plan path is relative to it.
2. "Edit ONLY the files your task's Files list names. Another agent owns every other file RIGHT NOW."
3. Read-only trees named explicitly, if any.
4. Pointer to the plan doc + task heading. Do NOT re-inline the contract the plan already owns — EXCEPT a contract authored in an EARLIER wave that this agent consumes (route table, response shape): restate that **inline, verbatim**. A cross-wave agent never sees the other agent's diff, and will invent endpoint names from a cross-reference.
5. "Run the acceptance command and report its REAL output. Never claim a pass you did not observe."
6. Exact commit paths + message.
7. **"`commit_sha` MUST be the commit carrying YOUR task's files. If no amend was needed, report your ORIGINAL commit sha. NEVER report another task's or an owner/infrastructure commit."** Omit this and a fix-round agent with nothing to amend cites whatever sha it last saw; the reviewer reads that diff, finds files the task never owned, and rejects — a false halt costing two full rounds.
8. **"A repo-wide tooling/dependency warning outside your Files list is the owner's to fix, not yours. Report it and judge your task on its own Files list."** Otherwise an agent blocks forever on something it is forbidden to touch.

Reviewer prompt: adversarial, **report only, never fix**, re-run the acceptance command itself, and check for files touched outside the task's list.

Force structure with `schema` — `{acceptance_passed: boolean, acceptance_output: string, commit_sha, files_changed}` for coders, `{approved: boolean, blocking_findings: [...]}` for reviewers. Validation happens at the tool layer, so the model retries on mismatch instead of you parsing prose.

## Script constraints

- Plain **JavaScript**, not TypeScript. Type annotations fail to parse.
- `Date.now()` / `Math.random()` / argless `new Date()` **throw** — they would break resume. Pass timestamps via `args`; vary by index for randomness.
- No filesystem, no Node APIs, inside the script. Agents do the file work.
- `parallel()` never rejects — a failed thunk resolves `null`. **Always `.filter(Boolean)`** and treat a shrunken array as dead agents, not success.
- Concurrency caps at `min(16, cores-2)`; excess queues. Pass all tasks anyway.

## Is an idle agent stuck? — three checks

An agent with a 24h Bash timeout that loses its child process hangs for 24 hours. Diagnose, never wait it out:

1. **Tail its transcript.** `<transcriptDir>/agent-<id>.jsonl` — last entry a `tool_use` with NO matching `tool_result`? It is waiting on that call.
2. **`ps` for that command.** Not running ⇒ orphaned; the result will never arrive. Running ⇒ genuinely working, leave it.
3. **Check the artifact the command should produce** (build output mtime, commit). Untouched confirms it never ran.

Corroborating tell: `ls -lt` the transcript dir — **every** agent file silent for minutes means the run, not one agent, is wedged. Compare `started` vs `result` counts in `journal.jsonl` to name the missing agent.

Recovery: `TaskStop` the run, then resume with `{scriptPath, resumeFromRunId}`. Completed agents replay from cache; only the orphan re-runs. `SendMessage` to a workflow subagent ALWAYS fails (`No transcript found for agent ID`) — they are not resumable individually.

## Resume cache = longest unchanged prefix

Resume is an ordered transcript, **not per-call memoization**. Calls match on `(prompt, opts)` only while every preceding call also matches. First changed/new call runs live; **every later call runs live too**, even when its own prompt is byte-identical.

Decision rule:
1. Identify earliest call needing correction.
2. Expect that call + full suffix to rerun. A middle call cannot rerun alone.
3. Change only what feeds that call so earlier prefix stays cached.
4. Already-complete nontarget **before** edited call running again ⇒ cache preservation failed. `TaskStop` immediately. A nontarget **after** edited call running again ⇒ expected suffix replay; leave it.

**Never edit shared `RULES` or a prompt template mid-run to fix one task** — that invalidates from first call onward. Append correction to narrowest task-specific branch, and keep every `phase` string byte-identical: `phase` lives in `opts`.

**Empty interpolation still changes bytes.** DO NOT add `${taskCorrection(n)}` on its own template line while returning `''` for unaffected tasks: line newline remains and moves earliest changed call forward, expanding replay. Branch before constructing prompt; unaffected branch MUST return original template byte-for-byte:

```js
// DO NOT — nontargets gain one newline:
return `${RULES}\n${taskCorrection(n)}\nRead plan`

// target — exact original for nontargets:
const correction = taskCorrection(n)
if (!correction) return originalPrompt(n)
return correctedPrompt(n, correction)
```

A cached `acceptance_passed: false` from a condition since fixed halts again on replay. Bust it deliberately, state resolution in prompt, and budget for suffix replay.

## Iterate without resending

Every invocation persists the script and returns its path. Edit that file, then:

```
Workflow({ scriptPath: '<path>', resumeFromRunId: '<runId>' })
```

Unchanged `agent()` calls return cached results instantly; the first edited call and everything after runs live. Before diagnosing an empty result, Read `<transcriptDir>/journal.jsonl` — it records each agent's actual return value.

## Landing

Workflows have no ship phase. Agents commit their own task; the workflow leaves the branch as-is. Land it yourself after the run — see the project's `.claude/scripts/ship.sh` and the SHIP END-TO-END rule in global CLAUDE.md. **Never leave the work built-but-unlanded and call it done.**
