feat(examples): task-graph — a plan of subtasks run as parallel agents, plus a skill to run it on Cloud - #555
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ktree via cwd Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Warning Review limit reachedNext included review available in 15 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Devin Review found 4 potential issues.
4 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| }; | ||
|
|
||
| const runSubtask = async (s: Subtask, parent?: string): Promise<void> => { | ||
| await Promise.all((s.dependsOn ?? []).map((d) => merged.get(d))); |
There was a problem hiding this comment.
🔴 Later dependencies start too early
A subtask starts early when its dependency appears later in the input array. merged.get() returns undefined, which Promise.all treats as resolved.
Learn more
The scheduler starts each async runSubtask while it is still populating merged. An async function executes through its first await immediately, so dependency lookup happens before later array entries are registered. The same ordering affects a follow-up that depends on a later sibling in its report.
Example: With [frontend dependsOn backend, backend], frontend reads no promise for backend and acquires a slot immediately. It can edit against the pre-backend tree although the plan requires backend first.
Recommended fix: Register every subtask in a batch before any runSubtask body can execute. For example, defer execution with Promise.resolve().then(() => runSubtask(...)), and route the initial plan through the same schedule helper.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 99cab6f. runSubtask now yields before reading dependencies, so the scheduling loop registers every subtask first. An unscheduled dependency now throws instead of resolving as undefined. Verified with a real run: the 4-subtask plan listed in reverse order (readme, index, truncate, slugify) merged in dependency order and completed success.
| const branch = `task-graph/${s.id}`; | ||
| // Branched from the run's branch as it is NOW, so every dependency's code is already in it. | ||
| const base = (await onRunBranch(() => f.run( | ||
| `git worktree add -q -B ${shellWord(branch)} ${shellWord(tree)} HEAD && git rev-parse HEAD`, |
There was a problem hiding this comment.
🔴 Existing task branches are overwritten
A preexisting task-graph/<id> branch is reset to the run’s HEAD. The user loses that branch’s reference to its commits.
Learn more
git worktree add -B <branch> <path> HEAD force-resets an existing branch before creating the worktree. Subtask IDs are short user-controlled names, so names such as task-graph/schema can already exist in a customer's repository.
Example: A user has unpublished work on task-graph/schema at commit A. Running a plan with ID schema moves that branch to the run's HEAD B, leaving A reachable only through recovery tools.
Recommended fix: Namespace branches with a run-unique identifier and refuse any collision before creating a worktree. Avoid -B; create a new branch with -b, then retain that exact unique name for merge and cleanup.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 99cab6f. Branches are now task-graph/<base-commit>/<id> and created with -b, so an existing branch makes the step fail instead of being reset. A rerun from the same commit clears only that namespace, which only this flow writes.
| await f.run( | ||
| 'if [ -f package.json ] && node -e \'p=require("./package.json");process.exit(p.scripts&&p.scripts.test?0:1)\'; ' + | ||
| 'then npm ci --no-audit --no-fund && npm test; else echo "no test script; skipping"; fi', | ||
| { timeout: "15m" }, | ||
| ); |
There was a problem hiding this comment.
🔴 Non-Node test suites never run
Repositories without an npm test script always print skipping and succeed. Their integrated changes can reach the final PR without any test run.
Learn more
The final gate recognizes only package.json with scripts.test. The flow and skill accept an arbitrary repository, and the final result can be pushed into a pull request, so every other build system bypasses the advertised integrated test run.
Example: In a Rust repository containing Cargo.toml, all subtask branches merge. The final command prints no test script; skipping, returns zero, and the ticket-triggered path opens a PR without running cargo test.
Recommended fix: Make the integrated test command explicit plan input or planner output and validate it before agents start. Alternatively, detect supported project manifests with deterministic commands and stop as needs_human when no test command can be established; do not treat an unknown test suite as success.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 99cab6f. New testCommand input. It defaults to npm ci && npm test only when there is an npm test script. With neither, the run stops as needs_human before testing or opening a PR. It never passes untested. README and skill document it.
| await f.run( | ||
| 'if [ -f package.json ] && node -e \'p=require("./package.json");process.exit(p.scripts&&p.scripts.test?0:1)\'; ' + | ||
| 'then npm ci --no-audit --no-fund && npm test; else echo "no test script; skipping"; fi', | ||
| { timeout: "15m" }, | ||
| ); |
There was a problem hiding this comment.
There was a problem hiding this comment.
Not changing this: the final test step adds no authority. Each agent already runs arbitrary shell commands (tests, builds, git) in the same sandbox, as the same user, with the same credentials as the runner. So an agent that edits package.json gains nothing it didn't already have. The trust boundary is the sandbox, not the agent/runner split. On Cloud that sandbox is per-run, and locally it's the user's own checkout.
| }; | ||
|
|
||
| const runSubtask = async (s: Subtask, parent?: string): Promise<void> => { | ||
| await Promise.all((s.dependsOn ?? []).map((d) => merged.get(d))); |
There was a problem hiding this comment.
Dependencies ignore later plan order
High Severity
runSubtask reads merged.get for each dependsOn id before those ids are inserted into merged. A valid DAG that lists a dependent before its dependency therefore waits on undefined and starts immediately. planError accepts any order, and Linear-built plans are not topologically sorted, so required merges are skipped.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit baa8476. Configure here.
There was a problem hiding this comment.
Fixed in 99cab6f, same change as the Devin thread above. Dependencies are read after every subtask is registered, and an unknown one fails closed. Verified with a reverse-ordered plan: merges landed slugify/truncate → index → readme.
| `(followups is usually empty; ids must be new).` | ||
| : `Finally write ${result} as JSON: {"summary":"what you did"}.`), | ||
| // Done means a result file AND at least one commit on the branch — not the agent saying so. | ||
| }).gate({ type: "subprocess_gate", command: `test -s ${shellWord(result)} && test "$(git -C ${shellWord(tree)} rev-list --count ${base}..HEAD)" -gt 0` }); |
There was a problem hiding this comment.
Result gate skips JSON check
Medium Severity
The subtask gate only checks that the result file is non-empty, then the branch is merged, then JSON.parse reads that file. Non-JSON output (agents often wrap JSON in fences) passes the gate, merges, and then throws, so dependents never run. The planner gate already requires parseable JSON.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit baa8476. Configure here.
There was a problem hiding this comment.
Fixed in 99cab6f. The subtask gate now requires JSON.parse to succeed on the result file, the same check as the planner gate, as well as a commit on the branch. A fenced or non-JSON result fails the gate before the merge.
| `Run the affected tests, then commit the merge.`, | ||
| }).gate({ type: "subprocess_gate", command: `git merge-base --is-ancestor ${shellWord(branch)} HEAD` })); | ||
| } | ||
| await f.run(`git worktree remove --force ${shellWord(tree)}`); |
There was a problem hiding this comment.
Worktree remove races git lock
Medium Severity
git worktree remove runs outside onRunBranch, so it overlaps another subtask's serialized git worktree add or git merge on the same repo. Concurrent git worktree/index locks fail the finishing subtask after its agent and merge already succeeded.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit baa8476. Configure here.
There was a problem hiding this comment.
Fixed in 99cab6f. git worktree remove now runs through onRunBranch, so it's serialized with worktree creation and merges.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: baa8476883
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const outcome = (await onRunBranch(() => f.run( | ||
| `git merge --no-ff -q -m ${shellWord(`task-graph: merge ${s.id}`)} ${shellWord(branch)} >/dev/null 2>&1 && echo merged || { git merge --abort; echo conflict; }`, |
There was a problem hiding this comment.
Derive step identities independently of completion order
When two independent agents finish in a different order from a later resume, each merge reaches this f.run in a different order and therefore receives a different ordinal-based step ID. Authored child admission keys are keyed by that ordinal, so a previously recorded merge can be matched with another subtask's command and the kernel refuses the changed spec; a crash after one parallel branch finishes can therefore make the run impossible to resume. Assign stable per-subtask identities or otherwise make operation creation order deterministic, and cover the parallel crash/resume case required by the repository.
AGENTS.md reference: AGENTS.md:L19-L21
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct: authored step ids are ordinals by call order, and with concurrent subtasks that order depends on which agent finishes first. A resume could pair a step with another step's journal. The fix belongs in the SDK: step identity needs to be keyed independently of completion order. It can't be fixed inside one example flow. In bdd6624 the README states the limit plainly: don't resume, rerun. Setup already clears a previous run's worktrees and branches, so a rerun is safe.
| `fit one focused agent session. Make dependsOn honest: only list a dependency when the subtask ` + | ||
| `really needs that code merged first — everything else runs in parallel. Do not write code.\n` + | ||
| `Write ONLY this JSON to ${work}/plan.json:\n${PLAN_SHAPE}\n\nTask:\n${brief}`, | ||
| }).gate({ type: "subprocess_gate", command: `node -e 'JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"))' ${shellWord(`${work}/plan.json`)}` }); |
There was a problem hiding this comment.
Contain the planner's writes before accepting its plan
When input.plan is omitted, the planner runs in the repository root with write access, while this gate verifies only that .relayflow/plan.json parses. If the coding agent edits any tracked or untracked project file despite the prompt, those edits remain in the run branch, are tested with the merged subtasks, and are returned by flows sync or included in the triggered PR without belonging to any reviewed subtask. Run planning in an isolated worktree or reject/reset every write outside the plan file.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in bdd6624. The planner gate now requires both a parseable plan.json and git status --porcelain to be empty. .relayflow/ is excluded, so only project files count. A planner that edits the repo fails the gate and the run stops before any subtask starts.
| 'if [ -f package.json ] && node -e \'p=require("./package.json");process.exit(p.scripts&&p.scripts.test?0:1)\'; ' + | ||
| 'then npm ci --no-audit --no-fund && npm test; else echo "no test script; skipping"; fi', |
There was a problem hiding this comment.
Fail instead of succeeding without repository tests
For any repository that is not an npm project with a root package.json and scripts.test—including Rust, Go, Python, pnpm-only, and many monorepos—this branch merely prints that tests were skipped and the flow subsequently calls done("success") or opens a PR. That contradicts the example's stated integrated-test gate and reports unverified output as complete; accept a declared test command or stop for human input when no supported test command can be proven.
AGENTS.md reference: AGENTS.md:L35-L36
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Already fixed in 99cab6f (this comment is on the earlier commit). The new testCommand input is used; without it, the flow runs npm ci && npm test only when there's an npm test script. With neither, the run stops as needs_human before testing or opening a PR.
| // Flow bookkeeping and worktrees live under an excluded dir so no merge or `git add -A` picks them up. | ||
| await f.run( | ||
| `rm -rf ${shellWord(work)} && git worktree prune && mkdir -p ${shellWord(`${work}/results`)} && ` + | ||
| `{ grep -qxF '.relayflow/' .git/info/exclude 2>/dev/null || echo '.relayflow/' >> .git/info/exclude; }`, |
There was a problem hiding this comment.
Resolve the Git exclude path in linked worktrees
When the documented local mode is launched from a linked Git worktree, .git is a regular gitfile rather than a directory, so redirecting to .git/info/exclude fails with Not a directory and the initialization step aborts before any subtask starts. Obtain the path through git rev-parse --git-path info/exclude instead of assuming the primary-checkout layout.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in bdd6624. Setup resolves the exclude file with git rev-parse --git-path info/exclude, and creates its directory, so it works from a linked worktree where .git is a gitfile. I checked it inside a real linked worktree: it resolves to the shared <common-dir>/info/exclude.
| **No sub-issues, or a written spec instead of Linear.** Omit `plan` | ||
| entirely. A planner agent then reads the repository and writes the graph itself. |
There was a problem hiding this comment.
Require plan approval before auto-planned work starts
For a written spec or a Linear issue without sub-issues, these instructions explicitly omit plan, so there is no graph available for step 3 to display or approve. The actual graph is created only after submission, and the flow immediately schedules its coding agents, violating the skill's explicit promise that the user approves the graph before any agent starts; generate the plan before confirmation or add a durable human gate after planning.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in bdd6624. The skill now has the calling agent write the plan itself, both from a spec and from a Linear issue without sub-issues, so step 3 always shows the real graph for approval. The in-flow planner, which runs without approval, is only for the hands-off flows deploy mode.
| `(followups is usually empty; ids must be new).` | ||
| : `Finally write ${result} as JSON: {"summary":"what you did"}.`), | ||
| // Done means a result file AND at least one commit on the branch — not the agent saying so. | ||
| }).gate({ type: "subprocess_gate", command: `test -s ${shellWord(result)} && test "$(git -C ${shellWord(tree)} rev-list --count ${base}..HEAD)" -gt 0` }); |
There was a problem hiding this comment.
Validate the result JSON before merging the branch
The completion gate accepts any nonempty result file, but the flow merges and removes the worktree before parsing that file at line 178. If an agent writes fenced JSON, truncated JSON, or any other nonempty malformed output, its code is merged first and the flow then fails during JSON.parse, leaving an integrated branch with no usable report. Parse and validate the expected result shape in this pre-merge gate.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Already fixed in 99cab6f (this comment is on the earlier commit). The subtask gate now runs JSON.parse on the result file, along with requiring a commit on the branch. That happens before the merge and before the worktree is removed, so a fenced or malformed result fails the gate and nothing is merged.
…pace, no silent test skip - Read dependencies after the scheduling loop registers every subtask, so a plan need not be topologically sorted (Linear's is not). Fail closed on an unscheduled dependency instead of treating it as resolved. - Name subtask branches task-graph/<base-commit>/<id> and create them with -b, so a repository's existing branches are never reset. - Gate each subtask on a parseable result file, not just a non-empty one. - Serialize worktree removal with the other run-branch git operations. - Take testCommand; with neither it nor an npm test script, stop as needs_human rather than passing untested. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… exclude via --git-path - The planner gate now also requires an untouched working tree, so a planner that edits project files fails instead of leaking edits into the run. - Resolve info/exclude with git rev-parse --git-path so setup works from a linked worktree, where .git is a file. - Skill: the calling agent always writes the plan so the user approves the real graph; the in-flow planner is for hands-off deploys only. - README: rerun rather than resume; authored step ids follow call order. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bdd6624. Configure here.
| }).gate({ | ||
| type: "subprocess_gate", | ||
| command: `node -e 'JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"))' ${shellWord(`${work}/plan.json`)} && ` + | ||
| `test -z "$(git status --porcelain)"`, |
There was a problem hiding this comment.
Planner gate misses committed edits
Medium Severity
The new planner gate only requires git status --porcelain to be empty. A planner that commits its edits leaves a clean tree, so the gate passes and those commits become HEAD for every later subtask worktree.
Reviewed by Cursor Bugbot for commit bdd6624. Configure here.
There was a problem hiding this comment.
Fixed in a8d1714. The flow now records HEAD before the planner runs, and the gate requires HEAD to be unchanged as well as a clean working tree. A planner that commits its edits fails the gate, and the run stops before any subtask starts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>


What
examples/task-graph: one large engineering task, about a day of work, runs as a graph of parallel coding agents. It also ships a Claude Code skill a customer's agent can use to run the flow on Agent Relay Cloud.This was built for a prospect whose Claude work gets stuck doing "seven subtasks one at a time". They asked for a graph of subtasks where each node shows its state.
task-graph.flow.tstakes a plan (subtasks withdependsOn), or has a planner agent write one.f.agent({ cwd })), then merges back. A conflict goes to a resolver agent, gated on the branch actually being merged.maxFollowupsfollow-ups (default 3, and follow-ups can't spawn their own). Without the cap, a 4-subtask test run grew to 20.flows deploy --on linear:team=…) opens one PR per ticket.example-plan.json: a 7-subtask "team invitations" feature, four waves.skill/run-task-graph/SKILL.mdwalks the customer's agent through:flowsCLI and Cloud login;flows run --cloud --sync-code;flows status --cloud;flows sync.examples/tsconfig.jsonnow typechecks the new example.Depends on
cwd, completion-gate fix). On 2.0.26 the second concurrent agent parks andcwdis refused. Merge after fix(sdk,kernel): run authored agents in parallel under --local-agent #554.agent-4,run-7and so on.Verified
examples/task-graphran with real Claude agents, a 4-subtask diamond,--local-agent.slugifyandtruncateoverlapped for about 32s;indexwaited for both to merge.completionReason: success, 137s, and the integrated tests passed.npm run typecheck:examples: no errors intask-graph. Three errors elsewhere are already onmain(the ramp helper export andURLinstuck-run-triage).flows check examples/task-graph/task-graph.flow.ts: passed.Limits (also in the README)
budgetheader: a budgeted authored flow runs one step at a time today (authored-budget.ts).🤖 Generated with Claude Code
Summary by cubic
Adds
examples/task-graph, a flow that runs one large task as a graph of parallel coding agents, each in its own git worktree and branch, starting once its dependencies have merged. A subtask only counts as done when it has a commit and a result file, and planned subtasks can add up tomaxFollowups(default 3) follow-up subtasks mid-run. Also ships a Claude Code skill (skill/run-task-graph) that turns a Linear issue into a plan, gets user approval, and runs it on Cloud, plus a 7-subtask example plan and README.Plans need not be sorted: dependencies are read after every subtask is registered, and an unscheduled dependency fails closed. Subtask branches are namespaced under
task-graph/<base-commit>/<id>so existing repository branches are never reset. Gates check for a parseable result file plus a commit; the planner gate additionally requires an untouched working tree and unchanged HEAD, so a planner that edits files or commits fails instead of leaking changes into the run. Worktree removal is serialized with the other run-branch git operations, andinfo/excludeis resolved via--git-pathso setup works from a linked worktree. AtestCommandcan be supplied; with neither it nor an npm test script, the run stops asneeds_humaninstead of passing untested.Depends on #554 for parallel agents and
f.agent({ cwd }); without it the example is broken on 2.0.26. Cloud will show generic step names until #553 and AgentWorkforce/cloud#3945 land. Nobudgetheader, since budgeted authored flows run one step at a time today. Resuming after a runner crash is untested, and the follow-up cap is not exercised in a real run.Written for commit a8d1714. Summary will update on new commits.
Note
Low Risk
New example and documentation only; no changes to SDK, kernel, or production runtime paths.
Overview
Adds
examples/task-graph, a new authored flow that runs one large engineering task as a dependency graph of parallel Claude agents, each in its own git worktree and branch, starting only after itsdependsOnsubtasks have merged back.The flow validates plans (DAG, ids, caps), optionally uses a planner agent when no plan is supplied, gates completion on commits plus result JSON (not agent claims), handles merge conflicts with a resolver agent, allows capped mid-run follow-ups (
maxFollowups), runs the integrated test suite once at the end (or stops asneeds_humanwithouttestCommand/npm test), and opens a single PR when triggered from a Linear ticket. Git work uses namespaced branches undertask-graph/<base-commit>/and.relayflow/bookkeeping excluded from merges.Also ships
example-plan.json(7-subtask team invitations), README (Cloud/local/deploy limits), arun-task-graphClaude Code skill for Linear → plan → approval → Cloud run →flows sync, and includestask-graph/*.tsinexamples/tsconfig.jsonfor typechecking.Depends on a post-2.0.26 SDK for concurrent agents and
f.agent({ cwd }); intentionally omits a flowbudgetheader because budgeted authored flows serialize steps today.Reviewed by Cursor Bugbot for commit bdd6624. Bugbot is set up for automated code reviews on this repo. Configure here.