Example pipelines
Each example below is a complete pipeline JSON that passes Jimothy’s validation. To use one, copy it, open Pipelines → New pipeline, paste it under Or import a pipeline JSON, and click Import. Then fill in the workspace (Local repository is left empty on purpose) and adjust harnesses and models to the ones you have.
1. Python feature: parallel checks and a gated PR
Section titled “1. Python feature: parallel checks and a gated PR”A feature pipeline for a Python repository that shows most of the pipeline features at once:
- Plan (Claude Code,
opus) → Implement (Claude Code,sonnet, with atest-writersubagent). - pytest and ruff run in parallel after Implement. Both have Continue on error, so a failure in one doesn’t skip the other.
- Checks passed? joins them and fails unless both succeeded. It owns the feedback loop back to Implement, so the loop fires only after both checks are finished (no half-finished parallel siblings).
- Codex review loops back to Implement on anything but
VERDICT: APPROVE. - Human approval can also loop back: reject with a comment and Implement runs again with it.
- Open pull request pushes the branch and opens the PR.
plan ─► implement ─┬─► pytest ─┬─► gate ─► review ─► approve ─► pr ▲ └─► ruff ──┘ │ │ │ └───────────────────────┴─────────┴──────────┘ loop backsThings to notice:
- The gate command compares statuses:
test {{steps.tests.status}} = succeededrenders astest 'succeeded' = succeeded, because values in shell steps are quoted automatically. - The Implement prompt uses
{{#if steps.tests.error}}and{{#if steps.lint.error}}to include only the checks that failed. A step’s error is cleared when it succeeds, so passing checks don’t add noise. - Rejection feedback comes from
{{steps.approve.output}}, not.comment: the loop resets the approval step and clears its comment, but its output keeps it. - The branch template includes
{{run.number}}, so re-running the same issue creates a new branch.
{ "jimothyPipeline": 1, "name": "Python feature: parallel checks + gated PR", "description": "Claude Code plans and builds, pytest and ruff run in parallel, Codex reviews, you approve or send it back, a PR opens.", "icon": "rocket", "color": "#60a5fa", "repo": { "mode": "worktree", "localPath": "", "baseBranch": "main", "branchTemplate": "jimothy/{{issue.key | slug}}-{{run.number}}" }, "concurrency": 2, "variables": { "testCommand": "pytest -q", "lintCommand": "ruff check ." }, "subagents": [ { "id": "test-writer", "description": "Writes focused pytest tests for new or changed code. Use after implementing a change.", "prompt": "You write minimal, meaningful pytest tests that follow the repository's existing test layout and fixtures. Run them and fix failures in the tests you wrote.", "tools": [ "Read", "Grep", "Glob", "Edit", "Write", "Bash" ], "model": "sonnet" } ], "steps": [ { "id": "plan", "name": "Plan", "type": "agent", "harnessId": "claude-code", "model": "opus", "dependsOn": [], "timeoutMinutes": 20, "prompt": "You are the planning agent on Jimothy, an automated software delivery crew.\n\nIssue {{issue.key}}: {{issue.title}}\n{{#if issue.url}}Link: {{issue.url}}{{/if}}\n\n{{issue.description}}\n\nStudy the repository and write a concise implementation plan: files to change and why, edge cases, and the tests to add. Do NOT modify any files. Output only the plan in Markdown." }, { "id": "implement", "name": "Implement", "type": "agent", "harnessId": "claude-code", "model": "sonnet", "dependsOn": [ "plan" ], "timeoutMinutes": 60, "retries": 1, "subagents": [ "test-writer" ], "prompt": "You are the implementation agent on Jimothy. You are on git branch {{run.branch}}.\n\nIssue {{issue.key}}: {{issue.title}}\n\n{{issue.description}}\n\nPlan:\n{{steps.plan.output}}\n{{#if steps.tests.error}}\nThe test suite failed on the previous attempt:\n{{steps.tests.output | truncate:4000}}\n{{/if}}{{#if steps.lint.error}}\nThe linter failed on the previous attempt:\n{{steps.lint.output | truncate:2000}}\n{{/if}}{{#if steps.review.output}}\nA reviewer requested changes. Address every point:\n{{steps.review.output}}\n{{/if}}{{#if steps.approve.output}}\nThe human reviewer rejected the previous attempt:\n{{steps.approve.output}}\n{{/if}}\nImplement the change completely. Use the test-writer subagent for tests. Run `{{vars.testCommand}}` and `{{vars.lintCommand}}` before you finish. Commit referencing {{issue.key}} and end with a short summary." }, { "id": "tests", "name": "pytest", "type": "shell", "dependsOn": [ "implement" ], "command": "{{vars.testCommand | raw}}", "timeoutMinutes": 20, "continueOnError": true }, { "id": "lint", "name": "ruff", "type": "shell", "dependsOn": [ "implement" ], "command": "{{vars.lintCommand | raw}}", "timeoutMinutes": 5, "continueOnError": true }, { "id": "gate", "name": "Checks passed?", "type": "shell", "dependsOn": [ "tests", "lint" ], "command": "test {{steps.tests.status}} = succeeded && test {{steps.lint.status}} = succeeded", "loopBackTo": "implement", "maxLoops": 2 }, { "id": "review", "name": "Codex review", "type": "agent", "harnessId": "codex", "dependsOn": [ "gate" ], "timeoutMinutes": 20, "passPattern": "VERDICT:\\s*APPROVE", "loopBackTo": "implement", "maxLoops": 2, "prompt": "You are a strict senior reviewer on Jimothy.\n\nReview `git diff {{run.baseBranch}}...HEAD` for {{issue.key}}: {{issue.title}}\n\n{{issue.description}}\n\nCheck correctness, tests and code quality. Do not modify files.\nEnd your answer with exactly one line:\nVERDICT: APPROVE\nor\nVERDICT: CHANGES_REQUESTED\nfollowed by a numbered list of required changes if any." }, { "id": "approve", "name": "Human approval", "type": "approval", "dependsOn": [ "review" ], "approvalMessage": "Codex approved **{{issue.key}}** on `{{run.branch}}`.\n\nApprove to push and open a PR, or reject with what to change and it goes back to the implementer.", "loopBackTo": "implement", "maxLoops": 3 }, { "id": "pr", "name": "Open pull request", "type": "shell", "dependsOn": [ "approve" ], "timeoutMinutes": 5, "command": "git push -u origin HEAD && gh pr create --base {{run.baseBranch}} --head {{run.branch}} --title \"$FACTORY_ISSUE_KEY: $FACTORY_ISSUE_TITLE\" --body \"Automated by Jimothy run #$FACTORY_RUN_NUMBER. $FACTORY_ISSUE_URL\"" } ]}2. Weekly dependency updates
Section titled “2. Weekly dependency updates”A maintenance pipeline meant for a Schedule trigger (for example 0 6 * * 1, Mondays at 06:00). It exits early when nothing is outdated.
-
Anything outdated? (shell) installs dependencies with the project’s setup command and prints
npm outdated, ending withNEEDS_UPDATE=yesorNEEDS_UPDATE=no. -
Decide (shell) turns that into a plain
yesorno. -
Upgrade dependencies, Run tests and Open pull request all have Run only if
{{steps.decide.output | trim}}. When the answer isno(falsy), all three are skipped and the run still succeeds. -
Run tests loops back to Upgrade dependencies up to twice; the upgrade prompt includes the failing test output on later passes.
-
The PR body is the agent’s summary, passed as
--body {{steps.upgrade.output | truncate:3000}}. It’s quoted automatically, so any characters in the summary are safe.
Notes:
- Schedule triggers create issues with keys like
SCHED-202609280600, so the branch template useschore/deps-{{run.number}}instead of the issue key. concurrency: 1prevents overlapping maintenance runs.setupCommandandtestCommandare pipeline variables here. If the pipeline is in a project with Setup and Test commands, delete them from the pipeline to use the project’s.
{ "jimothyPipeline": 1, "name": "Weekly dependency updates", "description": "Checks for outdated npm packages; if any, an agent upgrades them, tests loop back on failure, and a PR opens.", "icon": "workflow", "color": "#34d399", "repo": { "mode": "worktree", "localPath": "", "baseBranch": "main", "branchTemplate": "chore/deps-{{run.number}}" }, "concurrency": 1, "variables": { "testCommand": "npm test", "setupCommand": "npm ci" }, "steps": [ { "id": "check", "name": "Anything outdated?", "type": "shell", "dependsOn": [], "timeoutMinutes": 10, "command": "{{vars.setupCommand | raw}} >/dev/null && if [ -n \"$(npm outdated --parseable)\" ]; then npm outdated || true; echo NEEDS_UPDATE=yes; else echo NEEDS_UPDATE=no; fi" }, { "id": "decide", "name": "Decide", "type": "shell", "dependsOn": [ "check" ], "command": "echo {{steps.check.output}} | grep -q 'NEEDS_UPDATE=yes' && echo yes || echo no" }, { "id": "upgrade", "name": "Upgrade dependencies", "type": "agent", "harnessId": "claude-code", "model": "sonnet", "dependsOn": [ "decide" ], "runIf": "{{steps.decide.output | trim}}", "timeoutMinutes": 45, "prompt": "You are the maintenance agent on Jimothy. You are on branch {{run.branch}}.\n\nThese npm packages are outdated:\n{{steps.check.output | truncate:6000}}\n{{#if steps.test.output}}\nThe test suite failed after your previous upgrade:\n{{steps.test.output | truncate:4000}}\nFix the failures, or roll back the package that causes them.\n{{/if}}\nUpgrade patch and minor versions. Upgrade a major version only if the changelog shows no breaking change that affects this code. Run `{{vars.testCommand}}`. Commit with a message listing every upgraded package, and end with a short summary table (package, from, to)." }, { "id": "test", "name": "Run tests", "type": "shell", "dependsOn": [ "upgrade" ], "runIf": "{{steps.decide.output | trim}}", "command": "{{vars.testCommand | raw}}", "timeoutMinutes": 20, "loopBackTo": "upgrade", "maxLoops": 2 }, { "id": "pr", "name": "Open pull request", "type": "shell", "dependsOn": [ "test" ], "runIf": "{{steps.decide.output | trim}}", "timeoutMinutes": 5, "command": "git push -u origin HEAD && gh pr create --base {{run.baseBranch}} --head {{run.branch}} --title \"chore: weekly dependency updates\" --body {{steps.upgrade.output | truncate:3000}}" } ]}3. Spec writer for vague issues
Section titled “3. Spec writer for vague issues”A non-code pipeline that runs entirely on the Claude API harness in an Empty folder workspace. Point a GitHub trigger at it (for example on the label needs-spec):
- Needs a spec? (API,
claude-haiku-4-5) answers exactlyyesorno. The pass pattern^\s*(yes|no)\s*$fails the step if the model says anything else. - The remaining steps use Run only if
{{steps.classify.output | trim | lower}}.nois falsy, so for actionable issues everything else is skipped. - Draft spec (API,
claude-opus-5, with a system prompt) writes the spec from the issue text alone. API harnesses have no tools or file access, which is exactly right here. - Review spec shows the whole draft in the approval message (rendered as Markdown).
- Post comment writes the spec to a file and posts it with
gh issue comment.printf '%s\n' {{steps.spec.output}}is safe even though the spec contains quotes and newlines, because the value is shell-quoted.
{ "jimothyPipeline": 1, "name": "Spec writer for vague issues", "description": "An API model checks whether an issue is actionable, drafts a spec with acceptance criteria if not, and posts it to the GitHub issue after you approve.", "icon": "sparkles", "color": "#a78bfa", "repo": { "mode": "scratch" }, "concurrency": 3, "variables": {}, "steps": [ { "id": "classify", "name": "Needs a spec?", "type": "agent", "harnessId": "anthropic-api", "model": "claude-haiku-4-5", "dependsOn": [], "timeoutMinutes": 5, "passPattern": "^\\s*(yes|no)\\s*$", "prompt": "Issue {{issue.key}}: {{issue.title}}\n\n{{issue.description | default:\"(no description)\"}}\n\nDoes this issue need a written spec before an engineer can implement it (unclear scope, missing acceptance criteria, open product questions)? Answer with exactly one word on a single line: yes or no." }, { "id": "spec", "name": "Draft spec", "type": "agent", "harnessId": "anthropic-api", "model": "claude-opus-5", "dependsOn": [ "classify" ], "runIf": "{{steps.classify.output | trim | lower}}", "timeoutMinutes": 10, "systemPrompt": "You are a senior product engineer. You write crisp, testable specs in GitHub-flavored Markdown.", "prompt": "Write a spec for issue {{issue.key}}: {{issue.title}}\n{{#if issue.url}}({{issue.url}}){{/if}}\n\n{{issue.description | default:\"(no description)\"}}\n\nInclude: Problem, Proposed behavior, Acceptance criteria (a checklist), Out of scope, Open questions. Don't invent product decisions; list them as open questions instead." }, { "id": "approve", "name": "Review spec", "type": "approval", "dependsOn": [ "spec" ], "runIf": "{{steps.classify.output | trim | lower}}", "approvalMessage": "Post this spec as a comment on {{issue.key}}?\n\n---\n\n{{steps.spec.output}}" }, { "id": "post", "name": "Post comment", "type": "shell", "dependsOn": [ "approve" ], "runIf": "{{steps.classify.output | trim | lower}}", "timeoutMinutes": 2, "command": "printf '%s\\n' {{steps.spec.output}} > spec.md && gh issue comment \"$FACTORY_ISSUE_URL\" --body-file spec.md" } ]}Beyond code
Section titled “Beyond code”The next three pipelines don’t touch a codebase. They start from a webhook or a schedule, write with the Claude API, stop at an approval gate, and hand the result to another tool through an incoming webhook: a Slack incoming webhook, or a catch hook in Zapier, Make or n8n that forwards it to your helpdesk or CRM.
The hand-off steps build their JSON with printf and the json filter:
printf '{"text":%s}' {{steps.draft.output | json}} | curl -fsS -X POST -H 'Content-Type: application/json' --data @- {{vars.slackWebhookUrl}}json turns the draft into a JSON string, and the shell step quotes it on top of that, so quotes, newlines, $ and % in the text arrive intact. curl -f makes the step fail if the webhook answers with an error. Rejecting at the approval gate with a comment sends the draft back to be rewritten; the prompts read your feedback from {{steps.approve.output}}.
4. Support reply drafts
Section titled “4. Support reply drafts”A support tool (or a Zapier/Make automation on your helpdesk) posts each new ticket to a Webhook trigger with title (the subject), description (the message) and key (the ticket number).
- Triage (API,
claude-haiku-4-5) answers with one line likebilling / urgent. The pass pattern fails the step on anything else. - Draft reply (API,
claude-sonnet-5) writes the reply. Its system prompt includes{{vars.policies}}, so it can’t promise refunds or dates your policies don’t allow. Put your real policies in that variable. - Review reply shows the triage and the draft. Reject with a comment to get a rewrite, up to three times.
- Send reply posts
ticket,triageandreplytoreplyWebhookUrl, for example a Zapier catch hook that adds the reply to the ticket.
{ "jimothyPipeline": 1, "name": "Support reply drafts", "description": "Triages an incoming ticket, drafts a reply that follows your policies, and sends it on after you approve.", "icon": "sparkles", "color": "#f59e0b", "repo": { "mode": "scratch" }, "concurrency": 3, "variables": { "policies": "Refunds within 30 days of purchase, no questions asked. After 30 days, offer account credit instead. Never share another customer's data. Bugs: apologise, say the team is looking into it, and never promise a date.", "signature": "The Acme support team", "replyWebhookUrl": "" }, "steps": [ { "id": "triage", "name": "Triage", "type": "agent", "harnessId": "anthropic-api", "model": "claude-haiku-4-5", "dependsOn": [], "timeoutMinutes": 5, "passPattern": "^\\s*(billing|bug|how-to|account|other)\\s*/\\s*(low|normal|urgent)\\s*$", "prompt": "Support ticket {{issue.key}}\n\nSubject: {{issue.title}}\n\n{{issue.description | default:\"(no message)\"}}\n\nClassify this ticket. Answer with exactly one line in the form <category> / <urgency>, where category is one of billing, bug, how-to, account, other and urgency is one of low, normal, urgent." }, { "id": "draft", "name": "Draft reply", "type": "agent", "harnessId": "anthropic-api", "model": "claude-sonnet-5", "dependsOn": [ "triage" ], "timeoutMinutes": 10, "systemPrompt": "You are a friendly, precise customer support specialist. Follow these policies exactly and never promise anything they don't allow:\n\n{{vars.policies}}\n\nIf the policies don't cover the question, say you're checking with the team.", "prompt": "Ticket {{issue.key}} ({{steps.triage.output | trim}})\n\nSubject: {{issue.title}}\n\n{{issue.description | default:\"(no message)\"}}\n\nWrite the reply to send to the customer. Plain text, no subject line, under 180 words, signed \"{{vars.signature}}\".{{#if steps.approve.output}}\n\nYour previous draft was rejected with this feedback. Rewrite it:\n{{steps.approve.output}}{{/if}}" }, { "id": "approve", "name": "Review reply", "type": "approval", "dependsOn": [ "draft" ], "loopBackTo": "draft", "maxLoops": 3, "approvalMessage": "Send this reply to {{issue.key}}? Triage: {{steps.triage.output | trim}}\n\n---\n\n{{steps.draft.output}}" }, { "id": "send", "name": "Send reply", "type": "shell", "dependsOn": [ "approve" ], "timeoutMinutes": 2, "command": "printf '{\"ticket\":%s,\"triage\":%s,\"reply\":%s}' {{issue.key | json}} {{steps.triage.output | trim | json}} {{steps.draft.output | json}} | curl -fsS -X POST -H 'Content-Type: application/json' --data @- {{vars.replyWebhookUrl}}" } ]}5. Weekly “What’s new” post
Section titled “5. Weekly “What’s new” post”A Schedule trigger (for example 0 15 * * 5, Fridays at 15:00) turns the week’s commits into a customer-facing update. It uses an Existing folder workspace pointed at a clone of your repository and only reads from it.
- This week’s commits fetches and lists the last seven days of commit subjects on
vars.branch. - The remaining steps have Run only if conditions: a quiet week with no commits skips everything.
- Write the post (API,
claude-sonnet-5) leaves out refactors, CI, tests and dependency bumps, and answersNOTHING_TO_ANNOUNCEif nothing is left. - Anything to announce? turns that into
yesorno, andnoskips the approval and the post. The run still succeeds. - Post to Slack sends the approved post to a Slack incoming webhook in
slackWebhookUrl.
{ "jimothyPipeline": 1, "name": "Weekly what's new", "description": "Reads the week's commits, writes a customer-facing \"What's new\" post, and posts it to Slack after you approve.", "icon": "sparkles", "color": "#ec4899", "repo": { "mode": "inplace", "localPath": "" }, "concurrency": 1, "variables": { "branch": "origin/main", "audience": "customers of Acme, a reporting tool for finance teams", "slackWebhookUrl": "" }, "steps": [ { "id": "log", "name": "This week's commits", "type": "shell", "dependsOn": [], "timeoutMinutes": 5, "command": "git fetch -q origin && git log {{vars.branch}} --since='7 days ago' --no-merges --pretty='- %s' | head -300" }, { "id": "draft", "name": "Write the post", "type": "agent", "harnessId": "anthropic-api", "model": "claude-sonnet-5", "dependsOn": [ "log" ], "timeoutMinutes": 10, "runIf": "{{steps.log.output | trim}}", "systemPrompt": "You write product update posts for {{vars.audience}}. Plain and specific. No hype words, no exclamation marks.", "prompt": "These commits landed this week:\n\n{{steps.log.output | truncate:12000}}\n\nWrite a short \"What's new this week\" post for customers. Skip internal work (refactors, CI, tests, dependency bumps). Group related changes and describe each one by what customers can now do. Use Slack formatting: *bold* headings and - bullets.\n\nIf nothing is customer-facing, reply with exactly NOTHING_TO_ANNOUNCE and nothing else.{{#if steps.approve.output}}\n\nYour previous draft was rejected with this feedback. Rewrite it:\n{{steps.approve.output}}{{/if}}" }, { "id": "decide", "name": "Anything to announce?", "type": "shell", "dependsOn": [ "draft" ], "runIf": "{{steps.log.output | trim}}", "command": "printf '%s' {{steps.draft.output}} | grep -q NOTHING_TO_ANNOUNCE && echo no || echo yes" }, { "id": "approve", "name": "Review post", "type": "approval", "dependsOn": [ "decide" ], "loopBackTo": "draft", "maxLoops": 3, "runIf": "{{steps.decide.output | trim}}", "approvalMessage": "Post this to Slack?\n\n---\n\n{{steps.draft.output}}" }, { "id": "post", "name": "Post to Slack", "type": "shell", "dependsOn": [ "approve" ], "timeoutMinutes": 2, "runIf": "{{steps.decide.output | trim}}", "command": "printf '{\"text\":%s}' {{steps.draft.output | json}} | curl -fsS -X POST -H 'Content-Type: application/json' --data @- {{vars.slackWebhookUrl}}" } ]}6. Inbound lead research
Section titled “6. Inbound lead research”Point your website’s contact form (directly, or through Zapier or Make) at a Webhook trigger. Send the company name as title, the message as description, and name, email and website as extra fields. They reach the prompts as {{issue.raw.name}} and so on.
- Research the company is a Claude Code step in an Empty folder workspace, so it can search and read the web. It writes a cited brief with a fit rating against
vars.idealCustomerand labels guesses as guesses. - Draft first reply (API,
claude-sonnet-5) answers the lead’s question and mentions one specific thing from the brief. - Review lead shows the brief and the reply together. Reject with a comment to get a new reply.
- Send to CRM posts
lead,email,briefandreplytoleadWebhookUrl, for example a catch hook that creates the contact in your CRM and drafts the email.
{ "jimothyPipeline": 1, "name": "Inbound lead research", "description": "Researches a company from a contact-form submission, rates the fit, drafts a first reply, and sends both on after you approve.", "icon": "sparkles", "color": "#38bdf8", "repo": { "mode": "scratch" }, "concurrency": 2, "variables": { "product": "Acme, a reporting tool for finance teams", "idealCustomer": "Finance teams of 50 to 500 people that run on NetSuite or QuickBooks", "leadWebhookUrl": "" }, "steps": [ { "id": "research", "name": "Research the company", "type": "agent", "harnessId": "claude-code", "model": "sonnet", "dependsOn": [], "timeoutMinutes": 15, "prompt": "A lead filled in our contact form.\n\nCompany: {{issue.title}}\nWebsite: {{issue.raw.website | default:\"(none given)\"}}\nContact: {{issue.raw.name | default:\"(no name)\"}}, {{issue.raw.email | default:\"(no email)\"}}\nMessage:\n{{issue.description | default:\"(empty)\"}}\n\nWe sell {{vars.product}}. Our ideal customer: {{vars.idealCustomer}}.\n\nResearch the company on the web: what they do, rough size, industry, recent news, and any sign of the tools our ideal customer uses. Then write a brief in Markdown with these sections: Company, What they do, Size and signals, Fit (strong, possible or weak, with a one-line reason), Talking points. Cite the URLs you used. Only state what you found and label guesses as guesses. Your final answer is the brief and nothing else." }, { "id": "reply", "name": "Draft first reply", "type": "agent", "harnessId": "anthropic-api", "model": "claude-sonnet-5", "dependsOn": [ "research" ], "timeoutMinutes": 5, "prompt": "Research brief:\n{{steps.research.output | truncate:8000}}\n\nThe lead wrote:\n{{issue.description | default:\"(empty)\"}}\n\nDraft a first reply to {{issue.raw.name | default:\"them\"}}. Answer what they asked, mention one specific thing about their company from the brief, and offer a 20-minute call. Plain text, under 120 words, no subject line.{{#if steps.approve.output}}\n\nYour previous draft was rejected with this feedback. Rewrite it:\n{{steps.approve.output}}{{/if}}" }, { "id": "approve", "name": "Review lead", "type": "approval", "dependsOn": [ "reply" ], "loopBackTo": "reply", "maxLoops": 3, "approvalMessage": "New lead: {{issue.title}}\n\n{{steps.research.output}}\n\n---\n\n**Draft reply**\n\n{{steps.reply.output}}" }, { "id": "send", "name": "Send to CRM", "type": "shell", "dependsOn": [ "approve" ], "timeoutMinutes": 2, "command": "printf '{\"lead\":%s,\"email\":%s,\"brief\":%s,\"reply\":%s}' {{issue.title | json}} {{issue.raw.email | default:\"\" | json}} {{steps.research.output | json}} {{steps.reply.output | json}} | curl -fsS -X POST -H 'Content-Type: application/json' --data @- {{vars.leadWebhookUrl}}" } ]}Adapting examples
Section titled “Adapting examples”- Different harnesses. Replace
harnessIdandmodelwith harnesses from your Harnesses page. The pipeline editor’s harness picker shows which are installed (●) and which aren’t (○). - Different tools. Keep commands in variables (
testCommand,lintCommand) or project Commands, and reference them with| rawin shell steps. - Dry-run first. Switch agent steps to the Simulator harness to check the flow (dependencies, loops, conditions, approvals) without spending tokens. Simulator reviewers look for
VERDICTin the prompt and request changes on the first pass with therealisticmodel.