From 549c6df525b08acddbb9dd5ff4573371600942f8 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:29:41 +0530 Subject: [PATCH 1/3] feat(workflows): ship the script API reference with the workflows skill The workflows skill only taught harnesses how to run workflows, so a model asked to write one had to guess the API. Add reference.md beside SKILL.md covering the sandbox, meta rules, primitives (agent, phase, parallel, pipeline, workflow, log, args), agent options, schemas, edits, resume, and limits, all checked against the runtime. SKILL.md points to it before authoring, and the resume example no longer passes --args, which resume rejects. Managed skill sync copied only SKILL.md into ~/.devspace/skills, so it now copies every file in the bundled skill folder. Co-Authored-By: Claude Opus 5.5 --- skills/workflows/SKILL.md | 16 +++-- skills/workflows/reference.md | 119 ++++++++++++++++++++++++++++++++++ src/skills.test.ts | 6 ++ src/skills.ts | 19 ++++-- 4 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 skills/workflows/reference.md diff --git a/skills/workflows/SKILL.md b/skills/workflows/SKILL.md index 9b91fe472..433bb647d 100644 --- a/skills/workflows/SKILL.md +++ b/skills/workflows/SKILL.md @@ -1,13 +1,21 @@ --- name: workflows -description: Run a persisted multi-agent DevSpace workflow from a script file or a named project workflow, then wait for or inspect its result. +description: Write, run, and inspect persisted multi-agent DevSpace workflows, JavaScript scripts that compose subagent calls with agent, phase, parallel, and pipeline. --- # DevSpace workflows Use a workflow when a task has a repeatable multi-agent plan. Keep one-off delegation on the `subagents` commands. -Named workflows live in the current project's `.devspace/workflows/` directory. Run a named workflow or a specific file: +Named workflows live in the current project's `.devspace/workflows/` directory. + +## Write a workflow + +Before writing or editing a workflow script, read `reference.md` next to this file. It defines the only syntax and primitives the sandbox accepts (`agent`, `phase`, `parallel`, `pipeline`, `workflow`, `log`, `args`), the `meta` rules, `agent` options, structured output schemas, and limits. Do not guess the API. + +## Run a workflow + +Run a named workflow or a specific file: ```bash devspace workflow run --name [--args ''] @@ -40,10 +48,10 @@ Cancel only when the parent task no longer needs the run: devspace workflow cancel ``` -Resume an interrupted or failed run from its durable call history: +Resume an interrupted or failed run from its durable call history. Resume reuses the original arguments and write mode: ```bash -devspace workflow run --resume [--args ''] +devspace workflow run --resume ``` Use `--json` only for scripts that need structured output. diff --git a/skills/workflows/reference.md b/skills/workflows/reference.md new file mode 100644 index 000000000..ccfeb49a5 --- /dev/null +++ b/skills/workflows/reference.md @@ -0,0 +1,119 @@ +# Writing DevSpace workflows + +A workflow is one JavaScript file. It runs in an isolated QuickJS sandbox with no filesystem, network, `import`, +`require`, or timers: the only capabilities are the primitives below. Save named workflows as +`.devspace/workflows/.js` in the project, or run any file with `devspace workflow run --file `. + +## Shape + +```js +export const meta = { + name: "review-auth", + description: "Review auth changes from two perspectives", + concurrency: 2, +}; + +const reviews = await phase("Review", (agent) => parallel( + [ + { target: "codex", focus: "correctness" }, + { target: "claude", focus: "security" }, + ], + ({ target, focus }) => agent(`Review the auth diff for ${focus}. Cite files.`, { target, label: focus }), +)); + +const summary = await phase("Synthesize", (agent) => agent( + `Merge these reviews into one prioritized list: ${JSON.stringify(reviews)}`, + { target: "codex", label: "summary" }, +)); + +return { reviews, summary }; +``` + +- The body runs inside an async function: use top-level `await`, and `return` the run's result. The result must be + JSON-compatible. +- `meta` is optional. When present it must be the first statement, and it is parsed as data, never executed: + - `name`: string, at most 128 characters (default `"workflow"`). + - `description`: string, at most 2048 characters. + - `concurrency`: integer 1–16 (default 4). Caps how many agent calls this run executes at once. + - Values must be literal single-line strings or numbers. No other fields, expressions, or variables. + +## Primitives + +| Primitive | Behavior | +| --- | --- | +| `agent(prompt, options)` | Runs one subagent turn and resolves to its final response: a string, or the parsed JSON value when `options.schema` is set. Rejects if the agent fails or stops. | +| `phase(name, (agent) => ...)` | Groups calls under a named phase in the event log. Use the `agent` passed to the callback so each call is tagged with the phase. Resolves to the callback's value. | +| `parallel(items, worker, concurrency?)` | Calls `worker(item, index)` for every item, at most `concurrency` at a time (default `meta.concurrency`, maximum 16). Never rejects: resolves to one `{ status: "completed", value }` or `{ status: "failed", error: { code, message, retryable } }` per item, in input order. | +| `pipeline(items, ...stages)` | Passes each item through the stages in order, calling `stage(value, item, index)` where `value` is the previous stage's result. Items run concurrently like `parallel` and resolve to the same per-item outcomes. | +| `workflow(name, args?)` | Runs another named workflow from `.devspace/workflows/` and resolves to its result. | +| `log(...values)` | Records JSON-compatible progress in the run's event log. | +| `args` | The frozen JSON arguments passed with `--args` or `--args-file`; `{}` when none were passed. | + +Because `parallel` and `pipeline` report failures per item instead of rejecting, check `status` before using +`value`: + +```js +const results = await parallel(files, (file) => agent(`Summarize ${file}.`, { target: "codex" })); +const summaries = results.filter((r) => r.status === "completed").map((r) => r.value); +const failures = results.filter((r) => r.status === "failed").map((r) => r.error.message); +``` + +## `agent` options + +| Option | Meaning | +| --- | --- | +| `target` | **Required.** A provider or profile name from `devspace agents targets`. | +| `model`, `effort` | Override the target's model or reasoning effort. | +| `label` | Short name shown in `devspace workflow calls`. | +| `schema` | JSON Schema for a structured response (see below). | +| `writeMode` | `"read_only"` or `"allowed"`. Defaults to the run's authority and can never exceed it. | +| `isolation` | `"worktree"`: run this call in its own Git worktree created from committed `HEAD`. | +| `workspace` | A logical name (letters, digits, `_`, `-`; at most 64) that calls in this run share. Never a path. | + +Any other option is rejected. Each prompt should be self-contained: a subagent sees only its prompt and the +project, not the rest of the workflow. + +### Structured responses + +Pass `schema` when a later step consumes the result. DevSpace validates the response, asks the same agent once to +fix invalid JSON (read-only, and it counts toward the turn budget), then resolves to the parsed value. + +```js +const { bugs } = await agent("List the bugs in the diff as JSON.", { + target: "codex", + schema: { + type: "object", + properties: { bugs: { type: "array", items: { type: "string" } } }, + required: ["bugs"], + }, +}); +``` + +Schemas are strict draft-07 with inline structural constraints only: at most 16 KiB, 512 nodes, and 16 levels deep. +`$ref`, `$dynamicRef`, `$recursiveRef`, `$async`, `pattern`, `patternProperties`, and `format` are rejected. + +### Edits + +Runs are read-only unless started with `devspace workflow run ... --write-mode allowed`. In a writing run, give +editing calls `writeMode: "allowed"`. Add `isolation: "worktree"` when parallel calls might edit the same files, so +each works in its own worktree; isolated worktrees do not see uncommitted changes. Worktrees are kept for +inspection afterwards. + +## Resume + +`devspace workflow run --resume ` reruns the script with the original arguments. Completed read-only calls are +reused as long as the script makes the same calls in the same order with the same prompts and options; from the first +call that differs, everything runs fresh. Runs that made editing calls cannot be resumed. Keep prompts deterministic +(no timestamps or random values) so resume can reuse work. + +## Limits + +| Boundary | Limit | +| --- | --- | +| Agent turns per run | 128, including schema corrections | +| Nested `workflow()` calls | 8 per run, one level deep | +| Script wall clock | 15 minutes | +| Script source / arguments | 64 KiB / 128 KiB | +| Agent response / run result | 256 KiB | +| `log` and `phase` events | 64 KiB per run | +| Sandbox heap / stack | 32 MiB / 512 KiB | diff --git a/src/skills.test.ts b/src/skills.test.ts index d67f4acd7..616b83a46 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -100,6 +100,12 @@ test("the managed subagents skill replaces a stale user copy and workflows need flagged.find((skill) => skill.name === "workflows")?.filePath, join(devspaceSkills, "workflows", "SKILL.md"), ); + // The authoring reference the skill points to is synced beside it, and is not a skill of its own. + assert.match(await readFile(join(devspaceSkills, "workflows", "reference.md"), "utf8"), /# Writing DevSpace workflows/); + assert.deepEqual( + flagged.filter((skill) => skill.filePath.startsWith(join(devspaceSkills, "workflows"))).map((skill) => skill.name), + ["workflows"], + ); } finally { delete process.env.DEVSPACE_EXPERIMENTAL_WORKFLOWS; } diff --git a/src/skills.ts b/src/skills.ts index 2e3f496c4..59201d724 100644 --- a/src/skills.ts +++ b/src/skills.ts @@ -2,6 +2,7 @@ import { existsSync, lstatSync, mkdirSync, + readdirSync, readFileSync, renameSync, rmSync, @@ -44,17 +45,23 @@ function bundledSkillsDir(): string { return fileURLToPath(new URL("../skills", import.meta.url)); } -function syncManagedSkill(config: ServerConfig, name: ManagedSkillName): string { - const skillPath = join(name, "SKILL.md"); - const sourcePath = join(bundledSkillsDir(), skillPath); - const targetPath = join(config.devspaceSkillsDir, skillPath); +// Sync every bundled file, not just SKILL.md, so references a skill points to (such as reference.md) ship with it. +function syncManagedSkill(config: ServerConfig, name: ManagedSkillName): void { + const sourceDir = join(bundledSkillsDir(), name); + + for (const file of readdirSync(sourceDir)) { + syncManagedFile(join(sourceDir, file), join(config.devspaceSkillsDir, name, file), name); + } +} + +function syncManagedFile(sourcePath: string, targetPath: string, name: ManagedSkillName): void { const source = readFileSync(sourcePath, "utf8"); if (existsSync(targetPath)) { const stat = lstatSync(targetPath); if (stat.isFile() && source === readFileSync(targetPath, "utf8")) { - return targetPath; + return; } if (stat.isDirectory()) { @@ -72,8 +79,6 @@ function syncManagedSkill(config: ServerConfig, name: ManagedSkillName): string } finally { rmSync(tempPath, { force: true }); } - - return targetPath; } export function effectiveSkillPaths(config: ServerConfig, cwd: string): string[] { From 51f55761fa887ee8f05e2a75f90ce08c29dcbac3 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:42:18 +0530 Subject: [PATCH 2/3] feat(workflows): ship engineering workflow patterns with the skill Add patterns.md: complete, runnable scripts for plan-with-critic, implement-review-fix loops, a QA gate, explore-then-synthesize, multi-lens review with verified findings, bake-offs, mapping a change over many units, debugging with competing hypotheses, and composing named workflows, plus design rules (schemas for decisions, bounded loops, read-only readers in writing runs, workspace choice). Clarify in reference.md that a named workspace is created by its first call with isolation: "worktree" and reused by later calls. Co-Authored-By: Claude Opus 5.5 --- skills/workflows/SKILL.md | 2 + skills/workflows/patterns.md | 309 ++++++++++++++++++++++++++++++++++ skills/workflows/reference.md | 14 +- 3 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 skills/workflows/patterns.md diff --git a/skills/workflows/SKILL.md b/skills/workflows/SKILL.md index 433bb647d..2af06061e 100644 --- a/skills/workflows/SKILL.md +++ b/skills/workflows/SKILL.md @@ -13,6 +13,8 @@ Named workflows live in the current project's `.devspace/workflows/` directory. Before writing or editing a workflow script, read `reference.md` next to this file. It defines the only syntax and primitives the sandbox accepts (`agent`, `phase`, `parallel`, `pipeline`, `workflow`, `log`, `args`), the `meta` rules, `agent` options, structured output schemas, and limits. Do not guess the API. +Then read `patterns.md` for proven engineering loops to adapt: plan with a critic, implement-review-fix until approved, a QA gate that loops until tests pass, explore-then-synthesize, multi-lens review with verified findings, bake-offs between agents, mapping one change over many units, debugging with competing hypotheses, and composing them. + ## Run a workflow Run a named workflow or a specific file: diff --git a/skills/workflows/patterns.md b/skills/workflows/patterns.md new file mode 100644 index 000000000..a8faafc8d --- /dev/null +++ b/skills/workflows/patterns.md @@ -0,0 +1,309 @@ +# DevSpace workflow patterns + +Read `reference.md` first for the syntax. These are proven shapes for engineering work: adapt them, combine them, and +keep the parts that fit the task. Each one is a complete script. + +## Designing a workflow + +- **Pick targets per role.** Run `devspace agents targets` and match each step to the provider or profile that is best + at it: a strong reasoner to plan and judge, a careful editor to implement, a cheap fast model to explore, and a + different vendor to review. Pass them in `args` so one script serves many setups. +- **Decide with schemas.** Any step whose result controls the script (approve or not, pass or fail, a list to fan + out over) should return JSON through `schema`, never prose that the script has to parse. +- **Bound every loop.** Loops end on a verdict *or* a maximum round count. Budget agent turns up front: a run has 128, + and each schema correction costs one. +- **Make prompts self-contained.** A subagent only sees its prompt and the project. Paste in the plan, the diff + summary, or the earlier findings it needs. +- **Keep readers read-only.** In a run started with `--write-mode allowed`, every call inherits write access unless + it passes `writeMode: "read_only"`. Give it to every planner, reviewer, tester, judge, and investigator. +- **Use the right workspace.** Read-only steps run in the checkout. Sequential edits share one named worktree + (`isolation: "worktree"` plus `workspace`). Parallel edits each get their own worktree. +- **Handle per-item failures.** `parallel` and `pipeline` never throw; check each `status`. +- **Keep it resumable.** Same inputs should produce the same calls in the same order: no timestamps or random values + in prompts. + +Start writing runs with `--write-mode allowed`; everything else stays read-only by default. + +## Plan with a critic + +Draft a plan, have a second model attack it, and revise until it holds up. + +```js +export const meta = { name: "plan", description: "Draft, critique, and revise an implementation plan" }; + +const { task, planner = "codex", critic = "claude", rounds = 3 } = args; +const PLAN = { + type: "object", + properties: { steps: { type: "array", items: { type: "string" } }, risks: { type: "array", items: { type: "string" } } }, + required: ["steps", "risks"], +}; +const CRITIQUE = { + type: "object", + properties: { approved: { type: "boolean" }, problems: { type: "array", items: { type: "string" } } }, + required: ["approved", "problems"], +}; + +let plan = await agent(`Write an implementation plan for: ${task}. Read the relevant code first.`, { target: planner, writeMode: "read_only", schema: PLAN, label: "draft" }); + +for (let round = 1; round <= rounds; round++) { + const critique = await agent( + `Critique this plan for "${task}". Check it against the code; approve only if it is complete and correct.\n${JSON.stringify(plan)}`, + { target: critic, writeMode: "read_only", schema: CRITIQUE, label: `critique-${round}` }, + ); + if (critique.approved) return { plan, rounds: round }; + plan = await agent( + `Revise the plan to fix these problems:\n${critique.problems.join("\n")}\nPlan:\n${JSON.stringify(plan)}`, + { target: planner, writeMode: "read_only", schema: PLAN, label: `revise-${round}` }, + ); +} + +return { plan, rounds, approved: false }; +``` + +## Implement, review, fix until approved + +The core engineering loop. Edits stay in one named worktree so each fix builds on the last. + +```js +export const meta = { name: "build", description: "Implement, then review and fix until the reviewer approves" }; + +const { task, implementer = "claude", reviewer = "codex", rounds = 3 } = args; +const REVIEW = { + type: "object", + properties: { approved: { type: "boolean" }, findings: { type: "array", items: { type: "string" } } }, + required: ["approved", "findings"], +}; + +await phase("Implement", (agent) => agent(`Implement: ${task}. Add or update tests.`, { + target: implementer, writeMode: "allowed", isolation: "worktree", workspace: "feature", +})); + +for (let round = 1; round <= rounds; round++) { + const review = await phase(`Review ${round}`, (agent) => agent( + `Review the uncommitted diff for: ${task}. Approve only if it is correct, tested, and complete. Cite file:line.`, + { target: reviewer, writeMode: "read_only", workspace: "feature", schema: REVIEW }, + )); + if (review.approved) return { approved: true, rounds: round }; + await phase(`Fix ${round}`, (agent) => agent( + `A reviewer found these problems in your change for "${task}". Fix all of them:\n${review.findings.join("\n")}`, + { target: implementer, writeMode: "allowed", workspace: "feature" }, + )); +} + +return { approved: false, rounds }; +``` + +## QA gate + +Implement, then have a separate agent run the tests and report; fix and retry until green. + +```js +export const meta = { name: "qa", description: "Implement and loop until the test suite passes" }; + +const { task, implementer = "claude", tester = "codex", attempts = 3 } = args; +const RESULT = { + type: "object", + properties: { passed: { type: "boolean" }, failures: { type: "array", items: { type: "string" } } }, + required: ["passed", "failures"], +}; + +await agent(`Implement: ${task}.`, { target: implementer, writeMode: "allowed", isolation: "worktree", workspace: "qa" }); + +for (let attempt = 1; attempt <= attempts; attempt++) { + const result = await agent( + "Run the project's tests and type checks. Report each failure with its error output. Do not change any files.", + { target: tester, writeMode: "read_only", workspace: "qa", schema: RESULT, label: `test-${attempt}` }, + ); + if (result.passed) return { passed: true, attempts: attempt }; + await agent(`Make these failures pass without weakening the tests:\n${result.failures.join("\n")}`, { + target: implementer, writeMode: "allowed", workspace: "qa", label: `fix-${attempt}`, + }); +} + +return { passed: false, attempts }; +``` + +## Explore, then synthesize + +Fan cheap explorers out over separate questions, then let one model combine what they found. + +```js +export const meta = { name: "explore", description: "Answer several questions in parallel, then synthesize", concurrency: 4 }; + +const { goal, questions, explorer = "opencode", synthesizer = "codex" } = args; + +const answers = await phase("Explore", (agent) => parallel(questions, (question) => agent( + `${question}\nAnswer from the code only. Cite file paths and symbols.`, + { target: explorer, writeMode: "read_only", label: question.slice(0, 40) }, +))); + +const found = answers.map((answer, index) => ({ + question: questions[index], + answer: answer.status === "completed" ? answer.value : `failed: ${answer.error.message}`, +})); + +return await phase("Synthesize", (agent) => agent( + `Goal: ${goal}\nCombine these findings into one answer and name what is still unknown:\n${JSON.stringify(found)}`, + { target: synthesizer, writeMode: "read_only" }, +)); +``` + +## Multi-lens review with verification + +Review from several angles at once, then have a different model confirm each finding before reporting it, which +filters out false positives. + +```js +export const meta = { name: "review", description: "Review from several angles and keep only verified findings", concurrency: 4 }; + +const { scope = "the uncommitted diff", reviewers = ["codex", "claude"], verifier = "codex" } = args; +const lenses = ["correctness and edge cases", "security", "performance", "missing tests"]; +const FINDINGS = { + type: "object", + properties: { + findings: { + type: "array", + items: { + type: "object", + properties: { location: { type: "string" }, issue: { type: "string" } }, + required: ["location", "issue"], + }, + }, + }, + required: ["findings"], +}; +const VERDICT = { + type: "object", + properties: { real: { type: "boolean" }, reason: { type: "string" } }, + required: ["real", "reason"], +}; + +const reviews = await phase("Review", (agent) => parallel(lenses, (lens, index) => agent( + `Review ${scope} only for ${lens}. Report concrete issues with file:line, or none.`, + { target: reviewers[index % reviewers.length], writeMode: "read_only", schema: FINDINGS, label: lens }, +))); +const findings = reviews.flatMap((review) => (review.status === "completed" ? review.value.findings : [])); + +const verdicts = await phase("Verify", (agent) => parallel(findings, (finding) => agent( + `Check whether this reported issue is real by reading the code. Be skeptical.\n${JSON.stringify(finding)}`, + { target: verifier, writeMode: "read_only", schema: VERDICT }, +))); + +return findings + .map((finding, index) => ({ ...finding, verdict: verdicts[index] })) + .filter(({ verdict }) => verdict.status === "completed" && verdict.value.real) + .map(({ verdict, ...finding }) => ({ ...finding, reason: verdict.value.reason })); +``` + +## Bake-off + +Several implementers solve the same task in separate worktrees; a judge picks the best. Use it when the approach is +uncertain or quality matters more than turns. + +```js +export const meta = { name: "bakeoff", description: "Compare implementations from several agents and pick one" }; + +const { task, implementers = ["claude", "codex"], judge = "codex" } = args; +const PICK = { + type: "object", + properties: { winner: { type: "integer" }, reason: { type: "string" } }, + required: ["winner", "reason"], +}; + +const attempts = await phase("Implement", (agent) => parallel(implementers, (target) => agent( + `Implement: ${task}. Finish with a summary of your approach and which files you changed.`, + { target, writeMode: "allowed", isolation: "worktree", label: target }, +))); + +const candidates = attempts + .map((attempt, index) => ({ index, implementer: implementers[index], attempt })) + .filter(({ attempt }) => attempt.status === "completed"); +if (candidates.length === 0) throw new Error("Every implementation failed."); + +const pick = await phase("Judge", (agent) => agent( + `Pick the best implementation of "${task}" by index. Weigh correctness, simplicity, and tests.\n` + + candidates.map(({ index, implementer, attempt }) => `#${index} (${implementer}): ${attempt.value}`).join("\n\n"), + { target: judge, writeMode: "read_only", schema: PICK }, +)); + +return { winner: implementers[pick.winner], reason: pick.reason }; +``` + +The winning worktree is kept; inspect it with `devspace workflow calls ` and apply it from there. + +## Map over many items + +When the same change applies to many independent units (modules, endpoints, files), list them first, then run each +through the same stages. Each item gets its own worktree so parallel edits never collide. + +```js +export const meta = { name: "migrate", description: "Apply one change to every matching unit, then check each", concurrency: 3 }; + +const { change, finder = "opencode", worker = "claude", checker = "codex" } = args; +const UNITS = { type: "object", properties: { units: { type: "array", items: { type: "string" } } }, required: ["units"] }; +const CHECK = { type: "object", properties: { ok: { type: "boolean" }, notes: { type: "string" } }, required: ["ok", "notes"] }; + +const { units } = await agent(`List every file or module that needs this change, one per entry: ${change}`, { + target: finder, writeMode: "read_only", schema: UNITS, +}); + +const results = await pipeline( + units, + (unit, _item, index) => agent(`Apply this change to ${unit} only: ${change}`, { + target: worker, writeMode: "allowed", isolation: "worktree", workspace: `unit-${index}`, label: unit, + }), + (_summary, unit, index) => agent(`Verify the change to ${unit} is complete and correct: ${change}`, { + target: checker, writeMode: "read_only", workspace: `unit-${index}`, schema: CHECK, + }), +); + +return units.map((unit, index) => ({ unit, ...results[index] })); +``` + +## Debug with competing hypotheses + +Generate several explanations for a bug, test each independently, then fix the one that holds. + +```js +export const meta = { name: "debug", description: "Investigate competing hypotheses, then fix the confirmed cause" }; + +const { bug, thinker = "codex", investigator = "opencode", fixer = "claude" } = args; +const HYPOTHESES = { type: "object", properties: { hypotheses: { type: "array", items: { type: "string" } } }, required: ["hypotheses"] }; +const EVIDENCE = { type: "object", properties: { confirmed: { type: "boolean" }, evidence: { type: "string" } }, required: ["confirmed", "evidence"] }; + +const { hypotheses } = await agent(`List up to 4 distinct root-cause hypotheses for this bug, most likely first: ${bug}`, { + target: thinker, writeMode: "read_only", schema: HYPOTHESES, +}); + +const checks = await phase("Investigate", (agent) => parallel(hypotheses, (hypothesis) => agent( + `Bug: ${bug}\nTest this hypothesis against the code and, if useful, by running commands. Do not edit files.\nHypothesis: ${hypothesis}`, + { target: investigator, writeMode: "read_only", schema: EVIDENCE }, +))); + +const index = checks.findIndex((check) => check.status === "completed" && check.value.confirmed); +if (index === -1) return { fixed: false, checks }; + +const cause = { hypothesis: hypotheses[index], evidence: checks[index].value.evidence }; +await phase("Fix", (agent) => agent( + `Fix this bug at its root cause and add a regression test.\nBug: ${bug}\nCause: ${JSON.stringify(cause)}`, + { target: fixer, writeMode: "allowed", isolation: "worktree" }, +)); + +return { fixed: true, cause }; +``` + +## Compose a full pipeline + +Named workflows can call each other one level deep, so the patterns above become building blocks: + +```js +export const meta = { name: "ship", description: "Plan, build with review, then gate on QA" }; + +const plan = await workflow("plan", { task: args.task }); +const build = await workflow("build", { task: `${args.task}\nFollow this plan:\n${JSON.stringify(plan.plan)}` }); +return { plan, build }; +``` + +Nested workflows share the parent's turn budget, cancellation, and named worktrees: a nested workflow that uses +`workspace: "feature"` works on the same tree as any other step in the run that created it. Run the parent with the +write mode the nested workflows need, since they cannot exceed it. diff --git a/skills/workflows/reference.md b/skills/workflows/reference.md index ccfeb49a5..8dd273122 100644 --- a/skills/workflows/reference.md +++ b/skills/workflows/reference.md @@ -68,7 +68,7 @@ const failures = results.filter((r) => r.status === "failed").map((r) => r.error | `schema` | JSON Schema for a structured response (see below). | | `writeMode` | `"read_only"` or `"allowed"`. Defaults to the run's authority and can never exceed it. | | `isolation` | `"worktree"`: run this call in its own Git worktree created from committed `HEAD`. | -| `workspace` | A logical name (letters, digits, `_`, `-`; at most 64) that calls in this run share. Never a path. | +| `workspace` | A logical name (letters, digits, `_`, `-`; at most 64) for a worktree that later calls in this run reuse. Never a path. The first call that uses a name must also pass `isolation: "worktree"` to create it; later calls pass only `workspace`. | Any other option is rejected. Each prompt should be self-contained: a subagent sees only its prompt and the project, not the rest of the workflow. @@ -96,9 +96,19 @@ Schemas are strict draft-07 with inline structural constraints only: at most 16 Runs are read-only unless started with `devspace workflow run ... --write-mode allowed`. In a writing run, give editing calls `writeMode: "allowed"`. Add `isolation: "worktree"` when parallel calls might edit the same files, so -each works in its own worktree; isolated worktrees do not see uncommitted changes. Worktrees are kept for +each works in its own worktree; isolated worktrees do not see uncommitted changes. To let several calls work on the +same tree in turn (implement, then review, then fix), create a named worktree once and reuse it: + +```js +await agent("Implement the change.", { target: "claude", writeMode: "allowed", isolation: "worktree", workspace: "feature" }); +await agent("Review the uncommitted diff.", { target: "codex", writeMode: "read_only", workspace: "feature" }); +``` + +Without `workspace` or `isolation`, calls run in the checkout the workflow was started from. Worktrees are kept for inspection afterwards. +For proven multi-step engineering loops (plan, implement, review, QA, bake-offs, fan-out), see `patterns.md`. + ## Resume `devspace workflow run --resume ` reruns the script with the original arguments. Completed read-only calls are From 5d7dd0dde8b999d7fa69be47b1c38d7782c36c11 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:03:16 +0530 Subject: [PATCH 3/3] docs(workflows): note that exceeding a limit ends the run Found while testing on beta.6: a workflow() nested past one level failed the whole run even inside try/catch, by design. Co-Authored-By: Claude Opus 5.5 --- skills/workflows/reference.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skills/workflows/reference.md b/skills/workflows/reference.md index 8dd273122..840e2924f 100644 --- a/skills/workflows/reference.md +++ b/skills/workflows/reference.md @@ -127,3 +127,7 @@ call that differs, everything runs fresh. Runs that made editing calls cannot be | Agent response / run result | 256 KiB | | `log` and `phase` events | 64 KiB per run | | Sandbox heap / stack | 32 MiB / 512 KiB | + +Exceeding a limit ends the whole run, including a `workflow()` call nested past one level: `try`/`catch` in the +script cannot recover from it. Ordinary agent failures (unknown target, provider errors, invalid output) are normal +rejections you can catch, and `parallel`/`pipeline` report them per item.