/** * The Workflow tool description, ported verbatim from Claude Code's built-in * Workflow tool (extracted from the CC binary, with the minified * template-interpolation artifacts resolved: `'worktree'` enum, the `▸` nesting * glyph, the worktree-isolation note, etc.). This is the LLM-facing contract — * keeping it identical is the whole point of a "faithful" port, so the model * authors scripts against the same surface it knows from Claude Code. */ export const WORKFLOW_DESCRIPTION = `Execute a workflow script that orchestrates multiple subagents deterministically. Workflows run in the background — this tool returns immediately with a task ID, and a arrives when the workflow completes. Use /workflows to watch live progress. A workflow structures work across many agents — to be comprehensive (decompose and cover in parallel), to be confident (independent perspectives and adversarial checks before committing), or to take on scale one context can't hold (migrations, audits, broad sweeps). The script is where you encode that structure: what fans out, what verifies, what synthesizes. ONLY call this tool when the user has explicitly opted into multi-agent orchestration. Workflows can spawn dozens of agents and consume a large amount of tokens; the user must request that scale, not have it inferred. Explicit opt-in means the user directly asked you to run a workflow or use multi-agent orchestration in their own words ("use a workflow", "run a workflow", "fan out agents", "orchestrate this with subagents"), invoked a skill/command that tells you to call Workflow, or asked you to run a specific named/saved workflow. The ask must be in the user's words — a task that would merely benefit from a workflow does not count. For any other task — even one that would clearly benefit from parallelism — do NOT call this tool. Use a single subagent, or briefly describe what a multi-agent workflow could do and roughly what it would cost, and ask the user whether to run it. When you do call it, the right move is often **hybrid**: scout inline first (list the files, find the channels, scope the diff) to discover the work-list, then call Workflow to pipeline over it. You don't need to know the shape before the *task* — only before the *orchestration step*. Pass the script inline via \`script\`. Every invocation automatically persists its script to a file and returns the path in the tool result. To iterate on a workflow, edit that file and re-invoke Workflow with \`{scriptPath: ""}\` instead of resending the full script. Every script must begin with \`export const meta = {...}\`: export const meta = { name: 'find-flaky-tests', description: 'Find flaky tests and propose fixes', phases: [ { title: 'Scan', detail: 'grep test logs for retries' }, { title: 'Fix', detail: 'one agent per flaky test' }, ], } // script body starts here — use agent()/parallel()/pipeline()/phase()/log() phase('Scan') const flaky = await agent('grep CI logs for retry markers', {schema: FLAKY_SCHEMA}) The \`meta\` object must be a PURE LITERAL — no variables, function calls, spreads, or template interpolation. Required fields: \`name\`, \`description\`. Optional: \`whenToUse\`, \`phases\`. Use the SAME phase titles in meta.phases as in phase() calls — titles are matched exactly. Add \`model\` to a phase entry when that phase uses a specific model override, and \`effort\` ("off"|"minimal"|"low"|"medium"|"high"|"xhigh") for a per-phase reasoning level. Top-level \`meta.model\` and \`meta.effort\` apply as defaults. Script body hooks: - agent(prompt: string, opts?: {label?: string, phase?: string, schema?: object, model?: string, effort?: 'off'|'minimal'|'low'|'medium'|'high'|'xhigh', isolation?: 'worktree', agentType?: string}): Promise — spawn a subagent. Without schema, returns its final text as a string. With schema (a JSON Schema), the subagent is forced to call a structured_output tool and agent() returns the validated object — no parsing needed. Returns null if the subagent dies on a terminal error after retries (filter with .filter(Boolean)). opts.label overrides the display label. opts.phase explicitly assigns this agent to a progress group (use it inside pipeline()/parallel() stages to avoid races on the global phase() state). opts.model overrides the model for this agent call — default to omitting it (the agent inherits the session model). opts.effort sets the reasoning level for this agent call; precedence is opts.effort > meta.phases[].effort > meta.effort > SDK default. opts.isolation: 'worktree' runs the agent in a fresh git worktree — EXPENSIVE, use ONLY when agents mutate files in parallel and would conflict; the worktree is auto-removed if unchanged. opts.agentType uses a named subagent definition (binds tools/model/prompt); composes with schema. - pipeline(items, stage1, stage2, ...): Promise — run each item through all stages independently, NO barrier between stages. Item A can be in stage 3 while item B is still in stage 1. This is the DEFAULT for multi-stage work. Every stage callback receives (prevResult, originalItem, index). A stage that throws drops that item to null and skips its remaining stages. - parallel(thunks: Array<() => Promise>): Promise — run tasks concurrently. This is a BARRIER: awaits all thunks before returning. A thunk that throws resolves to null in the result array — the call never rejects, so .filter(Boolean) before using results. Use ONLY when you genuinely need all results together. - log(message: string): void — emit a progress message to the user. - phase(title: string): void — start a new phase; subsequent agent() calls group under this title. - args: any — the value passed as Workflow's \`args\` input, verbatim (undefined if not provided). Pass arrays/objects as actual JSON values, NOT a JSON-encoded string. - budget: {total: number|null, spent(): number, remaining(): number} — token budget. budget.total is null when no target was set. budget.spent() returns tokens spent across this run; budget.remaining() returns max(0, total - spent()) or Infinity if no target. Once spent reaches total, further agent() calls throw. Use for dynamic loops: while (budget.total && budget.remaining() > 50_000) { ... }. - workflow(nameOrRef: string | {scriptPath: string}, args?: any): Promise — run another workflow inline as a sub-step and return its result. The child shares this run's concurrency cap, agent counter, abort signal, and token budget. Nesting is one level only: workflow() inside a child throws. Subagents are told their final text IS the return value, so they return raw data. For structured output, use the schema option — validation happens at the tool-call layer so the model retries on mismatch. Scripts are plain JavaScript, NOT TypeScript — type annotations, interfaces, and generics fail to parse. The body runs in an async context — use await directly. Standard JS built-ins are available EXCEPT Date.now()/Math.random()/argless new Date(), which throw (they break resume); pass timestamps via args and vary randomness by index. No filesystem or Node.js API access. DEFAULT TO pipeline(). Only reach for a barrier (parallel between stages) when stage N genuinely needs ALL of stage N-1's results together (dedup/merge across the full set, early-exit on zero, or a prompt that references "the other findings"). "I need to flatten/map/filter first" is NOT a reason — do it inside a pipeline stage. When in doubt: pipeline. Concurrent agent() calls are capped at min(16, cpu cores - 2) per workflow — excess calls queue. Total agent count across a run is capped at 1000. A single parallel()/pipeline() call accepts at most 4096 items. The canonical multi-stage pattern — pipeline by default, each dimension verifies as soon as its review completes: const results = await pipeline( DIMENSIONS, d => agent(d.prompt, {label: \`review:\${d.key}\`, phase: 'Review', schema: FINDINGS_SCHEMA}), review => parallel(review.findings.map(f => () => agent(\`Adversarially verify: \${f.title}\`, {label: \`verify:\${f.file}\`, phase: 'Verify', schema: VERDICT_SCHEMA}) .then(v => ({...f, verdict: v})))) ) return { confirmed: results.flat().filter(Boolean).filter(f => f.verdict?.isReal) } Quality patterns to compose: adversarial verify (N skeptics per finding, kill if majority refute), perspective-diverse verify (each verifier a distinct lens), judge panel (N attempts scored by parallel judges), loop-until-dry (keep finding until K empty rounds), multi-modal sweep (each agent searches a different way), completeness critic (final "what's missing" agent). No silent caps: log() what was dropped. Scale to what the user asked for — a few finders for "find any bugs", a larger pool with 3–5 vote adversarial verification for "thoroughly audit this". ## Resume The tool result includes a runId. To resume after a kill or a script edit, relaunch with Workflow({scriptPath, resumeFromRunId}) — the longest unchanged prefix of agent() calls returns cached results instantly; the first edited/new call and everything after it run live. Same script + same args → 100% cache hit. ## Truncated results — read the full output before responding When a completed-workflow message arrives with the marker "…(truncated — full result via /workflows)" or any sign that the inline payload was cut off, DO NOT respond to the user from the truncated text alone. The truncated tail almost always contains the actual answer (review verdicts, validation output, final synthesis). Your next action MUST be to read the full result from the persisted journal before saying anything substantive: jq -r '.result.' ~/.pi/better-workflows/runs/.json The runId is in the completion message. The journal lives at ~/.pi/better-workflows/runs/.json with shape { result: { ... }, journal: [...], status, finishedAt }. Use jq, Read, or Bash to fetch the keys you returned from the script. Only then synthesize back to the user. Treat truncation as a partial delivery, never as the final word.`;