From 9a8442500b1edaedc4372a363a18349a937fd09f Mon Sep 17 00:00:00 2001 From: Waishnav Date: Tue, 21 Jul 2026 12:10:26 +0530 Subject: [PATCH 1/6] docs: claude's dw for implementation inspiration --- docs/dynamic-workflow/claude/README.md | 47 ++++ docs/dynamic-workflow/claude/agent.md | 205 ++++++++++++++ docs/dynamic-workflow/claude/architecture.md | 101 +++++++ docs/dynamic-workflow/claude/cheatsheet.md | 154 +++++++++++ docs/dynamic-workflow/claude/concurrency.md | 191 +++++++++++++ .../dynamic-workflow/claude/control-and-io.md | 179 ++++++++++++ docs/dynamic-workflow/claude/lifecycle.md | 108 ++++++++ docs/dynamic-workflow/claude/limits.md | 76 ++++++ docs/dynamic-workflow/claude/opt-in.md | 67 +++++ docs/dynamic-workflow/claude/orchestration.md | 138 ++++++++++ docs/dynamic-workflow/claude/patterns.md | 254 ++++++++++++++++++ docs/dynamic-workflow/claude/primitives.md | 62 +++++ docs/dynamic-workflow/claude/resume.md | 94 +++++++ .../claude/script-contract.md | 145 ++++++++++ docs/dynamic-workflow/claude/usecases.md | 180 +++++++++++++ docs/dynamic-workflow/claude/workflow-tool.md | 139 ++++++++++ 16 files changed, 2140 insertions(+) create mode 100644 docs/dynamic-workflow/claude/README.md create mode 100644 docs/dynamic-workflow/claude/agent.md create mode 100644 docs/dynamic-workflow/claude/architecture.md create mode 100644 docs/dynamic-workflow/claude/cheatsheet.md create mode 100644 docs/dynamic-workflow/claude/concurrency.md create mode 100644 docs/dynamic-workflow/claude/control-and-io.md create mode 100644 docs/dynamic-workflow/claude/lifecycle.md create mode 100644 docs/dynamic-workflow/claude/limits.md create mode 100644 docs/dynamic-workflow/claude/opt-in.md create mode 100644 docs/dynamic-workflow/claude/orchestration.md create mode 100644 docs/dynamic-workflow/claude/patterns.md create mode 100644 docs/dynamic-workflow/claude/primitives.md create mode 100644 docs/dynamic-workflow/claude/resume.md create mode 100644 docs/dynamic-workflow/claude/script-contract.md create mode 100644 docs/dynamic-workflow/claude/usecases.md create mode 100644 docs/dynamic-workflow/claude/workflow-tool.md diff --git a/docs/dynamic-workflow/claude/README.md b/docs/dynamic-workflow/claude/README.md new file mode 100644 index 00000000..b9ddb350 --- /dev/null +++ b/docs/dynamic-workflow/claude/README.md @@ -0,0 +1,47 @@ +# Claude Code Dynamic Workflows + +In-depth reference for Claude Code’s **dynamic workflow** system: the model-facing `Workflow` tool, the JavaScript script contract, every primitive injected into the script, resume/budget semantics, quality patterns, and how this enables a stronger model to orchestrate subagents one level above ordinary tool use. + +| Document | Contents | +|---|---| +| [Architecture](./architecture.md) | Three layers, control flow vs worker content, mental model | +| [Opt-in & Ultracode](./opt-in.md) | When the model may call Workflow; standing multi-agent mode | +| [Workflow tool API](./workflow-tool.md) | Tool inputs, return envelope, launch/iterate/named workflows | +| [Script contract](./script-contract.md) | `export const meta`, language rules, determinism bans | +| [Primitives overview](./primitives.md) | Map of all script hooks | +| [agent()](./agent.md) | Spawn API, schema, model/effort, worktree, agentType | +| [pipeline() & parallel()](./concurrency.md) | No-barrier default vs barrier; when each is correct | +| [phase, log, args, budget, workflow()](./control-and-io.md) | Progress UX, parameterization, token ceiling, nesting | +| [Limits & sandbox](./limits.md) | Concurrency caps, agent caps, script isolation | +| [Resume & journal](./resume.md) | `resumeFromRunId`, cache identity, `journal.jsonl` | +| [Quality patterns](./patterns.md) | Adversarial verify, judge panel, loop-until-dry, … | +| [Lifecycle & UX](./lifecycle.md) | Permission, `/workflows`, notifications, multi-phase | +| [One level above](./orchestration.md) | Bigger brain orchestrates smaller hands | +| [Use cases](./usecases.md) | Review fleets, migrations, research, self-repair | +| [Cheatsheet](./cheatsheet.md) | One-page API card | + +Related: standalone HTML overview at [`docs/claude-code-dynamic-workflows.html`](../../claude-code-dynamic-workflows.html). + +--- + +## One-sentence thesis + +**Deterministic control flow + stochastic workers + one orchestrator brain.** + +A single agent loop confuses *what to do next* with *how to do the work*. Dynamic workflows split them: the orchestrator model authors a short plain-JS script; the harness runs loops, conditionals, and fan-out as **code**; only `agent()` escapes into a model with tools. + +## Why it exists + +| Without workflows | With workflows | +|---|---| +| Fan-out is ad-hoc tool spam each turn | Fan-out is `parallel` / `pipeline` in a script | +| Verification is optional and forgettable | Verification is a stage in the graph | +| One context holds plan + all tool churn | Workers isolate tool churn; script aggregates returns | +| Scale = longer single conversation | Scale = fleet size × stages under budget | + +## Scope of this doc set + +- **In scope:** model-facing API as exposed by Claude Code ~2.1.x (`Workflow` / `RunWorkflow` tool, script primitives, opt-in, resume, budget, patterns). +- **Out of scope:** Anthropic product marketing, undocumented internal harness code, DevSpace’s separate durable workflow engine (see feature branches / other docs if present). + +Source basis: Claude Code Workflow tool description, session `workflows/scripts/*.js` examples, and engine rules encoded in the tool prompt (opt-in, ultracode, resume, concurrency). diff --git a/docs/dynamic-workflow/claude/agent.md b/docs/dynamic-workflow/claude/agent.md new file mode 100644 index 00000000..40fe3f97 --- /dev/null +++ b/docs/dynamic-workflow/claude/agent.md @@ -0,0 +1,205 @@ +# `agent()` + +The only primitive that spends model/tool budget on real work. Everything else in the script is control flow, UX, or nesting. + +## Signature + +```ts +agent( + prompt: string, + opts?: { + label?: string + phase?: string + schema?: object // JSON Schema + model?: string // e.g. session model ids / 'sonnet' | 'opus' | 'haiku' + effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' + isolation?: 'worktree' + agentType?: string // registry name: 'general-purpose', 'code-reviewer', 'claude', … + } +): Promise +``` + +## Return value + +| Mode | Resolves to | +|---|---| +| No `schema` | Final assistant text (`string`) | +| With `schema` | **Validated object** matching the JSON Schema (StructuredOutput tool; model retries on mismatch) | +| User skip / terminal API death after retries | `null` | + +Always treat results as possibly null when fan-out is large: + +```js +const rows = (await parallel(tasks)).filter(Boolean) +``` + +## Semantics + +1. Spawns a **subagent** with its own tool loop (Read, Bash, Edit, …). +2. Subagents are instructed that their **final text/object is the return value** to the coordinator script — not a user-facing message. +3. Prompt should be **self-contained**: workers do not inherit the full main-session transcript. +4. Session-connected **MCP tools** are reachable via ToolSearch (on-demand schemas). Interactively authenticated MCP may be missing in headless/cron. +5. Errors in the agent path surface as `null` for that call in combinators that swallow rejections; check journals if results look empty ([resume](./resume.md)). + +## Options + +### `label` + +Short string for `/workflows` progress UI (e.g. `review:security`, `verify:src/auth.ts`). Does not affect model behavior. + +### `phase` + +Explicit progress group assignment. **Prefer this inside `pipeline` / `parallel` stages** to avoid races on the global `phase()` state. Same string → same group box. Should match titles in `meta.phases` when you want tidy UI. + +### `schema` + +JSON Schema object. Forces structured output: + +- Validation at the tool-call layer. +- `agent()` returns the object — no `JSON.parse` of prose. +- Composes with `agentType` (StructuredOutput instruction is appended to that agent’s system prompt). + +Example: + +```js +const FINDINGS = { + type: 'object', + properties: { + findings: { + type: 'array', + items: { + type: 'object', + properties: { + file: { type: 'string' }, + line: { type: 'integer' }, + issue: { type: 'string' }, + severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] }, + }, + required: ['file', 'line', 'issue', 'severity'], + additionalProperties: false, + }, + }, + summary: { type: 'string' }, + }, + required: ['findings', 'summary'], + additionalProperties: false, +} + +const result = await agent( + 'Review auth changes for session fixation. Read-only. Cite file:line.', + { + label: 'review:auth', + phase: 'Review', + schema: FINDINGS, + effort: 'medium', + } +) +// result.findings is already shaped +``` + +### `model` + +Override the model for this call. + +- **Default: omit** — inherit the main-loop / session model (almost always correct). +- Set only when you are confident a different tier fits (e.g. small model for mechanical map, large for hard judge). +- When unsure, omit. + +### `effort` + +Reasoning effort for this call: `'low' | 'medium' | 'high' | 'xhigh' | 'max'`. + +- Omit → inherit session effort. +- Use `'low'` for cheap mechanical stages (enumerate files, simple extract). +- Reserve higher tiers for hard verify / judge / design stages. + +### `isolation: 'worktree'` + +Runs the agent in a **fresh git worktree**. + +| Property | Detail | +|---|---| +| Cost | Expensive (~200–500ms setup + disk) per agent | +| When | **Only** when agents **mutate files in parallel** and would otherwise conflict | +| Cleanup | Auto-removed if unchanged | +| When not | Read-only review, single writer, sequential pipeline of mutators on one tree | + +### `agentType` + +Custom subagent from the **same registry as the Agent tool** (e.g. `general-purpose`, `code-reviewer`, `Explore`, project-defined types, or `claude` where configured). + +- Overrides the default workflow subagent personality/tools policy for that call. +- Composes with `schema`. + +## Prompting workers well + +Workers start with **only** the prompt you pass (+ profile/system for `agentType`). Patterns: + +**Implementation** + +```text +Goal: … +Context: … +Relevant files: … +Acceptance criteria: +- … +Rules: +- Keep changes focused +- Do not unrelated-refactor +- Report blockers clearly +``` + +**Read-only investigation** + +```text +Question: … +Scope: … +Rules: +- Do not modify files +- Cite paths and symbols +- Separate facts from guesses +``` + +**Structured judge / refuter** + +```text +Try to REFUTE: +Default to refuted=true if uncertain. +Return only via the schema fields. +``` + +Pass prior stage data by **embedding it in the prompt** (stringified structured JSON), not by shared mutable state — scripts have no shared worker memory beyond what you thread through returns. + +## Cost & altitude tips + +| Stage kind | Typical opts | +|---|---| +| Enumerate / map / extract | `effort: 'low'`, maybe smaller `model` | +| Implement / edit | inherit model; medium effort; `isolation: 'worktree'` if parallel | +| Review dimension | schema + medium effort | +| Adversarial judge | higher effort; schema; independent prompts | +| Final verify gates | low/medium; focused prompt | + +The orchestrator stays high-altitude by keeping **structure** in the script and **content** in workers. See [orchestration](./orchestration.md). + +## Null and failure hygiene + +```js +const reviews = await parallel([ + () => agent(p1, { schema: FINDINGS }), + () => agent(p2, { schema: FINDINGS }), + () => agent(p3, { schema: FINDINGS }), +]).then(xs => xs.filter(Boolean)) + +if (!reviews.length) { + log('all reviewers failed or were skipped') + return { confirmed: [], error: 'no_reviews' } +} +``` + +Before claiming “workflow returned empty,” read `transcriptDir/journal.jsonl` — cached or failed agents may explain it ([resume](./resume.md)). + +## Next + +- [pipeline & parallel](./concurrency.md) +- [Patterns](./patterns.md) diff --git a/docs/dynamic-workflow/claude/architecture.md b/docs/dynamic-workflow/claude/architecture.md new file mode 100644 index 00000000..180e2fb6 --- /dev/null +++ b/docs/dynamic-workflow/claude/architecture.md @@ -0,0 +1,101 @@ +# Architecture + +## Three layers + +``` +User intent + │ + ▼ +┌──────────────────────────────────────────┐ +│ 1. Coordinator (main session model) │ +│ · talks to user · scouts work-list │ +│ · authors / selects script │ +│ · calls Workflow({ script, args }) │ +│ · synthesizes return value for user │ +└───────────────────┬──────────────────────┘ + │ Workflow tool (async) + ▼ +┌──────────────────────────────────────────┐ +│ 2. Workflow engine (JS runtime) │ +│ · parses export const meta │ +│ · runs script in async context │ +│ · hosts agent/pipeline/parallel/… │ +│ · enforces concurrency & agent caps │ +│ · journals each agent() for resume │ +└───────────────────┬──────────────────────┘ + │ agent() × N + ┌───────────┼───────────┐ + ▼ ▼ ▼ +┌────────────┐ ┌────────────┐ ┌────────────┐ +│ Subagent A │ │ Subagent B │ │ Subagent C │ +│ own tools │ │ own tools │ │ own tools │ +│ optional │ │ schema / │ │ worktree / │ +│ return │ │ model / │ │ agentType │ +│ text|obj │ │ effort │ │ │ +└────────────┘ └────────────┘ └────────────┘ +``` + +### 1. Coordinator (main loop) + +- Owns the conversation with the human. +- Scouts the repo / discovers the work-list *before* orchestration when possible (hybrid default). +- Decides whether Workflow is allowed ([opt-in](./opt-in.md)). +- Authors or selects the script and `args`. +- Receives the script’s return value after completion and narrates / chains the next phase. + +The coordinator is the **only** place that should redesign the global plan. Workers execute units; they do not renegotiate the graph with each other. + +### 2. Workflow engine + +- Not another chat peer. It is a **small concurrent orchestration runtime**. +- Script language: plain JavaScript (not TypeScript). +- Injected APIs only: see [primitives](./primitives.md). +- Progress UI: `/workflows` groups agents by `phase` / `label`. +- Persistence: script path under session directory, `runId`, transcripts, `journal.jsonl`. + +### 3. Subagents + +- Full tool loops of their own (Read, Bash, Edit, MCP via ToolSearch, …). +- Final text **is** the return value (or a schema-validated object) — not a user-facing essay unless the prompt asks for one. +- Optional isolation (`isolation: 'worktree'`), model, effort, and `agentType` overrides per call. + +## Mental model + +```js +// Coordinator decides STRUCTURE +Workflow({ script, args }) + → JS engine runs control flow + → agent(prompt, opts) × N // workers decide CONTENT + → script return value +→ coordinator narrates to user +``` + +| Concern | Who owns it | +|---|---| +| User intent, product judgment | Coordinator | +| Graph shape (fan-out, verify, merge) | Script (authored by coordinator) | +| Tool use, file reads, edits | Subagents | +| Concurrency caps, resume cache, budget hard stop | Engine | +| Permission to multi-agent at all | User (opt-in / ultracode) | + +## Hybrid default + +You do **not** need the full orchestration shape before starting the *task*. You need it before the *orchestration step*: + +1. Scout inline (list files, scope diff, find call sites). +2. Build the work-list in the coordinator context. +3. Call `Workflow` to pipeline over that list. +4. Read the result; optionally chain another workflow for the next phase. + +For larger product work, prefer **several well-scoped workflows across turns** over one forever-script. + +## What is not a layer + +- Workers do not form a free-form multi-agent chat room. +- The script has no filesystem or network — it cannot “just run shell.” Escape is only `agent()` / nested `workflow()`. +- The Workflow tool returns **immediately** (async launch). Completion arrives via task notification; live progress is `/workflows`. + +## Next + +- [Opt-in & Ultracode](./opt-in.md) +- [Workflow tool API](./workflow-tool.md) diff --git a/docs/dynamic-workflow/claude/cheatsheet.md b/docs/dynamic-workflow/claude/cheatsheet.md new file mode 100644 index 00000000..7604ac57 --- /dev/null +++ b/docs/dynamic-workflow/claude/cheatsheet.md @@ -0,0 +1,154 @@ +# Cheatsheet + +One-page API card. Details in the linked docs. + +## Tool + +```js +Workflow({ + script?, // inline JS; must start with pure-literal meta + name?, // built-in or .claude/workflows/ + scriptPath?, // persisted path; wins over script/name + args?, // verbatim → global args (real JSON, not stringified) + resumeFromRunId?, // ^wf_[a-z0-9-]{6,}$ stop prior run first +}) +// need: script | name | scriptPath +// returns async launch: taskId, runId, scriptPath, transcriptDir, … +``` + +[workflow-tool.md](./workflow-tool.md) · [opt-in.md](./opt-in.md) + +## Script header + +```js +export const meta = { + name: '…', // required, pure literal + description: '…', // required — permission dialog + phases: [ // optional + { title: 'Scan', detail: '…', model: 'sonnet' }, + ], + // whenToUse?: '…' +} +// plain JS only — no TS types +// no Date.now / Math.random / bare new Date +``` + +[script-contract.md](./script-contract.md) + +## Primitives + +```ts +agent(prompt, { + label?, phase?, schema?, model?, effort?, + isolation?: 'worktree', agentType?, +}): Promise + +pipeline(items, stage1, stage2, …): Promise +// stage(prev, originalItem, index) — NO barrier between stages + +parallel(thunks: Array<() => Promise>): Promise +// BARRIER; slots null on error; never rejects + +phase(title: string): void +log(message: string): void + +args: any +budget: { total: number|null, spent(): number, remaining(): number } + +workflow(name | { scriptPath }, args?): Promise +// nest depth 1 only +``` + +[primitives.md](./primitives.md) · [agent.md](./agent.md) · [concurrency.md](./concurrency.md) · [control-and-io.md](./control-and-io.md) + +## Rules of thumb + +1. Default multi-stage → **`pipeline`**; barrier only for cross-item merge. +2. Always **`.filter(Boolean)`** on parallel / nullable agent results. +3. Prefer **`schema`** for structured handoffs. +4. Guard budget loops: **`budget.total && budget.remaining() > …`**. +5. No entropy in scripts (resume safety). +6. **`isolation: 'worktree'`** only for parallel mutators. +7. **`log()`** anything a silent cap would hide. +8. Hybrid: scout → Workflow → synthesize → maybe next phase. +9. Omit **`model`** unless tier fit is clear. +10. Dedup open-ended hunts against **`seen`**, not only confirmed. + +## Caps (engine) + +| Cap | Value | +|---|---| +| Concurrent agents | `min(16, cores-2)` / workflow (queue rest) | +| Lifetime agents | 1000 / run | +| Items / parallel|pipeline | 4096 | +| Nested workflow | depth 1 | +| Budget | hard throw when spent ≥ total | + +[limits.md](./limits.md) + +## Resume + +```js +// stop prior run, then: +Workflow({ + scriptPath, + resumeFromRunId: runId, + args: sameArgs, +}) +// longest unchanged agent() prefix → cache +// read transcriptDir/journal.jsonl if results look wrong +``` + +[resume.md](./resume.md) + +## Pattern stubs + +```js +// adversarial verify +const votes = await parallel(Array.from({ length: 3 }, () => () => + agent(`Refute: ${claim}. Default refuted=true if uncertain.`, { schema: V }) +)) +const ok = votes.filter(Boolean).filter(v => !v.refuted).length >= 2 + +// loop-until-budget +while (budget.total && budget.remaining() > 50_000) { + const r = await agent('…', { schema: S }) + /* accumulate */ log(`${budget.remaining()} left`) +} + +// canonical review pipeline +await pipeline( + DIMENSIONS, + d => agent(d.prompt, { phase: 'Review', schema: F }), + review => parallel(review.findings.map(f => () => + agent(`Verify: ${f.title}`, { phase: 'Verify', schema: V }) + .then(v => ({ ...f, verdict: v })) + )) +) +``` + +[patterns.md](./patterns.md) + +## Opt-in (must have one) + +- User said `ultracode` / session ultracode on +- User asked for workflow / fan-out / multi-agent orchestration +- Skill/command requires Workflow +- Named workflow requested + +Else: single `Agent` or ask. + +## Altitude + +| Layer | Owns | +|---|---| +| Orchestrator | Intent, graph, schemas, synthesis, user | +| Script | Loops, fan-out, votes, budget stops | +| Workers | Tools, content, optional worktree | +| Engine | Caps, journal, UI, permissions plumbing | + +[orchestration.md](./orchestration.md) · [usecases.md](./usecases.md) + +## Index + +[README](./README.md) · [Architecture](./architecture.md) · [Lifecycle](./lifecycle.md) diff --git a/docs/dynamic-workflow/claude/concurrency.md b/docs/dynamic-workflow/claude/concurrency.md new file mode 100644 index 00000000..bf67020a --- /dev/null +++ b/docs/dynamic-workflow/claude/concurrency.md @@ -0,0 +1,191 @@ +# `pipeline()` & `parallel()` + +These two combinators are the heart of multi-agent structure. Using the wrong one wastes wall-clock or forces incorrect synchronization. + +## Quick contrast + +| | `pipeline` | `parallel` | +|---|---|---| +| Input | `items[]` + stage functions | `thunks[]` of `() => Promise` | +| Sync model | **No barrier** between stages | **Barrier** — wait for all thunks | +| Wall-clock | ≈ slowest **item chain** | ≈ slowest **thunk** (then next barrier stage) | +| Failure | Stage throw → that item becomes `null`, later stages skipped for it | Thunk throw / agent error → slot `null`; call never rejects | +| Default for multi-stage? | **Yes** | No — only when you need all results together | + +--- + +## `pipeline` + +### Signature + +```ts +pipeline( + items: any[], + stage1: (prev, originalItem, index) => any | Promise, + stage2?: (prev, originalItem, index) => any | Promise, + // ... +): Promise +``` + +### Semantics + +- Each **item** flows through **all stages independently**. +- Item A may be in stage 3 while item B is still in stage 1. +- Every stage receives `(prevResult, originalItem, index)`: + - Use `originalItem` / `index` to label work without stuffing identity only into stage-1 returns. +- A stage that **throws** drops that item to `null` and skips remaining stages for that item. +- Max items per call: **4096** (hard error if exceeded) — see [limits](./limits.md). + +### Canonical multi-stage pattern + +Review by dimension, then verify each finding **as soon as that dimension finishes** (not after all dimensions finish): + +```js +export const meta = { + name: 'review-changes', + description: 'Review changed files across dimensions, verify each finding', + phases: [{ title: 'Review' }, { title: 'Verify' }], +} + +const DIMENSIONS = [ + { key: 'bugs', prompt: '…' }, + { key: 'perf', prompt: '…' }, +] + +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 })) + ) + ) +) + +const confirmed = results + .flat() + .filter(Boolean) + .filter(f => f.verdict?.isReal) + +return { confirmed } +// Dimension "bugs" findings verify while "perf" is still reviewing. +``` + +### Transform inside a stage (no extra barrier) + +```js +// ❌ Smell: barrier only to flatten +const a = await parallel(items.map(i => () => agent(…))) +const b = a.filter(Boolean).flatMap(x => x.findings) +const c = await parallel(b.map(f => () => agent(verify(f)))) + +// ✅ Pipeline with transform in a stage +const c = await pipeline( + items, + i => agent(…), + r => r.findings, // pure transform + f => agent(verify(f), { schema: V }) // or map to parallel inside if many findings +) +``` + +If one item produces many findings, a stage may return `parallel(findings.map(...))` as in the canonical example. + +--- + +## `parallel` + +### Signature + +```ts +parallel(thunks: Array<() => Promise>): Promise +``` + +### Semantics + +- Runs thunks **concurrently**. +- **Barrier:** does not resolve until every thunk settles. +- A throwing thunk (or agent error) becomes **`null`** in that index — the `parallel` call **itself never rejects**. +- Always `.filter(Boolean)` before treating results as data. +- Same concurrency / item caps as overall engine ([limits](./limits.md)). + +### When a barrier is correct + +Use `parallel` (or a barrier between pipeline stages implemented via collecting all items) **only** when stage N needs **cross-item** context from **all** of stage N−1: + +1. **Dedup / merge** across the full set before expensive work. +2. **Early-exit** if total count is zero (“0 bugs → skip verification”). +3. Next prompt **references “the other findings”** for comparison. + +```js +// Correct barrier: need ALL findings before expensive verification +const all = await parallel( + DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS_SCHEMA })) +) +const deduped = dedupeByFileAndLine( + all.filter(Boolean).flatMap(r => r.findings) +) +if (!deduped.length) { + log('0 findings — skip verify') + return { confirmed: [] } +} +const verified = await parallel( + deduped.map(f => () => agent(verifyPrompt(f), { schema: VERDICT_SCHEMA })) +) +``` + +### When a barrier is NOT justified + +| Bad reason | Do this instead | +|---|---| +| “I need to flatten/map/filter first” | Transform inside a `pipeline` stage | +| “Stages are conceptually separate” | `pipeline` already models separate stages without sync | +| “It’s cleaner code” | Barrier latency is real — if 5 finders run and the slowest is 3× the fastest, a barrier wastes most of the fast agents’ idle time | + +**Smell test:** if you wrote `parallel → transform → parallel` with no cross-item dependency, rewrite as `pipeline`. + +--- + +## Nested concurrency + +Stages of a `pipeline` may call `parallel` (per item). Outer `parallel` may launch whole pipelines. Nested `workflow()` shares the parent concurrency pool. + +```js +// Per-item: many judges after one finder +await pipeline( + targets, + t => agent(findPrompt(t), { schema: BUGS }), + found => parallel( + found.bugs.map(b => () => agent(judgePrompt(b), { schema: VERDICT })) + ) +) +``` + +## Concurrency cap interaction + +Only ~`min(16, cpu_cores - 2)` agents run at once per workflow; the rest **queue**. You can still pass large arrays — they complete, they just don’t all run simultaneously. See [limits](./limits.md). + +## Decision flowchart + +``` +Need multi-stage over a list? + │ + ├─ Does stage N need the FULL set from stage N-1? + │ yes → parallel (barrier) then next stage + │ no → pipeline(items, stage1, stage2, …) + │ + └─ Single fan-out, one stage only? + → parallel([() => agent…, …]) or pipeline(items, oneStage) +``` + +## Next + +- [phase, log, args, budget, workflow()](./control-and-io.md) +- [Patterns](./patterns.md) diff --git a/docs/dynamic-workflow/claude/control-and-io.md b/docs/dynamic-workflow/claude/control-and-io.md new file mode 100644 index 00000000..b347469d --- /dev/null +++ b/docs/dynamic-workflow/claude/control-and-io.md @@ -0,0 +1,179 @@ +# `phase`, `log`, `args`, `budget`, `workflow()` + +Progress UX, parameterization, token ceilings, and one-level nesting. + +--- + +## `phase` + +```ts +phase(title: string): void +``` + +- Starts a **progress group** in `/workflows`. +- Subsequent `agent()` calls **without** `opts.phase` group under this title. +- Titles should match `meta.phases[].title` exactly for clean UI; unmatched titles still get their own group. +- Inside concurrent stages, prefer **`opts.phase`** on each `agent()` to avoid races on the global phase state: + +```js +// Global phase — fine for sequential sections +phase('Implement') +await agent('…', { label: 'impl' }) + +// Concurrent — set phase per agent +await parallel([ + () => agent('…', { phase: 'Review', label: 'r1' }), + () => agent('…', { phase: 'Review', label: 'r2' }), +]) +``` + +`phase` is UX only — it does not create isolation, budget buckets, or barriers. + +--- + +## `log` + +```ts +log(message: string): void +``` + +- Emits a **narrator line** above the progress tree. +- Use for counts, early exits, dropped coverage, loop progress. + +**Rule: no silent caps.** If the workflow bounds coverage (top-N, sampling, “first 20 files”), `log()` what was dropped. Silent truncation reads as “we covered everything.” + +```js +if (files.length > 50) { + log(`scoping to first 50 of ${files.length} files`) + files = files.slice(0, 50) +} +``` + +--- + +## `args` + +```ts +args: any // Workflow({ args }) value, or undefined if omitted +``` + +### Rules + +1. Value is **verbatim** from the tool call. +2. Pass **real** JSON arrays/objects in the tool invocation — **not** a stringified JSON blob. + +```js +// ✅ +Workflow({ script, args: ['a.ts', 'b.ts'] }) +// in script: args.map(f => …) + +// ❌ +Workflow({ script, args: '["a.ts","b.ts"]' }) +// args is a string → args.map throws +``` + +3. Primary channel for **parameterizing** named workflows (research question, path list, config). +4. Primary channel for values that must stay **stable across resume** (fixed timestamps, seeds) — see [script determinism](./script-contract.md). + +```js +// script +const topic = args?.topic ?? 'authentication' +const files = args?.files ?? [] +await agent(`Review ${topic} in ${files.join(', ')}`, { schema: FINDINGS }) +``` + +--- + +## `budget` + +```ts +budget: { + total: number | null + spent(): number + remaining(): number // max(0, total - spent) or Infinity if no target +} +``` + +### Semantics + +| Field / method | Meaning | +|---|---| +| `total` | Turn token target from user “+500k”-style directives; `null` if unset | +| `spent()` | Output tokens spent this turn across **main loop + all workflows** (shared pool) | +| `remaining()` | `max(0, total - spent())`, or **`Infinity`** if no target | + +### Hard ceiling + +Once `spent()` reaches `total`, further **`agent()` calls throw**. This is not advisory. + +### Guard loops + +Without a target, `remaining()` is `Infinity` and a `while (budget.remaining() > …)` loop runs until the **1000-agent** lifetime cap. Always guard: + +```js +const bugs = [] +while (budget.total && budget.remaining() > 50_000) { + const result = await agent('Find bugs in this codebase.', { schema: BUGS_SCHEMA }) + bugs.push(...result.bugs) + log(`${bugs.length} found, ${Math.round(budget.remaining() / 1000)}k remaining`) +} +``` + +### Static fleet sizing + +```js +const FLEET = budget.total + ? Math.floor(budget.total / 100_000) + : 5 +``` + +### Loop-until-count (no budget) + +```js +const bugs = [] +while (bugs.length < 10) { + const result = await agent('Find bugs…', { schema: BUGS_SCHEMA }) + bugs.push(...result.bugs) + log(`${bugs.length}/10 found`) +} +``` + +Prefer budget-aware or dry-round stops for open-ended hunts ([patterns](./patterns.md)). + +--- + +## Nested `workflow()` + +```ts +workflow( + nameOrRef: string | { scriptPath: string }, + args?: any +): Promise +``` + +### Semantics + +| Aspect | Detail | +|---|---| +| Purpose | Run another workflow **inline** as a sub-step; return its return value | +| `string` | Name in saved/built-in registry (same as `Workflow({ name })`) | +| `{ scriptPath }` | Script file on disk (e.g. previously persisted) | +| Shared with parent | Concurrency cap, agent counter, abort signal, token `budget` | +| UI | Child agents under a nested group in `/workflows` | +| Nesting depth | **One level only** — `workflow()` inside a child **throws** | +| Errors | Unknown name / unreadable path / child syntax error → throw (catch to handle) | + +```js +const map = await workflow('understand-subsystem', { root: 'src/auth' }) +const plan = await workflow('design-panel', { context: map }) +return { map, plan } +``` + +Use nesting to compose **reusable** named workflows. Prefer sequential top-level `Workflow` tool calls across turns when the coordinator must read results and replan with the user. + +--- + +## Next + +- [Limits & sandbox](./limits.md) +- [Resume & journal](./resume.md) diff --git a/docs/dynamic-workflow/claude/lifecycle.md b/docs/dynamic-workflow/claude/lifecycle.md new file mode 100644 index 00000000..4a2e41c6 --- /dev/null +++ b/docs/dynamic-workflow/claude/lifecycle.md @@ -0,0 +1,108 @@ +# Lifecycle & operator UX + +How a workflow run feels end-to-end. + +## Happy path + +``` +1. Authoring + Coordinator scouts → builds work-list → writes inline script (or picks name) + +2. Permission + User sees meta.description (+ size guideline if configured) + +3. Launch + Workflow tool returns immediately: + taskId, runId, scriptPath, transcriptDir, workflowName, status + +4. Progress + /workflows shows: + · phase groups + · agent labels + · nested child workflow groups + · narrator log() lines + +5. Completion + delivers script return value to coordinator + +6. Synthesis + Coordinator may: + · answer the user + · edit scriptPath + resume + · launch another Workflow for the next phase +``` + +## Multi-phase product work + +Prefer **several workflows across turns** over one mega-script: + +| Turn | Workflow | Coordinator does after | +|---|---|---| +| 1 | Understand | Read map; decide design scope | +| 2 | Design panel | Pick approach with user if needed | +| 3 | Implement + review repair | Inspect diff; request fixes | +| 4 | Verify / audit | Ship narrative + residual risk | + +The coordinator **stays in the loop** between phases — that is a feature. Ultracode makes this the default for substantive work ([opt-in](./opt-in.md)). + +## Hybrid single-phase + +``` +scout (inline tools) + → Workflow(pipeline over discovered items) + → synthesize answer +``` + +You need the work-list shape before the orchestration step, not before any investigation. + +## Background vs blocking mental model + +From the model’s perspective: + +- The `Workflow` **tool call** returns once the run is **registered** (async launch). +- The **result of the script** arrives later via notification / task completion channel. +- Do not assume the tool return value is the script’s `return {…}` object. + +Operators watch `/workflows` for live structure. + +## Iteration loop + +``` +launch → observe → Edit(scriptPath) → stop if needed → resumeFromRunId +``` + +See [resume](./resume.md). + +## Failure / skip paths + +| Event | Typical handling | +|---|---| +| Syntax error in script | `error` on launch result; fix script, relaunch | +| User denies permission | No run; ask or fall back to single Agent | +| User skips individual agent | That `agent()` → `null`; filter and continue or abort in script | +| Budget exhausted | Further `agent()` throws; catch / end loop; return partial | +| Agent terminal API failure | `null`; log; optionally retry with new call (not automatic) | + +## Named vs inline scripts + +| Mode | When | +|---|---| +| Inline `script` | First design of a one-off harness | +| `scriptPath` iterate | Evolving a run without re-pasting | +| `name` / `.claude/workflows/` | Reusable team harnesses | +| Nested `workflow(name)` | Compose reusable pieces inside a parent script | + +## Common single-phase catalog + +| Name | Intent | +|---|---| +| Understand | Parallel readers → structured map | +| Design | Judge panel → scored synthesis | +| Review | Dimensions → find → adversarially verify | +| Research | Multi-modal sweep → deep-read → synthesize | +| Migrate | Discover → transform (worktree) → verify | + +## Next + +- [One level above / orchestration](./orchestration.md) +- [Use cases](./usecases.md) diff --git a/docs/dynamic-workflow/claude/limits.md b/docs/dynamic-workflow/claude/limits.md new file mode 100644 index 00000000..268dfb93 --- /dev/null +++ b/docs/dynamic-workflow/claude/limits.md @@ -0,0 +1,76 @@ +# Limits & sandbox + +Hard bounds and isolation properties of the Claude Code workflow engine (model-facing behavior). + +## Engine caps + +| Limit | Value | Behavior if exceeded | +|---|---|---| +| Concurrent `agent()` calls | `min(16, cpu_cores - 2)` **per workflow** | Excess **queue**; still complete | +| Lifetime agent count | **1000** per workflow run | Runaway-loop backstop | +| Items per `parallel` / `pipeline` call | **4096** | **Explicit error** (not silent truncate) | +| Script non-determinism | `Date.now` / `Math.random` / bare `new Date` | **Throw** | +| Nested `workflow()` depth | **1** | Nested call inside child **throws** | +| Token budget | User “+N” target if set | Further `agent()` **throws** when spent ≥ total | + +Queued concurrency means you can pass large work-lists safely; wall-clock still stretches when the queue is deep. + +## Soft guidelines (not hard engine caps) + +| Guideline | Source | +|---|---| +| Workflow size: small ≈ 5, medium ≈ 15, large ≈ 50, unrestricted | User `/config` workflow size guideline | +| Thoroughness vs brevity | Task wording (“any bugs” vs “thoroughly audit”) | + +The model should treat size guidelines as authoring policy unless the user explicitly overrides with scale language or ultracode. + +## Script sandbox + +| Available | Not available | +|---|---| +| Plain JS built-ins (`JSON`, `Math`, `Array`, …) | Node APIs, `require`, `process` | +| Injected primitives | Filesystem, network, subprocess from script | +| `agent` / nested `workflow` as escape hatches | Direct shell or edit from script | + +Side effects (file edits, network via tools, git) happen **inside subagents** under normal Claude Code tool permission policy — not as raw script I/O. + +## Worktree isolation (per agent) + +```js +await agent(prompt, { isolation: 'worktree' }) +``` + +| Property | Detail | +|---|---| +| Cost | ~200–500ms setup + disk **per agent** | +| Use when | Parallel **mutators** would conflict on one checkout | +| Cleanup | Auto-remove if worktree unchanged | +| Avoid when | Read-only work, single writer, or sequential mutators | + +This is **opt-in per `agent()`**, not a default for all workers. + +## Permission / product gates + +Separate from engine caps: + +- User must [opt in](./opt-in.md) (or enable ultracode) before `Workflow` is called. +- Script `meta.description` surfaces in the permission dialog. +- Individual agents may still hit tool permission prompts per session policy. + +## MCP caveats + +Workflow agents can use session-connected MCP tools via ToolSearch. **Interactively authenticated** MCP servers (e.g. browser login flows) may be absent in headless or cron-style runs. + +## Practical sizing + +| Ask | Rough shape | +|---|---| +| “Find any bugs” | Few finders, single-vote verify | +| “Thoroughly audit” | Larger finder pool, 3–5 vote adversarial pass, synthesis | +| Open-ended hunt + budget | `while (budget.total && budget.remaining() > …)` | +| Open-ended hunt, no budget | loop-until-dry with dry-round counter — not unbounded `while(true)` without exit | + +## Next + +- [Resume & journal](./resume.md) +- [Patterns](./patterns.md) diff --git a/docs/dynamic-workflow/claude/opt-in.md b/docs/dynamic-workflow/claude/opt-in.md new file mode 100644 index 00000000..6a4b6b00 --- /dev/null +++ b/docs/dynamic-workflow/claude/opt-in.md @@ -0,0 +1,67 @@ +# Opt-in & Ultracode + +Workflows can spawn dozens of agents and consume large token budgets. Claude Code therefore gates the `Workflow` tool behind **explicit user opt-in**. A task that would merely “benefit from parallelism” is not enough. + +## When Workflow is allowed + +The model may call `Workflow` only when at least one of these holds: + +1. **Keyword `ultracode` in the user prompt** + A system-reminder confirms it for that turn/session context. + +2. **Session ultracode is on** + Standing opt-in for every substantive task (see below). + +3. **User asks in their own words** for multi-agent orchestration + Examples: “use a workflow”, “run a workflow”, “fan out agents”, “orchestrate this with subagents”. + The ask must be in the **user’s** words — not inferred from task complexity. + +4. **A skill / slash command’s instructions** tell the model to call Workflow. + +5. **User asks for a specific named / saved workflow.** + +## When Workflow is not allowed + +Even if multi-agent would clearly help: + +- Silent model decision that “parallel is better.” +- Inferring opt-in from difficulty alone. +- “This would be faster with more agents” without user request. + +**Fallback:** + +- Use the single `Agent` tool for one-off subagents, or +- Briefly describe what a multi-agent workflow would do and rough cost, and ask. Mention the user can say “use a workflow” later to skip the ask. + +## Ultracode mode + +When a system-reminder confirms ultracode is **on**: + +| Rule | Behavior | +|---|---| +| Opt-in | Standing — every substantive task may/should use a workflow | +| Goal | Most exhaustive correct answer; token cost is not a constraint | +| Multi-phase | Often **several workflows in sequence** (understand → design → implement → review) so the coordinator stays in the loop between phases | +| Patterns | Prefer adversarial verify, multi-modal sweep, completeness critic, loop-until-dry where they fit | +| Solo exception | Pure conversation or trivial mechanical edits only | + +When ultracode is **off**, revert to the explicit opt-in list above. + +## Hybrid still applies + +Opt-in does not mean “dump the whole task into one script immediately.” + +1. Scout inline → discover work-list. +2. Author workflow over that list. +3. Synthesize; chain next phase if needed. + +## Cost and size guidelines + +Users may set a **workflow size guideline** in `/config` (e.g. small ≈ 5 agents, medium ≈ 15, large ≈ 50, unrestricted). This is a **guideline** for the model’s authoring behavior, not the same as the engine’s hard caps ([limits](./limits.md)). + +Budget directives (“+500k”-style) feed `budget.total` inside scripts — a **hard** ceiling on further `agent()` calls once spent. See [control-and-io](./control-and-io.md). + +## Next + +- [Workflow tool API](./workflow-tool.md) +- [Orchestration altitude](./orchestration.md) diff --git a/docs/dynamic-workflow/claude/orchestration.md b/docs/dynamic-workflow/claude/orchestration.md new file mode 100644 index 00000000..39c75d34 --- /dev/null +++ b/docs/dynamic-workflow/claude/orchestration.md @@ -0,0 +1,138 @@ +# One level above agents + +Classic multi-agent demos put several peers in a chat room and hope coordination emerges. Dynamic workflows **invert** that: + +> **Coordination is a program** written by a high-capability model. +> **Workers are replaceable execution units.** + +That is “one level above” ordinary agent tool use. + +## Split of responsibility + +### Orchestrator (main session) + +- Understand user intent and constraints +- Discover the work-list (files, bugs, modules, APIs) +- Choose pattern (pipeline vs barrier, depth vs breadth) +- Author the script, schemas, and worker prompts +- Allocate model / effort tiers per stage +- Interpret structured returns; decide the next phase +- Talk to the human; own the correctness narrative + +### Workers (`agent()`) + +- Execute one bounded prompt with tools +- Return raw data or schema-validated objects +- Stay isolated (optional worktree) +- Do **not** redesign the global plan +- May be cheaper/faster models for mechanical stages +- May be specialized `agentType`s (reviewer, explorer, …) + +### Engine + +- Run control flow faithfully +- Enforce concurrency, agent caps, budget hard stop +- Journal for resume +- Present progress UI + +## Why altitude matters + +| Problem in a flat agent loop | Workflow fix | +|---|---| +| Plan and tool churn share one context | Workers isolate tool churn; script aggregates returns only | +| Model “forgets” to verify | Verify is a stage in code | +| Fan-out is improvised each turn | Fan-out is `parallel` / `pipeline` | +| Hard to scale thoroughness | Scale fleet size, votes, dry rounds, budget | +| Parallel edits stomp each other | `isolation: 'worktree'` on mutators | + +``` +User intent + │ + ▼ +┌──────────────────────────────────────────┐ +│ Orchestrator model (main session) │ +│ · plans · schemas · phase selection │ +│ · Workflow({ script, args }) │ +└───────────────────┬──────────────────────┘ + │ deterministic JS spine + ┌───────────┼───────────┐ + ▼ ▼ ▼ + agent() agent() agent() + worker A worker B worker C + │ │ │ + └───────────┼───────────┘ + ▼ + structured returns + │ + ▼ + orchestrator synthesizes + │ + ▼ + user-facing answer +``` + +## Model tiering pattern + +Keep the **session model strong** for orchestration (authoring scripts, reading results, deciding phases). + +Inside the workflow: + +| Stage | Typical choice | +|---|---| +| Mechanical map / extract | `effort: 'low'`; optional smaller `model` | +| Default work | **Omit `model`** — inherit session model | +| Hard judges / design | higher `effort`; keep strong model | +| Parallel mutators | same model + `isolation: 'worktree'` | + +When unsure about `model`, **omit**. Wrong downgrades are worse than paying full price on a small fleet. + +## Structured handoffs + +The interface between altitude layers is **data**, not chat: + +```js +// worker → script +{ findings: [{ file, line, issue, severity }] } + +// script → orchestrator +{ confirmed, dropped, stats } + +// orchestrator → user +narrative + residual risk + links to paths +``` + +Schemas make handoffs machine-checkable. Prefer them at every stage boundary that feeds another stage. + +## What the orchestrator must not outsource + +- Final user-facing judgment (“is this safe to merge?”) without reading key evidence +- Opt-in / cost honesty +- Choosing silent truncation +- Replacing a missing verification stage with “the workers looked careful” + +Workers can be wrong in correlated ways; adversarial / diverse-lens patterns exist to fight that ([patterns](./patterns.md)). + +## Comparison: Agent tool vs Workflow altitude + +| | Single `Agent` | `Workflow` | +|---|---|---| +| Altitude | Peer subagent | Programmed fleet under orchestrator | +| Coordination | Prompt prose | Code | +| Resume multi-step graph | Weak | Prefix journal | +| Best for | One bounded digression | Structured multi-agent jobs | + +## Enabling “bigger brain, many hands” + +Dynamic workflows let you: + +1. Put the **expensive reasoning** in graph design and synthesis. +2. Put the **expensive tokens** in parallel worker contexts that do not pollute each other. +3. Put the **reliability** in deterministic stages (verify, majority, dry-stop). +4. Put the **human** at phase boundaries instead of inside every tool call. + +That is the product of the feature — not just “more agents.” + +## Next + +- [Use cases](./usecases.md) +- [Cheatsheet](./cheatsheet.md) diff --git a/docs/dynamic-workflow/claude/patterns.md b/docs/dynamic-workflow/claude/patterns.md new file mode 100644 index 00000000..42dc59ec --- /dev/null +++ b/docs/dynamic-workflow/claude/patterns.md @@ -0,0 +1,254 @@ +# Quality patterns + +These are **not** extra APIs. They are recipes composed from [`agent`](./agent.md), [`pipeline` / `parallel`](./concurrency.md), [`budget`](./control-and-io.md), and [`log`](./control-and-io.md). Pick by task; compose freely. + +## Scale to the ask + +| User language | Shape | +|---|---| +| “Find any bugs” | Few finders, single-vote verify | +| “Thoroughly audit” / “be comprehensive” | Larger finder pool, 3–5 vote adversarial pass, synthesis | +| Unsure on research/review/audit | Lean thorough | +| Quick check | Lean brief | + +--- + +## Adversarial verify + +Spawn N independent skeptics per claim, each prompted to **REFUTE**. Kill if ≥ majority refute. Prevents plausible-but-wrong findings from surviving. + +```js +const votes = await parallel( + Array.from({ length: 3 }, () => () => + agent( + `Try to refute: ${claim}. Default to refuted=true if uncertain.`, + { schema: VERDICT, phase: 'Verify', effort: 'high' } + ) + ) +) +const survives = + votes.filter(Boolean).filter(v => !v.refuted).length >= 2 +``` + +--- + +## Perspective-diverse verify + +When a finding can fail in more than one way, give each verifier a **distinct lens** (correctness, security, perf, does-it-reproduce) instead of N identical refuters. Diversity catches failure modes redundancy cannot. + +```js +const lenses = ['correctness', 'security', 'repro'] +const votes = await parallel( + lenses.map(lens => () => + agent(`Judge "${desc}" via the ${lens} lens — real?`, { + schema: VERDICT, + phase: 'Verify', + label: `judge:${lens}`, + }) + ) +) +const real = votes.filter(Boolean).filter(v => v.real).length >= 2 +``` + +--- + +## Judge panel (design) + +Generate N independent attempts from different angles (MVP-first, risk-first, user-first). Score with parallel judges. Synthesize from the winner while grafting best ideas from runners-up. Beats single-attempt iteration when the solution space is wide. + +```js +const ANGLES = ['mvp-first', 'risk-first', 'user-first'] +const drafts = await parallel( + ANGLES.map(a => () => + agent(`Propose a design (${a}). Constraints: ${constraints}`, { + schema: DESIGN_SCHEMA, + phase: 'Design', + label: `draft:${a}`, + }) + ) +).then(xs => xs.filter(Boolean)) + +const scored = await parallel( + drafts.map(d => () => + agent(`Score this design vs criteria…\n${JSON.stringify(d)}`, { + schema: SCORE_SCHEMA, + phase: 'Score', + }) + ) +).then(xs => xs.filter(Boolean)) + +const winner = pickWinner(drafts, scored) +const synthesis = await agent( + `Synthesize final design from winner + graft runners-up…`, + { schema: DESIGN_SCHEMA, phase: 'Synthesize', effort: 'high' } +) +return { winner, synthesis, runnersUp: drafts } +``` + +--- + +## Loop-until-dry + +Unknown-size discovery (bugs, issues, edge cases): keep spawning finders until **K consecutive rounds** return nothing new. Simple `while (count < N)` misses the tail. + +**Critical:** dedup against **all `seen`**, not only `confirmed`. If you only track confirmed, judge-rejected findings reappear every round and the loop never converges. + +```js +const seen = new Set() +const confirmed = [] +let dry = 0 + +while (dry < 2) { + const found = ( + await parallel( + FINDERS.map(f => () => + agent(f.prompt, { phase: 'Find', schema: BUGS }) + ) + ) + ) + .filter(Boolean) + .flatMap(r => r.bugs) + + const fresh = found.filter(b => !seen.has(key(b))) + if (!fresh.length) { + dry++ + log(`dry round ${dry}/2`) + continue + } + dry = 0 + fresh.forEach(b => seen.add(key(b))) + log(`${fresh.length} fresh findings (${seen.size} seen total)`) + + const judged = await parallel( + fresh.map(b => () => + parallel( + ['correctness', 'security', 'repro'].map(lens => () => + agent(`Judge "${b.desc}" via ${lens} — real?`, { + phase: 'Verify', + schema: VERDICT, + }) + ) + ).then(vs => ({ + b, + real: vs.filter(Boolean).filter(v => v.real).length >= 2, + })) + ) + ) + + confirmed.push(...judged.filter(v => v.real).map(v => v.b)) +} + +return confirmed +``` + +Combine with [budget](./control-and-io.md#budget) for cost-bounded open-ended hunts: + +```js +while (dry < 2 && budget.total && budget.remaining() > 50_000) { + // … +} +``` + +--- + +## Multi-modal sweep + +Parallel agents each search a **different way** (by-container, by-content, by-entity, by-time). Each is blind to what the others surface — covers angles one search cannot. + +```js +const MODES = [ + { key: 'by-path', prompt: 'Find X by directory layout…' }, + { key: 'by-symbol', prompt: 'Find X by type/symbol names…' }, + { key: 'by-test', prompt: 'Find X by failing or related tests…' }, + { key: 'by-history', prompt: 'Find X by recent git history…' }, +] + +const sweeps = await parallel( + MODES.map(m => () => + agent(m.prompt, { + phase: 'Sweep', + label: `sweep:${m.key}`, + schema: HITS_SCHEMA, + effort: 'low', + }) + ) +).then(xs => xs.filter(Boolean)) + +const merged = dedupe(sweeps.flatMap(s => s.hits)) +// then deep-read top hits, then synthesize +``` + +--- + +## Completeness critic + +A final agent asks what is missing — modality not run, claim unverified, source unread. Output becomes the next work round. + +```js +const gaps = await agent( + `Given work done:\n${JSON.stringify(summary)}\nWhat is missing?`, + { schema: GAPS_SCHEMA, phase: 'Critic', effort: 'medium' } +) +if (gaps.items.length) { + log(`critic found ${gaps.items.length} gaps`) + // feed gaps into another pipeline / loop iteration +} +``` + +--- + +## Self-repair implementation loop + +Encode a real engineering process as a script: + +```text +Implement → multi-reviewer parallel → repair from structured findings → verify gates +``` + +See the implement/review/repair/verify example in [script contract](./script-contract.md). + +Tips: + +- Reviewers **read-only**; repair agent owns writes. +- Structured `FINDINGS` schema forces actionable file/line/fix fields. +- Final verify re-runs typecheck/tests and reports residual risk. +- Resume after fixing only the repair prompt if implement+review were good. + +--- + +## Review pipeline (default multi-stage) + +Already detailed in [concurrency](./concurrency.md): + +```text +dimensions → (per dimension) findings → (per finding) adversarial verify +``` + +Use barrier+dedup only when verification must see the global merged set first. + +--- + +## No silent caps + +If you bound coverage: + +```js +const MAX = 40 +if (sites.length > MAX) { + log(`transforming ${MAX}/${sites.length} sites; remainder skipped`) +} +const batch = sites.slice(0, MAX) +``` + +Silent truncation reads as full coverage. + +--- + +## Compose novel harnesses + +The list is not exhaustive. Valid compositions include tournament brackets, staged escalation (cheap finder → expensive judge only on survivors), and multi-phase product delivery under ultracode ([lifecycle](./lifecycle.md)). + +## Next + +- [Lifecycle & UX](./lifecycle.md) +- [Orchestration altitude](./orchestration.md) diff --git a/docs/dynamic-workflow/claude/primitives.md b/docs/dynamic-workflow/claude/primitives.md new file mode 100644 index 00000000..c30fa49b --- /dev/null +++ b/docs/dynamic-workflow/claude/primitives.md @@ -0,0 +1,62 @@ +# Primitives overview + +The workflow script body is an async JS context with a **closed** API surface. Only these hooks are injected. Everything else is ordinary JavaScript (with [determinism bans](./script-contract.md)). + +## Inventory + +| Primitive | Kind | Role | +|---|---|---| +| [`agent`](./agent.md) | async call | Spawn one subagent; get string or schema-validated object | +| [`pipeline`](./concurrency.md#pipeline) | combinator | Per-item multi-stage fan-out **without** barriers | +| [`parallel`](./concurrency.md#parallel) | combinator | Concurrent thunks; **barrier** until all complete | +| [`phase`](./control-and-io.md#phase) | side effect | Start a progress group for following agents | +| [`log`](./control-and-io.md#log) | side effect | Narrator line in `/workflows` progress UI | +| [`args`](./control-and-io.md#args) | binding | `Workflow({ args })` value, verbatim | +| [`budget`](./control-and-io.md#budget) | binding | Shared turn token ceiling: `total`, `spent()`, `remaining()` | +| [`workflow`](./control-and-io.md#nested-workflow) | async call | Run one nested named/path workflow (max depth 1) | + +## How they compose + +``` +meta (literal header) + │ + ▼ +phase / log ─────────────────────────── UX only + │ + ├── agent ─────────────────────────── unit of model work + │ + ├── parallel([() => agent…, …]) ──── barrier fan-out + │ + ├── pipeline(items, s1, s2, …) ───── streaming multi-stage + │ └── stages may call agent / parallel + │ + ├── budget.* ──────────────────────── scale / stop loops + │ + └── workflow(name|path) ───────────── nested graph (1 level) +``` + +## Design rules (short) + +1. **Default multi-stage shape is `pipeline`**, not barrier-then-map. +2. Use **`parallel` only** when stage N needs the **full** stage N−1 result set. +3. Always **`.filter(Boolean)`** after `parallel` / nullable `agent` results. +4. Prefer **`schema`** on `agent` for structured returns — no JSON parse roulette. +5. **`log()`** anything a silent cap would hide (top-N, drops, early exit). +6. Guard budget loops with **`budget.total &&`** (else `remaining()` is `Infinity`). +7. Put identity for later stages in **`(prev, originalItem, index)`**, not only in stage-1 return blobs. + +## Not primitives (but matter) + +| Concern | Where documented | +|---|---| +| Tool launch API | [workflow-tool.md](./workflow-tool.md) | +| Script / meta rules | [script-contract.md](./script-contract.md) | +| Caps & isolation | [limits.md](./limits.md) | +| Resume cache | [resume.md](./resume.md) | +| Recipes | [patterns.md](./patterns.md) | + +## Next + +- [agent()](./agent.md) +- [pipeline & parallel](./concurrency.md) +- [phase, log, args, budget, workflow()](./control-and-io.md) diff --git a/docs/dynamic-workflow/claude/resume.md b/docs/dynamic-workflow/claude/resume.md new file mode 100644 index 00000000..d0734b99 --- /dev/null +++ b/docs/dynamic-workflow/claude/resume.md @@ -0,0 +1,94 @@ +# Resume & journal + +Dynamic workflows are editable programs. Resume lets you change the plan mid-flight (or after a kill) without redoing finished `agent()` work. + +## Handles returned at launch + +| Field | Use | +|---|---| +| `runId` | Pass as `resumeFromRunId` on the next `Workflow` call | +| `scriptPath` | Edit in place; re-invoke without resending full `script` | +| `transcriptDir` | Subagent transcripts + `journal.jsonl` | +| `taskId` | Stop / track the background task | + +## How to resume + +1. **Stop** the prior run if it is still running (background task stop / equivalent). +2. Relaunch: + +```js +Workflow({ + scriptPath: '/…/workflows/scripts/review-wf_abc.js', + resumeFromRunId: 'wf_abc…', + args: previousArgs, // keep identical for full cache when script unchanged +}) +``` + +Same-session only for `resumeFromRunId` (local runs). + +## Cache identity rule + +The engine finds the **longest unchanged prefix** of `agent()` calls: + +- Same **prompt** + same **opts** (as hashed for identity) → return **cached** result instantly. +- First **edited or new** `agent()` call and **everything after it** run live. + +| Scenario | Result | +|---|---| +| Same script + same `args` | ~100% cache hit | +| Edit only post-processing after the last `agent()` | Cache hit all agents; re-run pure JS tail | +| Change prompt of agent #3 of 10 | Agents 1–2 cached; 3–10 live | +| Insert a new `agent()` early | From that call onward live | + +## Why scripts ban entropy + +`Date.now()`, `Math.random()`, and bare `new Date()` throw in scripts so control flow and prompt construction cannot silently diverge between original run and resume. See [script contract](./script-contract.md). + +If you need wall-clock: + +- Pass a fixed ISO string via `args` at launch, or +- Stamp times in the coordinator after the workflow returns. + +## `journal.jsonl` + +Path: `/journal.jsonl` + +- Records each agent’s **actual return value**. +- Before diagnosing empty or surprising workflow results, **read the journal** — do not assume cached results are non-empty. +- Fallback if no journal: read `agent-.jsonl` files in the transcript directory and hand-author a continuation script. + +## Operational patterns + +### Fix a bad verify stage after a long review + +1. Leave review `agent()` prompts unchanged. +2. Edit only verify-stage prompts / schema in `scriptPath`. +3. Resume with same `args` → review results cache; verify re-runs. + +### Add a completeness-critic pass + +1. Append a new phase + `agent()` at the end of the script. +2. Resume → entire prior prefix caches; only the new agent runs. + +### Re-run pure aggregation + +1. Change only the `return` / merge logic (no `agent()` signature changes). +2. Resume → full agent cache; new aggregation. + +## Failure modes to watch + +| Symptom | Check | +|---|---| +| Empty confirmed list | Journal: did judges return `null`? schema fail? | +| Unexpected re-run of early agents | Prompt/opts drift (template changed, args differ) | +| Resume rejected / no cache | Wrong session, missing `runId`, prior run not stopped | +| Divergent args | Even with same script, different `args` can change prompts that embed `args` → cache miss from first embedded call | + +## Relation to durability + +Claude Code resume is **session-oriented prefix replay** of orchestration journals. It is not the same as a multi-day durable job supervisor with external leases (a different control plane). For long-lived external orchestration, see product-specific durable systems; this doc describes the model-facing Workflow resume API only. + +## Next + +- [Patterns](./patterns.md) +- [Lifecycle](./lifecycle.md) diff --git a/docs/dynamic-workflow/claude/script-contract.md b/docs/dynamic-workflow/claude/script-contract.md new file mode 100644 index 00000000..dfd64fbc --- /dev/null +++ b/docs/dynamic-workflow/claude/script-contract.md @@ -0,0 +1,145 @@ +# Script contract + +A workflow script is plain JavaScript that starts with a pure-literal `meta` export, then runs in an async context with only the injected orchestration primitives available. + +## Minimal shape + +```js +export const meta = { + name: 'find-flaky-tests', + description: 'Find flaky tests and propose fixes', // shown in permission dialog + phases: [ + { title: 'Scan', detail: 'grep test logs for retries' }, + { title: 'Fix', detail: 'one agent per flaky test', model: 'sonnet' }, + ], + // optional: whenToUse — shown in workflow lists +} + +// body — async context; await freely +phase('Scan') +const flaky = await agent('grep CI logs for retry markers', { schema: FLAKY_SCHEMA }) +// ... +return { flaky } +``` + +## `meta` rules + +| Rule | Detail | +|---|---| +| Position | Must be the **first statement** in the script | +| Purity | **Pure literal only** — no variables, function calls, spreads, or template interpolation | +| Required | `name`, `description` | +| Optional | `whenToUse`, `phases` | +| Phase entries | `{ title, detail?, model? }` | +| Phase titles | Must match `phase('…')` call strings **exactly** for UI grouping; unmatched `phase()` still gets its own progress group | +| Per-phase model | Optional override for agents in that phase’s UI group (agent-level `opts.model` still applies per call) | +| Permission UX | `description` is what the user sees in the approval dialog | + +Invalid example (not pure literal): + +```js +const n = 'review' +export const meta = { name: n, description: `Review ${topic}` } // ❌ +``` + +## Language + +| Allowed | Forbidden | +|---|---| +| Plain JavaScript | TypeScript annotations, interfaces, generics | +| `async` body with top-level `await` | Node APIs (`fs`, `process`, `require`, …) | +| `JSON`, `Math`, `Array`, `Object`, `Map`, `Set`, … | Filesystem, network, subprocess | +| Template strings / normal expressions in the **body** | Non-determinism listed below | + +Type annotations like `: string[]` **fail to parse**. Keep types in comments or in JSON Schema objects as plain data. + +## Determinism bans (resume safety) + +These throw if called in the script (argless / pure entropy): + +- `Date.now()` +- `Math.random()` +- argless `new Date()` + +**Why:** [Resume](./resume.md) replays the longest unchanged prefix of `agent()` calls by hashing prompt + options. If the script branched on wall-clock or random, cache identity would lie and partial replay would be unsafe. + +**What to do instead:** + +- Pass fixed timestamps / seeds via `args`. +- Stamp wall-clock **after** the workflow returns, in the coordinator. +- For “random-like” diversity among agents, vary **prompt text or label by index** (deterministic in the script, different per worker). + +## Only escape hatches into models + +From the script you can only: + +1. Call **`agent()`** — spawn a subagent (tools, optional schema). +2. Call **`workflow()`** — run one nested saved/path workflow (one level only). + +There is no raw shell, no write-file, no HTTP from the orchestration body. That is intentional: orchestration stays pure; side effects live inside agents under normal permission/tool policy. + +## Return value + +Whatever the script `return`s becomes the workflow result delivered to the coordinator (via task notification). Prefer structured objects: + +```js +return { confirmed, dropped, stats: { found: seen.size } } +``` + +Subagents should return **raw data** (or schema objects), not user essays — the coordinator narrates. + +## Real-world example (implement → review → repair → verify) + +Condensed from a session script: + +```js +export const meta = { + name: 'implement-workflow-foundation', + description: 'Implement and verify durable workflow foundation', + phases: [ + { title: 'Implement', detail: 'build store and orchestrator', model: 'sonnet' }, + { title: 'Review', detail: 'audit correctness and tests' }, + { title: 'Repair', detail: 'apply verified fixes', model: 'sonnet' }, + { title: 'Verify', detail: 'run full validation' }, + ], +} + +phase('Implement') +const implementation = await agent(`…implementation prompt…`, { + label: 'implement:durable-foundation', + phase: 'Implement', + effort: 'medium', + agentType: 'claude', +}) + +phase('Review') +const FINDINGS = { /* JSON Schema */ } +const reviews = await parallel([ + () => agent(`…persistence audit…\n${implementation}`, { + label: 'review:persistence', phase: 'Review', schema: FINDINGS, effort: 'medium', agentType: 'claude', + }), + () => agent(`…correctness audit…\n${implementation}`, { + label: 'review:correctness', phase: 'Review', schema: FINDINGS, effort: 'medium', agentType: 'claude', + }), + () => agent(`…test quality audit…\n${implementation}`, { + label: 'review:tests', phase: 'Review', schema: FINDINGS, effort: 'low', agentType: 'claude', + }), +]).then(xs => xs.filter(Boolean)) + +phase('Repair') +const repair = await agent(`…fix from ${JSON.stringify(reviews)}…`, { + label: 'repair:review-findings', phase: 'Repair', effort: 'medium', agentType: 'claude', +}) + +phase('Verify') +const verification = await agent(`…gates…`, { + label: 'verify:full-gates', phase: 'Verify', effort: 'low', agentType: 'claude', +}) + +return { implementation, reviews, repair, verification } +``` + +## Next + +- [Primitives overview](./primitives.md) +- [agent()](./agent.md) diff --git a/docs/dynamic-workflow/claude/usecases.md b/docs/dynamic-workflow/claude/usecases.md new file mode 100644 index 00000000..c6735f5d --- /dev/null +++ b/docs/dynamic-workflow/claude/usecases.md @@ -0,0 +1,180 @@ +# Use cases + +Workloads that were awkward or unreliable as a single flat agent loop, and how dynamic workflows fit them. Pair with [patterns](./patterns.md) and [orchestration](./orchestration.md). + +## Comprehensive code review + +**Goal:** High confidence that findings are real before the user acts. + +**Shape:** + +```text +scout diff → dimensions (security, correctness, tests, perf) + → (pipeline) per-dimension findings + → adversarial / multi-lens verify per finding + → return survivors only +``` + +**Why workflow:** Verification is not optional prose — it is stages. Vote count scales with “thoroughly audit” vs “any issues.” + +**Primitives:** `pipeline`, `parallel`, `schema`, higher `effort` on judges. + +--- + +## Large migrations / refactors + +**Goal:** Touch many call sites without stomping edits or losing progress. + +**Shape:** + +```text +discover sites → pipeline(site → transform → local verify) + isolation: 'worktree' on mutators + resume after fixing one stage’s prompt +``` + +**Why workflow:** One context cannot hold hundreds of site-specific tool traces. Prefix resume avoids redoing finished sites when the transform prompt improves. + +**Primitives:** `pipeline`, `isolation: 'worktree'`, `resumeFromRunId`, `log` for skipped tails. + +--- + +## Research & multi-source synthesis + +**Goal:** Broad coverage then deep reading then a cited synthesis. + +**Shape:** + +```text +multi-modal sweep (parallel angles) + → merge/dedup hits + → deep-read top sources (pipeline) + → completeness critic + → synthesize +``` + +**Why workflow:** Sweeps are embarrassingly parallel; synthesis needs the merged set (barrier). Budget bounds open-ended browsing. + +**Primitives:** `parallel`, barrier merge, `budget`, critic `agent`. + +--- + +## Design exploration + +**Goal:** Explore a wide solution space without anchoring on the first idea. + +**Shape:** + +```text +N drafts from different angles (parallel) + → score panel (parallel) + → synthesize winner + graft runners-up +``` + +**Why workflow:** Single-thread iteration biases early. Independent drafts + structured scores beat one long chat. + +**Primitives:** judge panel pattern, `schema` for design objects, high effort on synthesis. + +--- + +## Unknown-size bug / issue hunts + +**Goal:** Keep finding until the map is dry, not until an arbitrary count. + +**Shape:** + +```text +loop-until-dry: + parallel finders → dedup vs seen → multi-lens judge → accumulate confirmed +``` + +**Why workflow:** `while (n < 10)` misses the tail; dry rounds + `seen` set converge. Budget optional hard stop. + +**Primitives:** loops, `parallel`, `Set` dedup, `budget.total && …`. + +--- + +## Self-repair implementation + +**Goal:** Ship a change with independent review pressure, not self-congratulation. + +**Shape:** + +```text +implement → parallel reviewers (schema findings) + → repair agent applies real issues + → verify gates (typecheck/tests) +``` + +**Why workflow:** Separation of implementer and reviewers; structured findings; deterministic phase order. + +**Primitives:** sequential `phase`s, `parallel` reviewers, schema, medium/low effort mix. + +**Example skeleton:** [script contract](./script-contract.md). + +--- + +## Heterogeneous agent fleets + +**Goal:** Specialists for map / edit / audit under one plan. + +**Shape:** + +```text +explorer agentType (read-only map) + → implementer agentType (edits, maybe worktree) + → reviewer agentType (schema audit) +``` + +**Why workflow:** `agentType` + model/effort per stage without the user manually jockeying three chats. + +**Primitives:** `agentType`, `model`/`effort` overrides, nested `workflow` for reusable specialist packs. + +--- + +## Phased product delivery under ultracode + +**Goal:** Maximum exhaustiveness for multi-day product work with human checkpoints. + +**Shape:** + +```text +turn 1: Understand workflow +turn 2: Design workflow +turn 3: Implement+repair workflow +turn 4: Review/audit workflow +``` + +**Why workflow:** Standing opt-in; each workflow is a well-scoped fan-out; coordinator synthesizes between turns. + +**Primitives:** full stack + [lifecycle](./lifecycle.md) multi-phase. + +--- + +## What this feature deliberately is not + +| Not | Because | +|---|---| +| Free-form multi-agent chat room | Workers do not negotiate the plan with each other | +| Silent always-on multi-agent | Cost; requires [opt-in](./opt-in.md) / ultracode | +| Multi-day durable external job system | Resume is session-oriented prefix replay, not external leases | +| Replacement for small tasks | Overhead of scripting + fleet is real; use single Agent or inline tools | + +--- + +## Choosing a shape quickly + +| Symptom | Reach for | +|---|---| +| Many independent units | `pipeline` or `parallel` fan-out | +| “I’m not sure we covered it” | multi-modal sweep + completeness critic | +| “Findings feel flaky” | adversarial / multi-lens verify | +| “Solution space is wide” | judge panel | +| “Don’t know how many exist” | loop-until-dry | +| “Parallel edits conflict” | `isolation: 'worktree'` | +| “Reran everything after a prompt tweak” | `resumeFromRunId` + stable prefix | + +## Next + +- [Cheatsheet](./cheatsheet.md) +- [README index](./README.md) diff --git a/docs/dynamic-workflow/claude/workflow-tool.md b/docs/dynamic-workflow/claude/workflow-tool.md new file mode 100644 index 00000000..b43a40ee --- /dev/null +++ b/docs/dynamic-workflow/claude/workflow-tool.md @@ -0,0 +1,139 @@ +# Workflow tool API + +The model-facing tool name is **`Workflow`** (alias **`RunWorkflow`**). + +- **Search hint:** orchestrate subagents with deterministic JavaScript workflow +- **Execution:** background — tool returns immediately with a task id +- **Completion:** `` when the script finishes +- **Live progress:** `/workflows` + +## When to use the tool (product intent) + +A workflow structures work across many agents to be: + +- **Comprehensive** — decompose and cover in parallel +- **Confident** — independent perspectives and adversarial checks before committing +- **Scalable** — migrations, audits, broad sweeps that one context cannot hold + +The script encodes structure: what fans out, what verifies, what synthesizes. + +Control flow should be **deterministic** (loops, conditionals, fan-out in code) rather than re-decided free-form by the model mid-orchestration. + +Common single-phase shapes (chain across turns for larger work): + +| Phase | Pattern | +|---|---| +| Understand | parallel readers over subsystems → structured map | +| Design | judge panel of N approaches → scored synthesis | +| Review | dimensions → find → adversarially verify | +| Research | multi-modal sweep → deep-read → synthesize | +| Migrate | discover sites → transform (worktree) → verify | + +See [opt-in](./opt-in.md) for permission to call this tool. + +## Input fields + +At least one of `script`, `name`, or `scriptPath` is required. + +| Field | Type | Role | +|---|---|---| +| `script` | string (optional, length-bounded) | Inline self-contained workflow script. Must begin with pure-literal `export const meta = { name, description, phases }`. **Preferred on first invocation** — do not Write a file first. | +| `name` | string (optional) | Predefined workflow: built-in or from `.claude/workflows/`. Resolves to a full script. | +| `scriptPath` | string (optional) | Path to a script on disk. Every invocation **persists** its script under the session directory and returns the path. Iterate with Write/Edit + re-invoke. **Takes precedence** over `script` and `name`. | +| `args` | any (optional) | Exposed to the script as global `args`, **verbatim**. Pass real JSON arrays/objects — **not** a JSON-encoded string (stringified lists break `args.map` / `args.filter`). | +| `resumeFromRunId` | string `^wf_[a-z0-9-]{6,}$` (optional) | Prior run id. Unchanged prefix of `agent()` calls replays from cache; first edited/new call and everything after runs live. Same-session only. **Stop the prior run first** before resuming. | +| `description` | string (optional) | **Ignored** — set description in script `meta`. | +| `title` | string (optional) | **Ignored** — set title/name in script `meta`. | + +### First run + +```js +Workflow({ + script: ` +export const meta = { + name: 'review-changes', + description: 'Review and adversarially verify findings', + phases: [ + { title: 'Review' }, + { title: 'Verify' }, + ], +} +// ... body using agent/pipeline/parallel ... +return { confirmed } +`, + args: { files: ['src/auth.ts', 'src/session.ts'] }, +}) +``` + +### Iterate without resending the full script + +```js +// Edit the returned scriptPath via Write/Edit, then: +Workflow({ + scriptPath: returnedScriptPath, + resumeFromRunId: runId, // optional: reuse cached agent() prefix + args: { files: ['src/auth.ts', 'src/session.ts'] }, +}) +``` + +### Named workflow + +```js +Workflow({ + name: 'review-changes', + args: { topic: 'authentication' }, +}) +``` + +## Return envelope (conceptual) + +The tool launches asynchronously. A typical success-shaped result includes: + +```ts +{ + status: 'async_launched' | 'remote_launched', + taskId: string, + taskType?: 'local_workflow' | 'remote_agent', + workflowName?: string, // meta.name + runId?: string, // for resumeFromRunId (local) + transcriptDir?: string, // subagent transcripts + journal.jsonl + scriptPath?: string, // persisted script for this invocation + summary?: string, + sessionUrl?: string, // when remote_launched + warning?: string, // non-blocking heads-up + error?: string, // e.g. syntax check failed +} +``` + +Notes: + +- `runId` is the handle for [resume](./resume.md). +- `scriptPath` is the handle for iteration without resending `script`. +- `transcriptDir` holds per-agent logs and `journal.jsonl` (actual agent return values). +- Remote launches may use `sessionUrl` instead of local `runId` as the resume handle. + +## Resolution order (engine behavior) + +Conceptually the engine resolves input as: + +1. If `scriptPath` → load (and optionally pair with inline `script` for built-in match checks). +2. Else if `name` → resolve from built-ins / `.claude/workflows/`. +3. Else if `script` → use inline body. +4. Else → validation error: must provide script, name, or scriptPath. + +## Relationship to the single Agent tool + +| | `Agent` tool | `Workflow` tool | +|---|---|---| +| Count | One subagent (or a few manual launches) | Many, under a script graph | +| Control flow | Model re-decides each turn | Script encodes loops/fan-out | +| Structured multi-stage | Manual | `pipeline` / `parallel` + schema | +| Cost risk | Lower | Higher — gated by opt-in | +| Resume of a multi-step graph | Limited | Prefix-cached by agent call identity | + +Use `Agent` for isolated one-offs. Use `Workflow` when the **structure** of multi-agent work must be reliable. + +## Next + +- [Script contract](./script-contract.md) +- [Primitives](./primitives.md) From a7c95ec0655f706b3f13eb85fbf12d26639a004f Mon Sep 17 00:00:00 2001 From: Waishnav Date: Tue, 21 Jul 2026 12:10:43 +0530 Subject: [PATCH 2/6] docs: devspace plan --- docs/dynamic-workflow/devspace/plan.md | 369 ++++++++ .../devspace/primitives-spec.md | 864 ++++++++++++++++++ 2 files changed, 1233 insertions(+) create mode 100644 docs/dynamic-workflow/devspace/plan.md create mode 100644 docs/dynamic-workflow/devspace/primitives-spec.md diff --git a/docs/dynamic-workflow/devspace/plan.md b/docs/dynamic-workflow/devspace/plan.md new file mode 100644 index 00000000..2c13780c --- /dev/null +++ b/docs/dynamic-workflow/devspace/plan.md @@ -0,0 +1,369 @@ +# DevSpace Dynamic Workflow Engine — Plan + +Builds on the locked bigger-model plan. Scope = **this worktree only**. +Subagents stay **CLI-only**. Workflows get **CLI + MCP** over shared primitives. + +--- + +## 0. Non-goals / locks + +| Lock | Meaning | +|---|---| +| No MCP `agent_run` / `agent_wait` / `agent_show` | Subagent feature surface remains `devspace agents *` (+ skill + shell). | +| Workflow workers call adapters **in-process** | `runLocalAgentProvider` / same registry as CLI worker. No shell-out to `agents run` for `agent()`. | +| No dashboard v1 | Events via store drain + CLI `--follow` / MCP status long-poll. | +| CC script API parity | `meta`, `agent`, `parallel`, `pipeline`, `phase`, `log`, `args`, `budget`, `workflow` + determinism bans. | +| Yolo sub-agents | Fixed write-capable adapter policy; **no** `writeMode` on `agent()`. | +| `isolation: 'worktree'` | **Must-have** on `agent()` (CC-like); default shared checkout. | +| `effort` (not `thinking`) | Profiles, CLI, store, adapters, `agent()` opts — rename across stack. | +| `budget` stub v1 | `{ total: null, spent: () => 0, remaining: () => Infinity }`. | +| Dual surface | `devspace workflow *` **and** MCP `run_workflow` / `workflow_status` / `workflow_cancel`. | +| All 6 providers v1 | codex/claude/opencode/pi/cursor/copilot via existing adapters. | +| `agentProviders.enabled` | Ordered config from onboarding; default provider = first enabled ∩ live. | +| Resume-by-replay right after engine core | Same milestone order as locked plan. | + +--- + +## 1. Control planes (do not conflate) + +``` +A) One-shot subagents (existing, unchanged API) + host/shell → devspace agents run|show|ls + → detached __worker → adapters → local_agent_sessions + +B) Dynamic workflows (new) + host MCP / CLI → run row + spawn workflow __worker + → sandboxed script + → agent() → adapters (in-process) + → workflow_* tables (not local_agent_sessions) +``` + +**Implication:** `devspace agents ls` does **not** list workflow-spawned agents. Observability = workflow events + `workflow_agent_calls`. Optional later dual-write — not v1. + +--- + +## 2. Architecture + +``` +┌─ CLI: workflow run|status|cancel|ls ─┐ ┌─ MCP: run_workflow|status|cancel ─┐ +│ parse / create run / spawn │ │ same primitives via workflow-tools │ +└──────────────────┬───────────────────┘ └──────────────────┬────────────────┘ + ▼ │ + WorkflowStore (SQLite WAL) ◄────────────────────────┘ + │ + │ detached: node cli.js workflow __worker + ▼ + workflow-engine + sandbox + api + │ + │ agent() [semaphore] + ▼ + runLocalAgentProvider(provider, input) ← existing adapters + │ + ▼ + journal: events + agent_calls (+ schema retries) +``` + +Server/CLI = **launcher + journal reader**. Worker owns execution, heartbeat, cancel watch, self group-kill. + +--- + +## 3. Accept bigger plan as-is (core) + +Keep their file split (flat `src/`): + +| Module | Role | +|---|---| +| `workflow-script.ts` | meta extract + wrap + `vm.Script` | +| `workflow-sandbox.ts` | context, determinism bans, console→log | +| `workflow-store.ts` | runs / events / agent_calls / cancel / reap | +| `workflow-api.ts` | agent/parallel/pipeline/phase/log/args/budget/workflow + semaphore | +| `workflow-engine.ts` | execute + `__worker` guts | +| `workflow-replay.ts` | resume cache | +| `workflow-schema.ts` | Ajv + retries | +| `workflow-files.ts` | named + persist scriptPath | +| `workflow-tools.ts` | MCP registration | +| `skills/dynamic-workflows/SKILL.md` | teaching | + +DB migration **v4** (v3 = `local_agent_sessions` ✓). +Tables: `workflow_runs`, `workflow_events`, `workflow_agent_calls` as specified. +Spawn pattern copy `spawnAgentWorker` (detached, stdio ignore, unref). + +API semantics: keep their CC-parity table (throws vs parallel→null, pipeline stages, ALS for phase, nested workflow depth 1, budget stub). + +MCP contracts + yield windows: keep (status max ~110s matches `MAX_POLL_YIELD_MS`). + +Milestones 1→8: keep order and verifiability. + +--- + +## 4. Refinements / deltas on the bigger plan + +### 4.1 Explicit separation from subagent CLI + +In SKILL + serverInstructions + tool descriptions: + +- Workflows = multi-agent **graphs**. +- One-off second opinions = still `devspace agents run` (CLI/skill). +- Do **not** tell models to implement workflows by shelling many `agents run` when `run_workflow` exists. + +### 4.2 `agent()` backend = adapters, not CLI + +```ts +// conceptual +runProvider({ provider, prompt, workspace, model, effort, providerSessionId? }) + → runLocalAgentProvider(provider, { prompt, workspace, writeMode: "allowed", model, effort, providerSessionId? }) +``` + +- Schema retries reuse `providerSessionId` when adapter returns it (codex/claude path). +- Do not create `local_agent_sessions` rows per call (avoids polluting `agents ls`, simpler cancel). +- If product later wants unified list, add a flag — not v1. +- `workspace` is either shared `workspaceRoot` or a managed worktree path when `opts.isolation === 'worktree'`. + +### 4.3 Provider resolution + config + +Config add (see primitives-spec §3): + +```ts +agentProviders?: { + enabled: AgentProviderId[] // order = preference; [0] = default + detectedAt?: string + lastProbe?: Array<{ id, available, detail? }> +} +``` + +Algorithm: `opts.provider` → `meta.defaultProvider` → first of `enabled ∩ liveAvailable`. +Missing `agentProviders` → compat all-available in code order. +Onboarding (`init`/`doctor`) probes PATH and writes `enabled`. +Unknown/unavailable: fail that `agent()` (throw → parallel null). + +### 4.4 Skills gating fix (required, not optional) + +Today `effectiveSkillPaths` drops **entire** bundled dir if user has seeded `subagent-delegation` — hides any new bundled skill. + +v1: include bundled **per-skill** (user copy wins on name collision). Seed `dynamic-workflows` in `user-config` next to subagent skill. + +### 4.5 MCP vs CLI symmetry + +| Op | CLI | MCP | +|---|---|---| +| Start | `workflow run --file\|--name\|--resume` | `run_workflow` | +| Poll | `status --follow` | `workflow_status` long-poll | +| Cancel | `cancel` | `workflow_cancel` | +| List | `ls` | (optional later; status by id enough v1) | + +Same store. Detached worker survives MCP session death (critical acceptance test). + +### 4.6 Replay: document deliberate CC divergence + +CC: longest unchanged **call-index** prefix. +v1: index+key, then **consume-once cacheKey** fallback (fan-out completion order). + +Document in SKILL under Resume. Do not pretend full CC resume identity. + +### 4.7 Sandbox choice + +Locked: `node:vm` + shadow Date/Math + no require/process/fetch/timers. +Host wall-clock max (default 6h). +Not SES (not in this tree; avoid new heavy dep). Accept vm is not a security boundary for hostile multi-tenant — DevSpace is single-user local. + +### 4.8 Cancel / kill + +1. `cancelRequested` flag. +2. Worker heartbeat (5s) → AbortController + journal `run_cancelled` + group SIGTERM. +3. Hard path after ≤5s: `terminateProcessTree` pid shim (existing `process-platform`). + +Known: in-flight adapter SDKs may not abort cleanly; group-kill is the backstop (already accepted). + +### 4.9 Pi timeout + +Document `PI_AGENT_TIMEOUT_MS = 120_000` in SKILL. Follow-up: make configurable — not milestone blocker. + +### 4.10 Script authoring feedback + +`run_workflow` / CLI parse **before** spawn. Syntax/meta errors return cheat-sheet snippet (tool desc + error). Line numbers preserved via export-strip + lineOffset. + +### 4.11 Concurrency + +`min(16, max(1, os.availableParallelism()-2))`, clamp by `meta.concurrency` if set. Semaphore gates **`agent()` only** (not pure JS stages). + +### 4.12 Named workflows paths + +1. `/.devspace/workflows/.js` +2. `~/.devspace/workflows/.js` (via config dir helper used by profiles) + +Name: `[a-z0-9-]+`. Persist exact source to `/workflows/runs/.js` for resume/edit. + +### 4.13 `workflow()` nest + +Same run, shared journal/semaphore/call counter, depth ≤ 1. Resolve name via `workflow-files`. No new process. + +### 4.14 package.json + +- Direct dep: `ajv` +- Tests: append new `*.test.ts` to existing per-file tsx chain +- Node engines already `>=22.19` (ok for `availableParallelism`) + +### 4.15 Docs location + +Keep design notes under `docs/dynamic-workflow/devspace/` (this plan + later runtime notes). Claude reference stays under `docs/dynamic-workflow/claude/`. + +### 4.16 `effort` rename (profiles + agent stack) + +| Today | Target | +|---|---| +| Profile `thinking:` | `effort:` | +| CLI `--thinking` | `--effort` (+ short deprecation alias optional) | +| DB/store `thinking` | `effort` (rename column in new mig or dual-read) | +| `LocalAgentRunInput.thinking` | `effort` | +| Workflow `agent()` opts | `effort` only | +| Replay cache key | includes `effort` | + +Provider-native strings pass through unchanged. + +### 4.17 `isolation: 'worktree'` (must-have) + +- Opt-in per call: `agent(prompt, { isolation: 'worktree', … })`. +- Default: shared `workspaceRoot`. +- Create under `config.worktreeRoot` / existing git-worktrees helpers; pin base SHA at run start. +- Adapter `cwd` = worktree path. +- Clean success → auto-remove; dirty/fail/cancel → preserve + journal `worktreePath`. +- **No** auto-merge into source. +- Non-git workspace → throw. +- Cache key includes `isolation`. +- Module touch: extend `workflow-api` + small worktree helper (wrap `git-worktrees.ts`). +- Skill: use for parallel mutators only. + +### 4.18 Milestone impact + +| Milestone | Extra | +|---|---| +| **3 Engine** | `isolation` path with fake/temp git repos in tests | +| **4 Worker+CLI** | real worktree create/cleanup; journal fields | +| **5 Resume** | cache key includes isolation | +| **8 Teach** | skill isolation + effort; seed `agentProviders` docs | +| Cross-cutting | rename `thinking`→`effort` in profile/CLI/store/adapters (can land with M3–4) | +| Config | `agentProviders` on user-config + init probe (with M4 or M8) | + +--- + +## 5. Script API (v1 contract — implement exactly) + +```js +export const meta = { + name: '…', + description: '…', + phases: [{ title: '…', detail?: '…' }], + // devspace-only: + defaultProvider?: 'codex'|'claude'|…, + concurrency?: number, +} + +phase('Review') +const rows = await parallel([ + () => agent(p1, { provider: 'claude', label: 'r1', effort: 'high', schema: S }), + () => agent(p2, { provider: 'codex', label: 'r2', schema: S }), +]) +const mut = await agent(implPrompt, { + provider: 'codex', + isolation: 'worktree', // parallel-safe writes + schema: DiffSummary, +}) +const out = await pipeline(items, stage1, stage2) +log('…') +// args, budget (stub), workflow(name, args?) +return { … } +``` + +Determinism bans: `Date.now`, `Math.random`, argless `new Date` → `WorkflowDeterminismError`. + +--- + +## 6. Milestones (same spine, sharper exit criteria) + +| # | Deliverable | Done when | +|---|---|---| +| **1 Journal** | schema + mig v4 + store + tests | create/append/drain/reap unit green | +| **2 Script/sandbox** | parse + vm + bans | meta edge cases + line nos + bans green | +| **3 Engine core** | api+engine, fake provider | semaphore, parallel null, pipeline no-barrier, phase ALS, nest depth | +| **4 Worker+CLI** | router, spawn, heartbeat, cancel, files | `--follow` log-only + 1 real provider; kill -9 → reap; cancel → group empty | +| **5 Resume** | replay + `--resume` | cancel mid-run; resume shows cached prefix events | +| **6 Schema** | ajv enforce + retries | bad JSON → schema_retry → success/exhaust | +| **7 MCP** | 3 tools + server wiring | Inspector: run+status; **kill MCP, worker still finishes** | +| **8 Teach** | skill, seed, skills.ts fix, instructions | fresh + pre-seeded config both advertise skill | + +E2E: `npm test` + `npm run typecheck`; live fan-out 2 providers CLI; same MCP; cancel+resume. + +--- + +## 7. Mapping to existing code (touch list) + +| Existing | Use | +|---|---| +| `local-agent-adapters.ts` / `runLocalAgentProvider` | `agent()` backend | +| `local-agent-availability.ts` | provider pick / error text | +| `local-agent-store.ts` | **pattern only** (not dual-write) | +| `cli.ts` `spawnAgentWorker` / `agents __worker` | copy for `workflow __worker` | +| `process-platform.terminateProcessTree` | hard cancel | +| `db/client` WAL + busy_timeout 5000 | multi-process journal | +| `server.ts` `registerAppTool` + `config.subagents` gate | tools only if subagents on | +| `skills.ts` / `user-config.ts` | gate fix + seed | +| `process-sessions` yield bounds | MCP status yield caps | + +--- + +## 8. Risk register (accepted + one process risk) + +| Risk | Mitigation | +|---|---| +| Adapter no abort | group-kill worker | +| Daemonizing child escapes group | document; SIGTERM+adapter finally | +| Pi 120s cap | SKILL note | +| Replay key fallback ≠ CC | document | +| Laptop sleep heartbeat false fail | `kill(pid,0)` before reap | +| Host model still shells `agents run` for graphs | skill + tool cheat-sheet steer to `run_workflow` | +| Long MCP poll vs proxy timeouts | yield ≤110s; client re-calls status | + +--- + +## 9. What we explicitly do **not** build in v1 + +- MCP tools for raw subagents +- Dashboard / live TUI +- Real token `budget` tied to host +- `writeMode` on `agent()` (isolation **is** in scope) +- Auto-merge of agent worktrees into source checkout +- Auto file-change / diff events per stage +- Declaring DAG JSON alternate API (script is the API) +- Dual-write to `local_agent_sessions` +- SES lockdown + +--- + +## 10. Implementation order for a coding agent + +1. Mig + store (no behavior risk). +2. Script + sandbox (pure). +3. Engine against fakes (locks API). +4. Wire CLI worker to real adapters. +5. Replay. +6. Schema. +7. MCP. +8. Skill/docs/gating. + +Do not open MCP before CLI smoke — debug path must work headless without a host. + +--- + +## Resolved questions (see also [primitives-spec.md](./primitives-spec.md)) + +1. **Default provider:** `opts.provider` → `meta.defaultProvider` → first of **onboarding-configured** `agentProviders.enabled` ∩ live available. Full config schema in primitives-spec §3. +2. **writeMode:** **not in v1 API**; skill teaches prompt-based RO/write. +3. **Isolation:** **`isolation?: 'worktree'` is v1 must-have** on `agent()`; default shared; no auto-merge. +4. **Effort rename:** `thinking` → **`effort`** across profiles, CLI, store, adapters, `agent()` opts, cache keys. +5. **MCP list:** skip; **CLI** `workflow ls` yes. +6. **Size caps:** transport/storage bounds (§8 of primitives-spec); not “coverage” truncation. +7. **Nested workflow:** CC-like `name | { scriptPath }`, depth 1, shared journal/semaphore. +8. **Cancel:** cooperative flag → then group-kill. + +**File-change tracking:** out of scope. +**Schema:** `opts.schema` + Ajv + retries — in scope. \ No newline at end of file diff --git a/docs/dynamic-workflow/devspace/primitives-spec.md b/docs/dynamic-workflow/devspace/primitives-spec.md new file mode 100644 index 00000000..ef53d596 --- /dev/null +++ b/docs/dynamic-workflow/devspace/primitives-spec.md @@ -0,0 +1,864 @@ +# DevSpace Dynamic Workflow — Primitives & API Spec + +Implementation + contract spec for every surface, inspired by Claude Code’s Workflow environment. +Pairs with [plan.md](./plan.md). Subagents remain CLI-only; this document is **workflow only**. + +--- + +## 0. Product goals (locks) + +| Goal | Surface | +|---|---| +| DW for coding agents that lack Workflow (pi, codex, opencode, cursor, …) | **CLI + skill** — host agent authors script, runs `devspace workflow *` | +| ChatGPT as orchestrator, not implementer | **MCP workflow tools** (togglable with subagents) — plan + `run_workflow` / status / cancel | +| Ship both in dev | One engine; two entrypoints; converge later on performance/UX | + +``` +coding agent ── skill + CLI ──► engine ── agent() ──► adapters +ChatGPT ── MCP tools ──► engine ── agent() ──► adapters +``` + +--- + +## 1. Resolved decisions + +| # | Topic | Decision | +|---|---|---| +| 1 | Default provider | **Configured enabled provider list** (onboarding auto-detect CLIs → `config.json`). Runtime: `opts.provider` → `meta.defaultProvider` → **first entry of enabled+available list**. | +| 2 | Access / writeMode | **Not in v1 API.** No `writeMode`. Skill teaches **prompt-based** RO vs write. Isolation handles *where* writes land (see isolation). | +| 3 | List runs | **No MCP list tool v1.** **CLI** `devspace workflow ls` yes. | +| 4 | Size caps | Soft/hard bounds on journal + results (§8). | +| 5 | Nested `workflow()` | CC-inspired: `name \| { scriptPath }`, depth 1, shared journal/semaphore (§7.8). | +| 6 | Cancel | Cooperative flag → worker abort → hard `terminateProcessTree` (§9). | +| 7 | **`effort` rename** | Profile frontmatter, CLI (`--effort`), store column, runtime input, and `agent()` opts use **`effort`** (not `thinking`). Adapters map `effort` → provider-native flags. | +| 8 | **`isolation`** | **Must-have v1** on `agent()`: `opts.isolation?: 'worktree'`. Shared checkout default; worktree when set (§7.1, §7.1b). | +| — | File-change tracking | Out of scope. Shared disk / worktree is truth; no auto per-stage diff. | +| — | Structured output | **In scope:** `opts.schema` + Ajv + retries (§7.1). | + +--- + +## 2. Claude Code inspiration map + +| CC concept | CC behavior (model-facing) | DevSpace v1 | +|---|---|---| +| `Workflow` tool | Host tool; async; script/name/scriptPath/args/resume | CLI `workflow run` + MCP `run_workflow` | +| `export const meta` | Pure literal; name, description, phases | Same + optional `defaultProvider`, `concurrency` | +| `agent(prompt, opts)` | Spawn worker; string or schema object; null on skip/death in combinators | Same return contract; **throw** on failure; `parallel` → null | +| `opts.schema` | StructuredOutput / validated object | Ajv enforce + retry in engine | +| `opts.model` / `effort` | Tier/effort overrides | `model` + **`effort`** (renamed from `thinking`; provider passthrough) | +| `opts.isolation: 'worktree'` | Per-agent worktree | **v1 must-have** — same semantics, DevSpace-managed worktrees | +| Access / sandbox | Session permission mode; not `writeMode` on agent() | Prompt RO/write + **isolation for write containment** | +| `pipeline` | No barrier; per-item chains | Same | +| `parallel` | Barrier; null slots | Same | +| `phase` / `log` | Progress UX | Journal events + CLI follow / MCP drain | +| `args` | Verbatim tool args | Same | +| `budget` | Shared host token hard ceiling | **Stub** `{ total: null, spent:0, remaining: Infinity }` | +| `workflow()` | Nested name/scriptPath; depth 1; shared caps | Same spirit | +| Determinism bans | Date.now / Math.random / bare new Date | Same | +| Resume | Prefix cache by prompt+opts | Index+key + consume-once cacheKey fallback | +| File diffs per stage | **Not a primitive** | Same — no auto-diff | + +--- + +## 3. Config: agent providers (what to add) + +### Today (`DevspaceUserConfig` / `.devspace/config.json`) + +Existing fields (unchanged conceptually): + +```ts +// src/user-config.ts — today +interface DevspaceUserConfig { + host?: string + port?: number + allowedRoots?: string[] + publicBaseUrl?: string | null + allowedHosts?: string[] + stateDir?: string + worktreeRoot?: string + agentDir?: string + subagents?: boolean // master switch only +} +``` + +There is **no** persisted enable-list. Runtime exposes every implemented provider that is **currently on PATH** (`getLocalAgentProviderAvailabilitySnapshot`). Order is code order of `LOCAL_AGENT_PROVIDERS`, not user preference. No onboarding write-back. + +### Add: `agentProviders` on user config + +```ts +/** Known built-in ids — keep in sync with LocalAgentProvider */ +type AgentProviderId = + | "codex" + | "claude" + | "opencode" + | "pi" + | "cursor" + | "copilot" + +interface AgentProvidersConfig { + /** + * Ordered enable-list. Order = preference. + * index 0 = default fallback for agent() when provider omitted. + * Only ids in this list may be used by workflows/subagents (if present). + * Missing/empty → fall back to "all currently available" (compat) OR + * require init (prefer: treat missing as "auto = all available in code order"). + */ + enabled: AgentProviderId[] + + /** ISO time of last successful probe (init/doctor). Optional. */ + detectedAt?: string + + /** + * Optional last probe snapshot for doctor UI (not required at runtime). + * Do not use as source of truth for enablement — `enabled` is. + */ + lastProbe?: Array<{ + id: AgentProviderId + available: boolean + detail?: string // path or error + }> +} + +interface DevspaceUserConfig { + // ...existing... + subagents?: boolean + agentProviders?: AgentProvidersConfig // NEW +} +``` + +### Example `~/.devspace/config.json` + +```json +{ + "host": "127.0.0.1", + "port": 7676, + "allowedRoots": ["/home/you/work"], + "subagents": true, + "agentProviders": { + "enabled": ["codex", "claude", "opencode", "pi"], + "detectedAt": "2026-07-21T12:00:00.000Z", + "lastProbe": [ + { "id": "codex", "available": true, "detail": "/usr/bin/codex" }, + { "id": "claude", "available": true, "detail": "/home/you/.local/bin/claude" }, + { "id": "opencode", "available": true }, + { "id": "pi", "available": true }, + { "id": "cursor", "available": false, "detail": "not found" }, + { "id": "copilot", "available": false, "detail": "not found" } + ] + } +} +``` + +### Semantics + +| Concern | Spec | +|---|---| +| **Master switch** | `subagents: true` still required for workflow tools + agent CLI + skills. | +| **Enable-list** | `agentProviders.enabled` is the only user-facing allowlist. | +| **Order** | First entry = default `agent()` provider after availability filter. | +| **Live ∩ config** | `candidates = enabled.filter(id => currentlyAvailable(id))`. Stale enable of uninstalled CLI → skipped with doctor warning, not hard-fail until no candidates. | +| **Unknown ids** | Reject on write/init; ignore-with-warn at read if config hand-edited. | +| **Missing `agentProviders`** | Compat: `enabled` effective = all available in built-in order (today’s behavior). Init should still write the block. | +| **Empty `enabled: []`** | Error at first `agent()` / `agents run`: “no providers enabled”. | +| **Env override (optional)** | `DEVSPACE_AGENT_PROVIDERS=codex,claude` replaces `enabled` for process (ops/debug). | +| **ServerConfig** | Load into `ServerConfig.agentProviders: { enabled: AgentProviderId[] }` resolved at boot. | + +### Onboarding (`devspace init` / `doctor`) + +1. Probe all six providers (reuse `local-agent-availability`). +2. Set `enabled` = available ids in **stable product order**: + `codex → claude → opencode → pi → cursor → copilot` (only those available). +3. Write `detectedAt` + optional `lastProbe`. +4. `doctor` re-probes; offers to refresh `enabled` (add newly installed; optionally keep user-disabled by not auto-re-adding removed ids — v1: refresh = rewrite available set, document that). + +### Default provider algorithm + +``` +resolveProvider(opts, meta, config): + enabled = config.agentProviders?.enabled + ?? ALL_IMPLEMENTED_IN_CODE_ORDER + candidates = enabled ∩ liveAvailable(PATH) + if opts.provider: + if opts.provider ∉ enabled → throw (disabled in config) + if opts.provider ∉ liveAvailable → throw (not installed) + return opts.provider + if meta.defaultProvider: + same checks against candidates + return meta.defaultProvider + if candidates[0] → return candidates[0] + throw NoProviderError +``` + +Skill: “Pass `provider` when you care; else first enabled+available provider.” +--- + +## 4. Entry surfaces + +### 4.1 CLI + +``` +devspace workflow run (--file | --name | --resume ) + [--arg key=value]... [--follow] +devspace workflow status [--follow] +devspace workflow cancel +devspace workflow ls +devspace workflow __worker # hidden +``` + +| Flag | Spec | +|---|---| +| `--file` | Read script from path (must be under allowed roots when policy applies). | +| `--name` | Resolve via [§6 named files](#6-script-sources). | +| `--resume` | New run row; replay journal from prior runId. | +| `--arg k=v` | Build `args` object (values: JSON-parse if possible else string). | +| `--follow` | Drain events until terminal; print log/phase/agent lines. | + +Spawn: same pattern as `agents __worker` (detached, stdio ignore, unref). Inputs only from run row. + +### 4.2 MCP (togglable with `config.subagents`) + +| Tool | Input | Output (conceptual) | +|---|---|---| +| `run_workflow` | `workspaceId`, `script?` \| `name?` \| `resumeFromRunId?`, `args?`, `yieldTimeMs?` | `{ runId, status, events, nextSeq, result? }` after parse+spawn+short yield | +| `workflow_status` | `runId`, `sinceSeq?`, `yieldTimeMs?` | long-poll events / terminal | +| `workflow_cancel` | `runId` | `{ runId, status }` | + +**No** `workflow_ls` on MCP v1. +**No** `agent_*` MCP tools. + +Tool description embeds ~25-line API cheat-sheet (CC-style education in-band). + +### 4.3 Skill + +`skills/dynamic-workflows/SKILL.md` (+ seed on init): + +- When to use CLI vs when host is ChatGPT (MCP). +- Full primitive reference. +- Prompt patterns for read-only vs write (instead of writeMode). +- Provider list / default fallback. +- Schema examples, resume, cancel, 3 worked examples. + +--- + +## 5. Script contract + +### 5.1 Shape + +```js +export const meta = { + name: 'review-auth', + description: 'Fan-out review of auth changes', + phases: [ + { title: 'Review', detail: 'parallel reviewers' }, + { title: 'Synthesize' }, + ], + // DevSpace extensions (optional): + defaultProvider: 'codex', + concurrency: 4, +} + +// body — async IIFE context +phase('Review') +// ... +return { summary } +``` + +### 5.2 `meta` rules (CC + DS) + +| Rule | Spec | +|---|---| +| First statement | `export const meta = {…}` | +| Pure literal | No vars, calls, spreads, templates in meta object | +| Required | `name`, `description` | +| Optional CC | `phases[]` `{ title, detail? }`, `whenToUse?` | +| Optional DS | `defaultProvider?`, `concurrency?` (clamped to engine max) | +| Validation | Zod `WorkflowMetaSchema` + JSON round-trip purity | +| Extract | Regex start + balanced-brace scanner; `vm.runInNewContext('('+literal+')')` | + +### 5.3 Transform pipeline (`workflow-script.ts`) + +1. Extract/validate meta. +2. Strip leading `export ` → 7 spaces (preserve line numbers). +3. Reject stray `import` / top-level `export` after meta. +4. Wrap: + +```js +(async ({ agent, parallel, pipeline, phase, log, args, budget, workflow, meta, console }) => { + // user body +}) +``` + +5. `new vm.Script(wrapped, { filename: 'workflow:'+name, lineOffset: -1 })`. +6. Friendly errors: missing meta, syntax (with line), purity fail. + +### 5.4 Language bans (CC) + +| Banned in script | Behavior | +|---|---| +| `Date.now()` | `WorkflowDeterminismError` | +| `Math.random()` | same | +| argless `new Date()` | same | +| `require` / `process` / `fetch` / timers | not in context | +| TypeScript syntax | parse fail | + +Allowed: normal JS, `JSON`, `Array`, `Map`, `Set`, `Date.parse`, `new Date(isoString)`. + +`console.log/warn/error` → `log` events. + +--- + +## 6. Script sources + +| Source | Resolution | +|---|---| +| Inline (`--file` content / MCP `script`) | Persist to `/workflows/runs/.js` | +| Named (`--name` / MCP `name`) | (1) `/.devspace/workflows/.js` (2) `~/.devspace/workflows/.js` | +| Resume | Load persisted path on prior run (user may edit that copy) | + +Name sanitization: `^[a-z0-9-]+$`. +Run row stores `scriptPath`, `scriptHash`, `source: inline|named`. + +--- + +## 7. Primitives (spec + implementation) + +All injected into the sandbox. Host deps: `{ journal, runProvider, availableProviders, replay?, concurrency, signal, workspaceRoot }`. + +--- + +### 7.1 `agent(prompt, opts?)` + +#### Spec (public) + +```ts +type AgentOpts = { + label?: string + phase?: string // overrides current ALS phase for this call + schema?: object // JSON Schema → validated object return + model?: string + effort?: string // was "thinking"; provider-native effort/reasoning level + provider?: string // DevSpace; default via §3 + isolation?: "worktree" // must-have; omit = shared workspace root + // NO writeMode in v1 +} + +function agent(prompt: string, opts?: AgentOpts): Promise +// with schema → Promise (validated) +// without → Promise (finalResponse text) +``` + +| Behavior | Spec | +|---|---| +| Failure | **Throw**. `parallel` maps throw → `null`. | +| Success string | Adapter `finalResponse`. | +| Success schema | Validated object; raw text also journaled. | +| Call index | Program order at invocation (before semaphore). | +| Semaphore | Only `agent()` acquires permit. | +| Cancel | Abort signal → throw cancelled. | +| Replay | Cache key includes isolation; hits journal `from_cache`. | +| Isolation | See §7.1b. | + +#### Implementation notes + +``` +async function agent(prompt, opts) { + const callIndex = nextCallIndex() + const provider = resolveProvider(opts, meta, config) + const phase = opts.phase ?? alsPhase.getStore() + const isolation = opts.isolation === "worktree" ? "worktree" : "shared" + const cacheKey = sha256(canonicalJson({ + prompt, provider, + model: opts.model ?? null, + effort: opts.effort ?? null, + schema: opts.schema ?? null, + isolation, + })) + if (replay) { + const hit = replay.match(callIndex, cacheKey) + if (hit) { journal.completeCached(...); return hit.value } + } + await semaphore.acquire(signal) + let worktree: WorktreeHandle | null = null + try { + journal.beginAgentCall({ callIndex, cacheKey, provider, isolation, ... }) + const cwd = isolation === "worktree" + ? (worktree = await createAgentWorktree({ runId, callIndex, workspaceRoot })).path + : workspaceRoot + const run = (p) => runProvider({ + provider, prompt: p, model: opts.model, effort: opts.effort, workspace: cwd, + }) + const result = opts.schema + ? await enforceSchema({ schema: opts.schema, prompt, run, journal, callIndex }) + : (await run(prompt)).finalResponse + journal.completeAgentCall(...) + return result + } catch (e) { + journal.failAgentCall(...) + throw e + } finally { + semaphore.release() + if (worktree) await finalizeAgentWorktree(worktree) // §7.1b + } +} +``` + +`runProvider` wraps `runLocalAgentProvider` with **`effort`** (not `thinking`) on `LocalAgentRunInput`. **No** `local_agent_sessions` dual-write v1. + +### 7.1b `isolation: 'worktree'` (must-have) + +Inspired by CC: expensive (~setup+disk); use when parallel **mutators** would conflict. Not a read-only switch. + +| Rule | Spec | +|---|---| +| Default | Omit / undefined → agent `cwd` = workflow `workspaceRoot` (shared checkout). | +| `"worktree"` | Fresh git worktree under managed root (reuse `config.worktreeRoot` / existing git-worktrees helpers). | +| Base | Pin to workspace HEAD (or open-workspace base SHA if known) at **run start**; all worktrees for the run share that pin unless documented otherwise. | +| Path layout | e.g. `/wf//c/` or UUID; must stay inside managed root. | +| Adapter cwd | Provider runs with `workspace: worktreePath`. | +| Success + dirty | **Preserve** worktree; journal `worktreePath` + `dirty: true` on agent_call / event data. **Do not** auto-merge/cherry-pick into source. | +| Success + clean | Optional auto-remove (CC: remove if unchanged). v1: remove if `git status` clean. | +| Failure / cancel | Preserve for diagnosis; retention e.g. 7d cleanup job later; v1: leave on disk + path in journal. | +| Handoff | Later stages **do not** see worktree files unless they use the same path or agent return text lists paths. Prefer **schema returns** for findings; implementer stages that must compose should use **shared** isolation or sequential shared agents. | +| Parallel safety | Multiple `isolation: 'worktree'` agents concurrent = OK. Mixing worktree + shared writers = caller responsibility (skill: don’t). | +| Non-git workspace | `isolation: 'worktree'` → throw clear error (worktrees require git). | +| Cost | Skill: use only for parallel mutators. | +| Cache key | Includes `isolation` so resume doesn’t reuse shared result for worktree call. | + +Events/data extras: + +```ts +// agent_call_started / completed data +{ worktreePath?: string, isolation: "shared" | "worktree", dirty?: boolean } +``` + +**Not v1:** auto-apply worktree diffs to main checkout; multi-worktree merge tools. +#### Structured output (`workflow-schema.ts`) + +Inspired by CC `schema` → StructuredOutput: + +1. Augment prompt: respond with **only** JSON conforming to schema. +2. Run provider. +3. Extract JSON (fences strip + balanced-brace). +4. Ajv validate (`allErrors: true`, `strict: false`). +5. On fail: journal `schema_retry`; re-run with error text; reuse `providerSessionId` if adapter returned one (max 2 retries). +6. Exhaustion → throw; parallel → null. + +--- + +### 7.2 `parallel(thunks)` + +#### Spec (CC) + +```ts +function parallel(thunks: Array<() => Promise>): Promise> +``` + +| Rule | Spec | +|---|---| +| Barrier | Await all thunks before resolve. | +| Error | Thunk throw / agent throw → that index `null`; **parallel never rejects**. | +| Empty | `[]` → `[]`. | +| Cap | Max **4096** thunks (hard error). | +| Concurrency | Limited by agent semaphore only (thunks can start together; agents queue). | + +#### Implementation + +```js +async function parallel(thunks) { + assertMaxItems(thunks.length) + const results = await Promise.all( + thunks.map(t => t().then(v => v, () => null)) + ) + return results +} +``` + +--- + +### 7.3 `pipeline(items, ...stages)` + +#### Spec (CC) + +```ts +type Stage = (prev: any, originalItem: any, index: number) => any | Promise + +function pipeline(items: any[], ...stages: Stage[]): Promise +``` + +| Rule | Spec | +|---|---| +| Sync | **No barrier** between stages across items. | +| Per item | Sequential stages for that item’s chain. | +| Stage args | `(prevResult, originalItem, index)`. First stage `prev` = item. | +| Throw | That item becomes `null`; remaining stages skipped for it. | +| Cap | Max **4096** items. | +| Wall-clock | ≈ slowest item chain (true concurrency across items). | + +#### Implementation sketch + +```js +async function pipeline(items, ...stages) { + assertMaxItems(items.length) + return Promise.all(items.map((item, index) => + (async () => { + let prev = item + for (const stage of stages) { + try { prev = await stage(prev, item, index) } + catch { return null } + } + return prev + })() + )) +} +``` + +--- + +### 7.4 `phase(title)` + +#### Spec (CC) + +```ts +function phase(title: string): void +``` + +| Rule | Spec | +|---|---| +| Effect | Sets **current phase** for subsequent agents without `opts.phase`. | +| Events | Journal `phase_started` (and optional end on next phase). | +| Concurrency | **AsyncLocalStorage** so concurrent pipeline chains don’t race. | +| UI | CLI `--follow` / MCP events group by phase; match `meta.phases[].title` when possible. | + +```js +function phase(title) { + alsPhase.enterWith(title) // or run with ALS in engine wrapper + journal.appendEvent({ type: 'phase_started', phase: title }) +} +``` + +Prefer documenting: inside concurrent stages set `opts.phase` explicitly (same advice as CC). + +--- + +### 7.5 `log(message)` + +#### Spec (CC) + +```ts +function log(message: string): void +``` + +- Journal `log` event; data truncated per §8. +- CLI follow prints narrator lines. +- Skill: log drops/caps (“no silent caps”). + +`console.log` → same path. + +--- + +### 7.6 `args` + +#### Spec (CC) + +```ts +const args: unknown // frozen; from run input; undefined if omitted +``` + +| Rule | Spec | +|---|---| +| MCP | Pass real JSON object/array — not stringified JSON string. | +| CLI | `--arg k=v` → object; values JSON-parsed when valid. | +| Freeze | `Object.freeze` deep where practical. | +| Resume | Same args required for max cache hits when prompts embed args. | + +--- + +### 7.7 `budget` (stub v1) + +#### Spec (CC shape, stub values) + +```ts +const budget = Object.freeze({ + total: null as number | null, + spent(): number { return 0 }, + remaining(): number { return Infinity }, +}) +``` + +| Future | Wire `total` from CLI/MCP optional `maxAgentCalls` or token directive; hard-throw when exceeded. | +| v1 | Shape present so scripts/skills match CC; loops must still use dry-round or count, not infinite budget loops. | + +Skill warns: do not `while (budget.remaining() > x)` without other exit — remaining is Infinity. + +--- + +### 7.8 `workflow(nameOrRef, args?)` — nested + +#### How CC behaves (inspiration) + +- `workflow(name | { scriptPath }, args?)` +- Runs child **inline** in same run. +- Shares concurrency cap, agent counter, abort, token budget. +- Child agents appear nested in progress UI. +- **Depth 1 only** — nest inside child throws. +- Return value = child’s script return. +- Errors: unknown name / unreadable path / syntax → throw. + +#### DevSpace v1 + +```ts +function workflow( + nameOrRef: string | { scriptPath: string }, + childArgs?: unknown, +): Promise +``` + +| Rule | Spec | +|---|---| +| `string` | Resolve named file (§6). | +| `{ scriptPath }` | Absolute/resolved path to `.js` (must pass root allowlist if enforced). | +| Depth | `nestDepth` ALS/counter; `> 1` → throw. | +| Shared | Same journal runId, semaphore, call-index sequence, cancel signal. | +| Meta | Child meta used for phase titles optionally; run name stays parent. | +| Events | Optional `phase` prefix or `label: nest:childName`. | +| No new process | In-process second script execute. | +| Resume | Child `agent()` calls continue global callIndex — replay still works. | + +```js +async function workflow(nameOrRef, childArgs) { + if (nestDepth >= 1) throw new Error('workflow() nesting limited to one level') + const source = resolveNestedSource(nameOrRef, workspaceRoot) + const parsed = parseWorkflowScript(source) + return executeNested({ parsed, args: childArgs, nestDepth: nestDepth + 1, ...sharedDeps }) +} +``` + +--- + +## 8. Size caps (education + defaults) + +### Why caps exist + +Without bounds: + +- One agent can return multi‑MB logs → SQLite bloat, slow drain. +- MCP tool results can exceed host message limits. +- Event `dataJson` spam freezes `--follow`. +- Malicious/buggy script `return` of huge graphs. + +This is **not** semantic truncation of “coverage”; it’s **transport/storage safety**. Skill still says: if you intentionally sample files, `log()` that you did. + +### Recommended v1 limits + +| Asset | Cap | On exceed | +|---|---|---| +| Event `dataJson` | ~8 KiB string | Truncate + `"truncated": true` | +| `responseText` on agent_calls | e.g. 1 MiB | Truncate stored copy; prefer schema path for structure | +| `structuredJson` | e.g. 256 KiB | Fail agent call (throw) | +| Script `return` → `resultJson` | e.g. 256 KiB | Fail run `errorKind: 'result_too_large'` | +| `args` JSON | e.g. 64 KiB | Reject at createRun | +| Inline script source | e.g. 512 KiB | Reject at parse | +| Events drain page | limit param default 100–500 | Cursor `nextSeq` | + +Numbers can be constants in `workflow-store.ts`; tune later. + +--- + +## 9. Cancel, heartbeat, reap + +| Step | Spec | +|---|---| +| Heartbeat | Worker every 5s updates `heartbeatAt`; polls `cancelRequested`. | +| Cooperative | Set flag → worker AbortController → journal `run_cancelled` → group SIGTERM. | +| Hard | After ≤5s: `terminateProcessTree` on pid; mark cancelled. | +| Reap | `heartbeat` stale >60s **and** `kill(pid,0)` dead → mark failed `errorKind: 'heartbeat'`. | +| Sleep gap | Liveness check avoids false fail after laptop sleep. | + +Adapters: no individual abort API — accepted; group-kill is backstop. + +--- + +## 10. Resume / replay + +| Piece | Spec | +|---|---| +| New run | `--resume` / `resumeFromRunId` creates new run with `resumedFromRunId`. | +| Cache key | `sha256(canonicalJson({ prompt, provider, model, effort, schema, isolation }))` | +| Match | (1) same callIndex + key (2) on first miss, consume-once by key (fan-out order). | +| Record | Cache hits written as new rows `from_cache=1` so chains chain. | +| Determinism | Bans make prompt construction stable if args fixed. | + +Document CC divergence (consume-once) in skill. + +--- + +## 11. Journal schema (behavioral) + +### `workflow_runs` + +id, name, source, scriptPath, scriptHash, workspaceRoot, workspaceId?, argsJson, status (`starting|running|completed|failed|cancelled`), error?, errorKind?, resultJson?, pid?, heartbeatAt?, cancelRequested, resumedFromRunId?, timestamps. + +### `workflow_events` + +(runId, seq) PK; type enum including `run_started`, `phase_started`, `log`, `agent_call_*`, `schema_retry`, `run_*`; phase; label; dataJson truncated. + +### `workflow_agent_calls` + +(runId, callIndex) PK; cacheKey; provider; model; label; phase; status; fromCache; providerSessionId?; responseText; structuredJson?; error?; times. + +Adapter `items[]` **not** persisted. + +--- + +## 12. Access model: prompt + isolation (no writeMode) + +### What Claude Code does + +CC `agent()` opts include `label`, `phase`, `schema`, `model`, `effort`, **`isolation`**, `agentType` — **not** `writeMode`. + +| Layer | Role | +|---|---| +| Host permission mode | Approve / bypass tools | +| `agentType` / tools | Read-oriented vs full agents | +| **`isolation: 'worktree'`** | Mutations in private tree; no auto-merge | +| **Prompt** | “Do not modify files” / implementer instructions | + +### What DevSpace does in v1 + +| Layer | Behavior | +|---|---| +| API | **No writeMode**; **yes `isolation?: 'worktree'`** | +| Adapter | Fixed yolo-style policy (current profile behavior) | +| Isolation | Engine creates managed worktree; cwd for that agent only | +| Skill | RO vs write **prompts** + when to set isolation | + +```text +READ-ONLY reviewer: +- Do not modify files. Return findings via schema. + +IMPLEMENTER (shared tree — sequential): +- Minimal edits; report paths. + +IMPLEMENTER (parallel): +- isolation: 'worktree' +- Report worktree-relative paths + summary in return value. +- Orchestrator decides merge; engine will not auto-merge. +``` +--- + +## 13. File changes (explicit non-primitive) + +| Approach | v1 | +|---|---| +| Shared workspace; later agents see prior edits on disk | Yes | +| Return structured paths/findings between stages | Yes (schema) | +| Auto git snapshot / diff after each agent | **No** | +| Per-agent worktree | **No** (follow-up) | +| Host `show_changes` after whole workflow | Optional host behavior; not engine | + +--- + +## 14. End-to-end authoring examples + +### Fan-out review (ChatGPT or local agent) + +```js +export const meta = { + name: 'fanout-review', + description: 'Two reviewers then synthesize', + phases: [{ title: 'Review' }, { title: 'Synthesize' }], +} + +const S = { /* FINDINGS schema */ } +phase('Review') +const reviews = await parallel([ + () => agent('Read-only review security…', { provider: 'claude', label: 'sec', schema: S }), + () => agent('Read-only review tests…', { provider: 'codex', label: 'test', schema: S }), +]) +phase('Synthesize') +const summary = await agent( + `Merge findings:\n${JSON.stringify(reviews.filter(Boolean))}`, + { label: 'merge', schema: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] } }, +) +return { reviews, summary } +``` + +### Pipeline over files (coding agent CLI) + +```js +export const meta = { + name: 'migrate-files', + description: 'Per-file transform', + phases: [{ title: 'Edit' }], +} + +const files = args.files +return pipeline( + files, + (f) => agent(`Update imports in ${f}. Minimal edit. Report path.`, { + label: `edit:${f}`, + phase: 'Edit', + }), +) +``` + +--- + +## 15. Implementation checklist (by primitive) + +| Primitive / surface | Module | Tests focus | +|---|---|---| +| meta parse | `workflow-script.ts` | purity, line nos, missing meta | +| sandbox bans | `workflow-sandbox.ts` | Date/Math throw; console→log | +| agent | `workflow-api.ts` | provider resolve, throw, callIndex order | +| schema | `workflow-schema.ts` | retry, validate, exhaust | +| parallel | `workflow-api.ts` | null on error, barrier | +| pipeline | `workflow-api.ts` | no-barrier proof, stage args | +| phase ALS | `workflow-api.ts` | concurrent chains | +| log / args / budget | `workflow-api.ts` | freeze, stub budget | +| workflow nest | `workflow-api.ts` + engine | depth 1, shared journal | +| store | `workflow-store.ts` | seq, reap, cancel | +| replay | `workflow-replay.ts` | index+key, consume-once | +| CLI | `cli.ts` | run/status/cancel/ls/__worker | +| MCP | `workflow-tools.ts` | yield, survive disconnect | +| skill | `skills/dynamic-workflows` | education | +| providers config | `user-config` / init / availability | ordered default | + +--- + +## 16. Non-goals recap (v1) + +- MCP raw agent tools +- `writeMode` on `agent()` (isolation **is** in scope) +- Auto-merge of worktrees into source checkout +- Real host token budget +- Auto file-change / diff events per stage +- MCP run list +- Dashboard +- Dual-write `local_agent_sessions` + +--- + +## 17. `effort` rename (profiles + runtime + agent opts) + +| Surface today | Target | +|---|---| +| Profile YAML `thinking:` | `effort:` | +| CLI `devspace agents run --thinking` | `--effort` | +| `LocalAgentRecord.thinking` / DB column | `effort` (migration: rename column or accept both briefly) | +| `LocalAgentRunInput.thinking` | `effort` | +| Adapter mapping (`modelReasoningEffort`, claude effort, pi `--thinking`) | Read from `input.effort` | +| Docs / examples / skill | `effort` only | +| Workflow `agent()` opts | `effort` only | +| Workflow journal / cache key | `effort` | + +Provider passthrough values stay free strings (`low`, `high`, `xhigh`, …) — DevSpace does not translate between providers. + +**Compat (optional short window):** read profile `thinking` if `effort` missing; CLI accept `--thinking` as alias deprecated. Prefer clean break if you’re fine breaking profile files (examples are under our control). + +## 18. Open only if product changes mind + +1. Exact byte constants for §8. +2. Nested `{ scriptPath }` must be under workspace only? +3. Worktree retention days / cleanup job timing. +4. Whether `agents run` CLI also gains `--isolation worktree` (workflow-first is enough for v1). From 2800b806a15b539654e9a2892012b2e3072afbe4 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Tue, 21 Jul 2026 12:10:52 +0530 Subject: [PATCH 3/6] html response --- docs/claude-code-dynamic-workflows.html | 1150 +++++++++++++++++++++++ 1 file changed, 1150 insertions(+) create mode 100644 docs/claude-code-dynamic-workflows.html diff --git a/docs/claude-code-dynamic-workflows.html b/docs/claude-code-dynamic-workflows.html new file mode 100644 index 00000000..d7fa34ac --- /dev/null +++ b/docs/claude-code-dynamic-workflows.html @@ -0,0 +1,1150 @@ + + + + + + + Claude Code Dynamic Workflows — API & Primitives Reference + + + + + + + + + +
+
+
+
Claude Code · 2.1.x
+

Dynamic Workflows — API & Primitives

+
+
+ Workflow tool + agent / pipeline / parallel + resume + budget + orchestrator tier +
+
+
+ +
+ + + +
+ + +
+
+

01 · Thesis

+

+ Deterministic control flow.
+ Stochastic workers. One orchestrator brain. +

+
+
+
+

Problem

+

A single agent loop confuses what to do next with how to do the work. Fan-out, verification, and synthesis become ad-hoc tool calls that the model re-invents every turn.

+
+
+

Mechanism

+

Claude Code exposes a Workflow tool. The model authors a short plain-JS script. The harness runs that script: loops, conditionals, and fan-out are code — not free-form model decisions mid-orchestration.

+
+
+

Payoff

+

A stronger “orchestrator” model designs the graph once. Weaker/cheaper or specialized subagents execute units of work. Scale, confidence, and isolation become programmable.

+
+
+
+ Key insight. Dynamic workflows are not “another agent.” They are a programmable multi-agent runtime the model can call. The script is the plan; agent() is the only way work escapes into a model. +
+
+ + +
+
+

02 · Architecture

+

Three layers

+
+ +
+
Main session
orchestrator model
+
Workflow tool
+
JS runtime
script + hooks
+
agent()
+
Subagents
N isolated workers
+
+ +
+
+

1. Coordinator (main loop)

+
    +
  • Talks to the user
  • +
  • Scouts the repo / work-list
  • +
  • Authors or selects a workflow script
  • +
  • Calls the Workflow tool
  • +
  • Synthesizes the returned result
  • +
+
+
+

2. Workflow engine

+
    +
  • Parses export const meta
  • +
  • Runs the script in an async JS context
  • +
  • Hosts agent / pipeline / parallel / phase / log / budget / workflow
  • +
  • Enforces concurrency & agent caps
  • +
  • Journals each agent call for resume
  • +
+
+
+

3. Subagents

+
    +
  • Own tool loops (Read, Bash, Edit, …)
  • +
  • Optional structured output via schema
  • +
  • Optional worktree isolation
  • +
  • Optional model / effort / agentType overrides
  • +
  • Final text (or validated object) is the return value
  • +
+
+
+ +
+

Mental model

+
// Coordinator decides STRUCTURE +Workflow({ script, args }) + → JS engine runs control flow + → agent(prompt, opts) × N // workers decide CONTENT + → script return value +→ coordinator narrates to user
+
+
+ + +
+
+

03 · Opt-in policy

+

When the model may call Workflow

+
+

+ Workflows can spawn dozens of agents and burn large token budgets. Claude Code therefore gates the tool behind explicit user opt-in. A task that would “benefit from parallelism” is not enough. +

+
+
+

Allowed triggers

+
    +
  • User prompt contains the keyword ultracode (system-reminder confirms).
  • +
  • Session ultracode is on (standing opt-in for every substantive task).
  • +
  • User asks in their own words: “use a workflow”, “fan out agents”, “orchestrate with subagents”, etc.
  • +
  • A skill / slash command’s instructions tell the model to call Workflow.
  • +
  • User asks for a specific named / saved workflow.
  • +
+
+
+

Not enough

+
    +
  • “This would be faster in parallel.”
  • +
  • The model silently deciding multi-agent is better.
  • +
  • Inferring opt-in from task complexity alone.
  • +
+

Fallback: use the single Agent tool, or describe the multi-agent plan + rough cost and ask.

+
+
+ +
+

Ultracode mode

+

When ultracode is on, opt-in is standing. The model should author and run a workflow for every substantive task. Token cost is not a constraint; the goal is the most exhaustive correct answer. Multi-phase work is often several workflows in sequence (understand → design → implement → review) so the coordinator stays in the loop between phases. Solo only for pure conversation or trivial mechanical edits.

+
+ +
+ Hybrid default. Scout inline first (list files, scope the diff, discover the work-list), then call Workflow to pipeline over that list. You need the shape before the orchestration step, not before the whole task. +
+
+ + +
+
+

04 · Workflow tool API

+

Tool surface the model calls

+
+

Tool name: Workflow (alias RunWorkflow). Search hint: orchestrate subagents with deterministic JavaScript workflow. Runs in the background — returns immediately with a task ID; a <task-notification> arrives on completion. Live progress: /workflows.

+ +

Input fields

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRole
scriptstring (optional, max-bounded)Inline self-contained workflow script. Must start with pure-literal export const meta = {...}. Preferred first invocation — do not Write a file first.
namestring (optional)Named workflow from built-ins or .claude/workflows/. Resolves to a script.
scriptPathstring (optional)Path to a persisted script on disk. Every invocation writes one under the session dir and returns the path. Iterate with Write/Edit + re-invoke. Takes precedence over script / name.
argsany (optional)Value exposed to the script as global args, verbatim. Pass real JSON arrays/objects — not stringified JSON (stringified lists break args.map).
resumeFromRunIdstring matching ^wf_[a-z0-9-]{6,}$Prior run ID. Unchanged prefix of agent() calls replays from cache; first changed/new call and after run live. Same-session only. Stop the prior run first.
description / titleignoredSet display name/description in script meta, not these fields.
+
+

Validation: at least one of script, name, or scriptPath is required.

+ +

Return envelope (conceptual)

+
+
// async launch — tool returns before script finishes +{ + status: "async_launched" | "remote_launched", + taskId: "...", + taskType: "local_workflow" | "remote_agent", + workflowName: "review-changes", // meta.name + runId: "wf_…", // for resumeFromRunId + transcriptDir: "/…/…", // subagent transcripts + journal.jsonl + scriptPath: "/…/workflows/scripts/….js", + summary?: "…", + warning?: "…", + error?: "…" // e.g. syntax check failed +}
+
+ +
+
+

First run

+
Workflow({ + script: `export const meta = {…} +…`, + args: { files: changed } +})
+
+
+

Iterate

+
// edit the returned scriptPath, then: +Workflow({ + scriptPath: returnedPath, + resumeFromRunId: runId, // optional cache + args: { files: changed } +})
+
+
+
+ + +
+
+

05 · Script contract

+

What a valid workflow script is

+
+ +
+
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', model: 'sonnet' }, + ], + // optional: whenToUse (workflow list), model on a phase +} + +// body runs in an async context — await freely +phase('Scan') +const flaky = await agent('…', { schema: FLAKY_SCHEMA }) +// … +return { flaky }
+
+ +
+
+

meta rules

+
    +
  • Must be the first statement.
  • +
  • Pure literal only — no variables, function calls, spreads, or template interpolation.
  • +
  • Required: name, description.
  • +
  • Optional: whenToUse, phases[] with title, detail, optional per-phase model.
  • +
  • Phase titles in meta.phases must match phase() calls exactly for UI grouping.
  • +
  • description is shown in the permission dialog.
  • +
+
+
+

Language & environment

+
    +
  • Plain JavaScript only — not TypeScript. Type annotations, interfaces, generics fail to parse.
  • +
  • Standard built-ins: JSON, Math, Array, etc.
  • +
  • Forbidden for determinism: Date.now(), Math.random(), argless new Date() — they throw (would break resume).
  • +
  • No filesystem, no Node APIs, no network from the script.
  • +
  • Only escape hatch into models/tools: agent() (and nested workflow()).
  • +
  • Pass timestamps via args; stamp wall-clock after the workflow returns.
  • +
+
+
+ +
+ Why non-determinism is banned. Resume replays the longest unchanged prefix of agent() calls by hashing prompt + opts. If the script could branch on time or random, cache identity would lie. Keep control flow pure; put entropy in agent prompts (vary by index) or in post-processing outside the script. +
+
+ + +
+
+

06 · Script primitives

+

Every hook the script can call

+

These are the only APIs injected into the script body. Together they form a small concurrent orchestration language.

+
+ + +
+
+ core +

agent()

+
+
agent(prompt: string, opts?: { + label?: string, + phase?: string, + schema?: object, // JSON Schema + model?: string, // e.g. 'sonnet' | 'opus' | 'haiku' | session ids + effort?: 'low'|'medium'|'high'|'xhigh'|'max', + isolation?: 'worktree', + agentType?: string // registry name, e.g. 'general-purpose', 'code-reviewer', 'claude' +}): Promise<any>
+ +
+
+

Semantics

+
    +
  • Spawns one subagent with its own tool loop.
  • +
  • Without schema: resolves to the agent’s final text (string).
  • +
  • With schema: forces a StructuredOutput tool call; returns the validated object — no fragile JSON parsing.
  • +
  • Returns null if the user skips the agent or it dies after terminal API retries. Always .filter(Boolean) before use.
  • +
  • Subagents are told: final text/object is the return value, not a user-facing message.
  • +
+
+
+

Options in practice

+
    +
  • label — UI/progress label (review:security).
  • +
  • phase — assign progress group inside pipeline/parallel (avoids races on global phase()).
  • +
  • model — omit by default (inherit session model). Override only when tier fit is clear.
  • +
  • effortlow for mechanical stages; higher only for hard verify/judge stages.
  • +
  • isolation: 'worktree' — expensive (~200–500ms + disk). Use only when parallel mutators would conflict. Unchanged worktrees auto-remove.
  • +
  • agentType — custom subagent from the same registry as the Agent tool; composes with schema.
  • +
+
+
+ +
+
const FINDINGS = { + type: 'object', + properties: { + findings: { + type: 'array', + items: { + type: 'object', + properties: { + file: { type: 'string' }, + line: { type: 'integer' }, + issue: { type: 'string' }, + }, + required: ['file', 'line', 'issue'], + additionalProperties: false, + }, + }, + }, + required: ['findings'], + additionalProperties: false, +} + +const result = await agent( + 'Review auth changes for session fixation. Read-only.', + { + label: 'review:auth', + phase: 'Review', + schema: FINDINGS, + effort: 'medium', + agentType: 'claude', + } +) +// result.findings is already typed-shaped JSON
+
+ +
+ MCP access. Workflow subagents can reach session-connected MCP tools via ToolSearch (schemas load on demand). Interactively authenticated MCP servers may be missing in headless/cron runs. +
+
+ + +
+
+ default multi-stage +

pipeline()

+
+
pipeline(items: any[], stage1, stage2, ...): Promise<any[]> +// each stage: (prevResult, originalItem, index) => Promise<any> | any
+
+
+

Semantics

+
    +
  • Each item flows through all stages independently.
  • +
  • No barrier between stages: item A can be in stage 3 while item B is still in stage 1.
  • +
  • Wall-clock ≈ slowest single-item chain — not sum of per-stage slowest times.
  • +
  • Stage callbacks receive (prevResult, originalItem, index) so later stages can label work without stuffing identity into stage-1 returns.
  • +
  • A throwing stage drops that item to null and skips remaining stages for it.
  • +
+
+
+

When to use

+

Default for multi-stage work. Prefer over barrier-then-map whenever each item’s next stage does not need the full previous stage’s result set.

+

Smell test: if you wrote parallel → transform → parallel with no cross-item dependency, rewrite as pipeline with the transform inside a stage.

+
+
+
+
const DIMENSIONS = [ + { key: 'bugs', prompt: '…' }, + { key: 'perf', prompt: '…' }, +] +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 })) + ) + ) +) +// bugs findings verify while perf is still reviewing +const confirmed = results.flat().filter(Boolean) + .filter(f => f.verdict?.isReal)
+
+
+ + +
+
+ barrier +

parallel()

+
+
parallel(thunks: Array<() => Promise<any>>): Promise<any[]>
+
+
+

Semantics

+
    +
  • Runs thunks concurrently.
  • +
  • Barrier: awaits all before returning.
  • +
  • Throwing thunk / agent error → that slot is null. The call itself never rejects — always .filter(Boolean).
  • +
  • Use only when you genuinely need all results together.
  • +
+
+
+

Barrier is correct when…

+
    +
  • Dedup / merge across the full set before expensive work.
  • +
  • Early-exit if total count is zero.
  • +
  • Next stage’s prompt references “the other findings.”
  • +
+

Not justified by…

+
    +
  • “I need to flatten first” — do it inside a pipeline stage.
  • +
  • “Stages are conceptually separate” — pipeline already models that.
  • +
  • “Cleaner code” — barrier latency is real.
  • +
+
+
+
+
// Correct barrier: need ALL findings before expensive verification +const all = await parallel( + DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS_SCHEMA })) +) +const deduped = dedupeByFileAndLine( + all.filter(Boolean).flatMap(r => r.findings) +) +const verified = await parallel( + deduped.map(f => () => agent(verifyPrompt(f), { schema: VERDICT_SCHEMA })) +)
+
+
+ + +
+
+ progress UX +

phase() · log()

+
+
phase(title: string): void +log(message: string): void
+
+
+

phase(title)

+

Starts a progress group. Subsequent agent() calls without explicit opts.phase group under this title in /workflows. Inside concurrent stages, prefer opts.phase to avoid races on the global phase state. Same string → same group box.

+
+
+

log(message)

+

Narrator line above the progress tree. Use for counts, dropped coverage, early-exit reasons — anything a silent cap would hide. “No silent caps” is a first-class quality rule.

+
+
+
+ + +
+
+ inputs & cost +

args · budget

+
+
args: any +// value of Workflow({ args }) — undefined if omitted + +budget: { + total: number | null, + spent(): number, + remaining(): number // max(0, total - spent) or Infinity if no target +}
+
+
+

args

+
    +
  • Parameterize named workflows: research question, file list, config object.
  • +
  • Pass real JSON: args: ["a.ts", "b.ts"] — not a stringified list.
  • +
  • Only channel for non-deterministic / external inputs that must stay stable across resume (timestamps, seeds as fixed values).
  • +
+
+
+

budget

+
    +
  • Turn token target from user “+500k”-style directives.
  • +
  • budget.total is null when no target was set.
  • +
  • spent() is shared across main loop + all workflows this turn.
  • +
  • Hard ceiling: further agent() calls throw once spent ≥ total.
  • +
  • Always guard loops with budget.total && — else remaining() is Infinity and you hit the 1000-agent cap.
  • +
+
+
+
+
// Scale depth to budget +const bugs = [] +while (budget.total && budget.remaining() > 50_000) { + const result = await agent('Find bugs…', { schema: BUGS_SCHEMA }) + bugs.push(...result.bugs) + log(`${bugs.length} found, ${Math.round(budget.remaining()/1000)}k remaining`) +} + +// Or static fleet sizing +const FLEET = budget.total + ? Math.floor(budget.total / 100_000) + : 5
+
+
+ + +
+
+ composition +

workflow()

+
+
workflow( + nameOrRef: string | { scriptPath: string }, + args?: any +): Promise<any>
+
+
    +
  • Run another workflow inline as a sub-step; return whatever it returns.
  • +
  • String name → saved/built-in registry (same as Workflow({ name })).
  • +
  • { scriptPath } → run a script file already on disk.
  • +
  • Child shares parent’s concurrency cap, agent counter, abort signal, and token budget.
  • +
  • Child agents appear under a nested group in /workflows.
  • +
  • Nesting is one level onlyworkflow() inside a child throws.
  • +
  • Throws on unknown name / unreadable path / child syntax error; catch to handle.
  • +
+
+
+
+ + +
+
+

07 · Limits & sandbox

+

Hard bounds the engine enforces

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LimitValueNotes
Concurrent agent() callsmin(16, cpu_cores - 2) per workflowExcess queues; all items still complete.
Lifetime agent count1000Runaway-loop backstop.
Items per parallel/pipeline call4096 maxMore → explicit error, not silent truncate.
Workflow size guidelineuser /config: small≈5, medium≈15, large≈50, unrestrictedGuideline, not hard engine cap (unless user configures otherwise).
Script determinismno Date.now / Math.random / bare new DateRequired for resume cache identity.
Script I/OnoneNo FS/network/Node; only agent/workflow escapes.
Worktree isolationopt-in per agentExpensive; auto-clean if unchanged.
+
+
+ + +
+
+

08 · Resume & journal

+

Edit the plan without redoing finished work

+
+

+ Every launch returns a runId and persists the script. After a pause, kill, or script edit: stop the prior run, then relaunch with Workflow({ scriptPath, resumeFromRunId }). +

+
+
+

Cache hit rule

+

Longest unchanged prefix of agent() calls (same prompt + opts) returns cached results instantly. First edited/new call and everything after runs live.

+
+
+

Perfect replay

+

Same script + same args → 100% cache hit. Use this for pure post-processing edits after a completed run.

+
+
+

journal.jsonl

+

Under transcriptDir. Records each agent’s actual return value. Before diagnosing empty/weird results, read the journal — do not assume cached results are non-empty.

+
+
+
+
Workflow({ + scriptPath: '/…/workflows/scripts/review-wf_abc.js', + resumeFromRunId: 'wf_abc…', + args: previousArgs, +})
+
+
+ Fallback. If no journal is available, read agent-<id>.jsonl files in the transcript directory and hand-author a continuation script. +
+
+ + +
+
+

09 · Quality patterns

+

Composable harness shapes

+

These are not special APIs — they are recipes built from the primitives. Pick by task; compose freely.

+
+ +
+
+

Adversarial verify

+

N independent skeptics per claim, prompted to refute. Kill if ≥ majority refute. Kills plausible-but-wrong findings.

+
const votes = await parallel(Array.from({length: 3}, () => () => + agent(`Try to refute: ${claim}. Default refuted=true if uncertain.`, { schema: VERDICT }) +)) +const survives = votes.filter(Boolean) + .filter(v => !v.refuted).length >= 2
+
+
+

Perspective-diverse verify

+

Distinct lenses (correctness, security, repro, perf) instead of N identical refuters. Diversity catches failure modes redundancy cannot.

+
+
+

Judge panel

+

N independent attempts from different angles (MVP-first, risk-first, user-first). Score in parallel; synthesize from winner while grafting runner-up ideas. Beats single-attempt iteration when the solution space is wide.

+
+
+

Loop-until-dry

+

Unknown-size discovery: keep finding until K consecutive rounds return nothing new. Dedup against all seen, not only confirmed — else rejected findings reappear forever.

+
+
+

Multi-modal sweep

+

Parallel agents each search a different way (by-container, by-content, by-entity, by-time). Each is blind to the others — covers angles one search cannot.

+
+
+

Completeness critic

+

Final agent asks: modality not run? claim unverified? source unread? Output becomes the next work round.

+
+
+ +
+

Exhaustive review composition

+
const seen = new Set(), confirmed = [] +let dry = 0 +while (dry < 2) { + const found = (await parallel(FINDERS.map(f => () => + agent(f.prompt, { phase: 'Find', schema: BUGS }) + ))).filter(Boolean).flatMap(r => r.bugs) + const fresh = found.filter(b => !seen.has(key(b))) + if (!fresh.length) { dry++; continue } + dry = 0; fresh.forEach(b => seen.add(key(b))) + const judged = await parallel(fresh.map(b => () => + parallel(['correctness','security','repro'].map(lens => () => + agent(`Judge "${b.desc}" via ${lens} — real?`, { + phase: 'Verify', schema: VERDICT + }) + )).then(vs => ({ + b, + real: vs.filter(Boolean).filter(v => v.real).length >= 2 + })) + )) + confirmed.push(...judged.filter(v => v.real).map(v => v.b)) +} +return confirmed
+
+ +
+ Scale to the ask. “Find any bugs” → few finders, single-vote verify. “Thoroughly audit” → larger pool, 3–5 vote adversarial pass, synthesis stage. When unsure on research/review/audit: lean thorough; on quick checks: lean brief. +
+
+ + +
+
+

10 · Lifecycle & operator UX

+

How a run feels from the outside

+
+
    +
  1. Authoring. Coordinator scouts, then writes inline script (or picks name).
  2. +
  3. Permission. User sees meta.description (and size guideline if set).
  4. +
  5. Launch. Tool returns immediately with taskId, runId, scriptPath, transcriptDir.
  6. +
  7. Progress. /workflows shows phase groups, labels, nested child workflows, narrator log() lines.
  8. +
  9. Completion. <task-notification> delivers the script’s return value to the coordinator.
  10. +
  11. Synthesis. Coordinator may run another workflow, resume with edits, or answer the user.
  12. +
+
+
+

Common single-phase workflows

+
    +
  • Understand — parallel readers → structured map
  • +
  • Design — judge panel of N approaches → scored synthesis
  • +
  • Review — dimensions → find → adversarially verify
  • +
  • Research — multi-modal sweep → deep-read → synthesize
  • +
  • Migrate — discover sites → transform (worktree) → verify
  • +
+
+
+

Multi-phase product work

+

Run several workflows in sequence across turns. The coordinator reads each result before choosing the next phase. Each workflow stays a well-scoped fan-out — not a giant forever-script.

+
+
+
+ + +
+
+

11 · One level above agents

+

Bigger brain orchestrates smaller hands

+
+

+ Classic multi-agent demos put several peers in a chat room and hope coordination emerges. Dynamic workflows invert that: coordination is a program written by a high-capability model; workers are replaceable execution units. +

+ +
+
+

Orchestrator responsibilities

+
    +
  • Understand user intent and constraints
  • +
  • Discover the work-list (files, bugs, APIs, modules)
  • +
  • Choose pattern (pipeline vs barrier, depth vs breadth)
  • +
  • Author the script + schemas + prompts
  • +
  • Allocate model/effort tiers per stage
  • +
  • Interpret structured returns; decide next phase
  • +
  • Talk to the human; own correctness narrative
  • +
+
+
+

Worker responsibilities

+
    +
  • Execute one bounded prompt with tools
  • +
  • Return raw data or schema-validated objects
  • +
  • Stay isolated (optional worktree)
  • +
  • Do not redesign the global plan
  • +
  • May be cheaper/faster models for mechanical stages
  • +
  • May be specialized agentTypes (reviewer, explorer)
  • +
+
+
+ +
+

Why this is “one level above”

+
+
+

Control altitude

+ The orchestrator reasons about graphs, budgets, and verification policy — not about every file read. Workers absorb token-heavy tool churn inside their own contexts. +
+
+

Context isolation

+ Each agent() gets a clean context for its unit of work. The script aggregates only return values. One agent’s rabbit hole cannot pollute another’s prompt. +
+
+

Deterministic spine

+ Loops, fan-out, early-exit, and majority votes are code. They do not “forget” to verify on a bad day. The model invents the harness once; the engine executes it faithfully. +
+
+
+ +
+ Model tiering pattern. Keep the session model strong for orchestration (authoring scripts, reading results, deciding phases). Inside the workflow, omit model for most calls (inherit), or pin effort: 'low' / smaller models for mechanical map stages and reserve high effort for adversarial judges. The orchestrator’s context stays small; total work scales with fleet size. +
+ +
+

Altitude diagram

+
User intent + │ + ▼ +┌──────────────────────────────────────────┐ +│ Orchestrator model (main session) │ +│ · plans · schemas · phase selection │ +│ · Workflow({ script, args }) │ +└───────────────────┬──────────────────────┘ + │ deterministic JS spine + ┌───────────┼───────────┐ + ▼ ▼ ▼ + agent() agent() agent() + worker A worker B worker C + (tools) (tools) (tools) + │ │ │ + └───────────┼───────────┘ + ▼ + structured returns + │ + ▼ + orchestrator synthesizes + │ + ▼ + user-facing answer
+
+
+ + +
+
+

12 · What this feature enables

+

Workloads that were awkward before

+
+ +
+
+

Comprehensive code review

+

Fan out by dimension (security, correctness, tests, perf). Verify each finding adversarially. Merge only survivors. Scale vote count to thoroughness of the ask.

+
+
+

Large migrations / refactors

+

Discover call sites, pipeline each site through transform + verify with isolation: 'worktree' so parallel mutators do not clobber each other. Resume after fixing one stage’s prompt.

+
+
+

Research & multi-source synthesis

+

Multi-modal sweep (web, code, docs, git history), deep-read promising hits, completeness critic, then cited synthesis — with budget-bounded loops.

+
+
+

Design exploration

+

Judge panel: N independent designs from different angles, scored in parallel, grafted synthesis. Better than iterating one design in a single context.

+
+
+

Unknown-size bug hunts

+

Loop-until-dry finders + diverse-lens judges. Dedup against seen set. Stop when two dry rounds pass. Depth scales with budget.total.

+
+
+

Self-repair implementation loops

+

Implement → multi-reviewer parallel → repair from structured findings → verify gates. Same shape as real engineering process, encoded as a script the orchestrator can re-run with resume.

+
+
+

Heterogeneous agent fleets

+

Mix agentTypes and models: explorer for map, implementer for edit, reviewer for audit. Orchestrator stays vendor of truth; workers stay specialists.

+
+
+

Phased product delivery under ultracode

+

Standing multi-agent mode: every substantive step is a workflow. Human watches /workflows; orchestrator chains phases across turns without stuffing everything into one mega-context.

+
+
+ +
+

What it deliberately is not

+
    +
  • Not a durable multi-day job system with external supervisors (that is a different control plane).
  • +
  • Not free-form multi-agent chat; workers do not negotiate plan changes with each other.
  • +
  • Not automatic: user must opt in (or enable ultracode).
  • +
  • Not a replacement for single-agent work on small tasks — overhead is real.
  • +
+
+
+ + +
+
+

13 · Cheatsheet

+

Quick reference

+
+
+
// Tool +Workflow({ script | name | scriptPath, args?, resumeFromRunId? }) + +// Script header (pure literal) +export const meta = { name, description, phases? } + +// Primitives +agent(prompt, { label, phase, schema, model, effort, isolation, agentType }) +pipeline(items, stage1, stage2, …) // no barrier — default +parallel([ () => …, … ]) // barrier — rare +phase(title) +log(message) +args // Workflow args, verbatim +budget.{ total, spent(), remaining() } // hard token ceiling +workflow(name | { scriptPath }, args?) // one-level nest + +// Rules of thumb +// 1. Default to pipeline; barrier only for cross-item merge. +// 2. Always .filter(Boolean) on parallel/agent results. +// 3. Prefer schema for structured returns. +// 4. Guard budget loops with budget.total && … +// 5. No Date.now / Math.random in scripts. +// 6. isolation:'worktree' only for parallel mutators. +// 7. log() anything a silent cap would hide. +// 8. Hybrid: scout → Workflow → synthesize → maybe next phase.
+
+ +
+
+

Primitive count

+

8

+

agent · pipeline · parallel · phase · log · args · budget · workflow

+
+
+

Tool inputs

+

5

+

script · name · scriptPath · args · resumeFromRunId

+
+
+

Design goal

+

Altitude

+

Strong model plans; many workers execute; engine enforces the graph

+
+
+ +

+ Source of truth for this document: Claude Code binary tool description for the Workflow tool (v2.1.x family), observed script examples under session workflows/scripts/, and the runtime rules encoded in the tool prompt (opt-in, ultracode, resume, budget, concurrency). This is a model-facing API reference, not Anthropic product documentation. +

+
+ +
+
+ + From 28f3e5b8e6077bbacc389e0877bdd42ee66748be Mon Sep 17 00:00:00 2001 From: Waishnav Date: Tue, 21 Jul 2026 17:01:59 +0530 Subject: [PATCH 4/6] feat(workflow): freeze public types and limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add workflow-types contract module (statuses, AgentOpts, journal shapes, cache key helper, budget stub, concurrency). Wire test into package.json. Fix primitives-spec §13: isolation worktree is v1 must-have. Co-Authored-By: Claude --- .../devspace/primitives-spec.md | 2 +- package.json | 2 +- src/workflow-types.test.ts | 63 ++++ src/workflow-types.ts | 291 ++++++++++++++++++ 4 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 src/workflow-types.test.ts create mode 100644 src/workflow-types.ts diff --git a/docs/dynamic-workflow/devspace/primitives-spec.md b/docs/dynamic-workflow/devspace/primitives-spec.md index ef53d596..042b9715 100644 --- a/docs/dynamic-workflow/devspace/primitives-spec.md +++ b/docs/dynamic-workflow/devspace/primitives-spec.md @@ -753,7 +753,7 @@ IMPLEMENTER (parallel): | Shared workspace; later agents see prior edits on disk | Yes | | Return structured paths/findings between stages | Yes (schema) | | Auto git snapshot / diff after each agent | **No** | -| Per-agent worktree | **No** (follow-up) | +| Per-agent worktree (`isolation: 'worktree'`) | **Yes v1** (must-have; §7.1b) | | Host `show_changes` after whole workflow | Optional host behavior; not engine | --- diff --git a/package.json b/package.json index 25f21059..ee020b1a 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dev": "node scripts/dev-server.mjs", "postinstall": "node scripts/fix-node-pty-permissions.mjs", "start": "node dist/cli.js serve", - "test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts", + "test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts && tsx src/workflow-types.test.ts", "typecheck": "tsc -p tsconfig.json --noEmit" }, "keywords": [], diff --git a/src/workflow-types.test.ts b/src/workflow-types.test.ts new file mode 100644 index 00000000..374e167d --- /dev/null +++ b/src/workflow-types.test.ts @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import { + WORKFLOW_MAX_ITEMS, + WORKFLOW_MAX_NEST_DEPTH, + buildAgentCacheKeyInput, + createStubBudget, + defaultWorkflowConcurrency, + resolveWorkflowConcurrency, +} from "./workflow-types.js"; + +assert.equal(WORKFLOW_MAX_ITEMS, 4096); +assert.equal(WORKFLOW_MAX_NEST_DEPTH, 1); + +assert.deepEqual( + buildAgentCacheKeyInput({ + prompt: "hi", + provider: "codex", + model: undefined, + effort: "high", + schema: null, + isolation: "worktree", + }), + { + prompt: "hi", + provider: "codex", + model: null, + effort: "high", + schema: null, + isolation: "worktree", + }, +); + +assert.deepEqual( + buildAgentCacheKeyInput({ + prompt: "x", + provider: "claude", + }), + { + prompt: "x", + provider: "claude", + model: null, + effort: null, + schema: null, + isolation: "shared", + }, +); + +const budget = createStubBudget(); +assert.equal(budget.total, null); +assert.equal(budget.spent(), 0); +assert.equal(budget.remaining(), Infinity); + +assert.equal(defaultWorkflowConcurrency(8), 6); +assert.equal(defaultWorkflowConcurrency(2), 1); +assert.equal(defaultWorkflowConcurrency(1), 1); +assert.equal(defaultWorkflowConcurrency(32), 16); + +assert.equal(resolveWorkflowConcurrency(undefined, 8), 6); +assert.equal(resolveWorkflowConcurrency(2, 8), 2); +assert.equal(resolveWorkflowConcurrency(100, 8), 6); +assert.equal(resolveWorkflowConcurrency(0, 8), 1); + +console.log("workflow-types.test.ts: ok"); diff --git a/src/workflow-types.ts b/src/workflow-types.ts new file mode 100644 index 00000000..11b14193 --- /dev/null +++ b/src/workflow-types.ts @@ -0,0 +1,291 @@ +/** + * Frozen contracts for DevSpace Dynamic Workflows. + * Engine modules must import these rather than invent parallel shapes. + * + * Locks: + * - No writeMode on AgentOpts (prompt RO/write + isolation containment). + * - budget is a stub shape in v1. + * - nest depth 1; max pipeline/parallel items 4096. + * - concurrency default min(16, max(1, availableParallelism()-2)). + */ + +import type { LocalAgentProvider } from "./local-agent-profiles.js"; + +// --------------------------------------------------------------------------- +// Limits +// --------------------------------------------------------------------------- + +export const WORKFLOW_MAX_ITEMS = 4096; +export const WORKFLOW_MAX_NEST_DEPTH = 1; +export const WORKFLOW_MAX_SCHEMA_RETRIES = 2; +export const WORKFLOW_HEARTBEAT_MS = 5_000; +export const WORKFLOW_CANCEL_HARD_MS = 5_000; +export const WORKFLOW_HOST_TIMEOUT_MS = 6 * 60 * 60 * 1000; +export const WORKFLOW_MCP_YIELD_MS = 110_000; + +/** Soft/hard transport + storage caps (not semantic coverage truncation). */ +export const WORKFLOW_LIMITS = { + eventDataJsonBytes: 8 * 1024, + responseTextBytes: 1 * 1024 * 1024, + structuredJsonBytes: 256 * 1024, + resultJsonBytes: 256 * 1024, + argsJsonBytes: 64 * 1024, + scriptSourceBytes: 512 * 1024, + eventDrainDefault: 200, + eventDrainMax: 500, +} as const; + +// --------------------------------------------------------------------------- +// Provider config (user config / ServerConfig) +// --------------------------------------------------------------------------- + +export type AgentProviderId = LocalAgentProvider; + +export interface AgentProviderProbe { + id: AgentProviderId; + available: boolean; + detail?: string; +} + +/** + * Ordered enable-list. index 0 = default fallback after live availability filter. + * Missing block on disk → compat all-available in product order. + * Explicit enabled: [] → no providers; first agent() fails. + */ +export interface AgentProvidersConfig { + enabled: AgentProviderId[]; + detectedAt?: string; + lastProbe?: AgentProviderProbe[]; +} + +// --------------------------------------------------------------------------- +// Script meta + agent opts +// --------------------------------------------------------------------------- + +export interface WorkflowPhaseMeta { + title: string; + detail?: string; +} + +export interface WorkflowMeta { + name: string; + description: string; + phases?: WorkflowPhaseMeta[]; + whenToUse?: string; + /** DevSpace extension */ + defaultProvider?: AgentProviderId; + /** DevSpace extension; clamped to engine max */ + concurrency?: number; +} + +/** + * Public agent() options. Deliberately no writeMode. + */ +export interface AgentOpts { + label?: string; + phase?: string; + schema?: object; + model?: string; + /** Provider-native effort/reasoning level (was thinking). */ + effort?: string; + provider?: AgentProviderId | string; + isolation?: "worktree"; +} + +export type AgentIsolationMode = "shared" | "worktree"; + +// --------------------------------------------------------------------------- +// Status / events +// --------------------------------------------------------------------------- + +export type WorkflowRunStatus = + | "starting" + | "running" + | "completed" + | "failed" + | "cancelled"; + +export type WorkflowAgentCallStatus = + | "running" + | "completed" + | "failed" + | "cancelled" + | "from_cache"; + +export type WorkflowEventType = + | "run_started" + | "run_completed" + | "run_failed" + | "run_cancelled" + | "phase_started" + | "log" + | "agent_call_started" + | "agent_call_completed" + | "agent_call_failed" + | "agent_call_cached" + | "schema_retry" + | "worktree_created" + | "worktree_finalized"; + +export type WorkflowErrorKind = + | "syntax" + | "meta" + | "determinism" + | "provider_disabled" + | "provider_unavailable" + | "no_provider" + | "provider" + | "schema" + | "cancelled" + | "timeout" + | "heartbeat" + | "worktree" + | "nest_depth" + | "path" + | "result_too_large" + | "args_too_large" + | "script_too_large" + | "internal"; + +// --------------------------------------------------------------------------- +// Journal row shapes (behavioral; store maps snake_case) +// --------------------------------------------------------------------------- + +export type WorkflowRunSource = "inline" | "named" | "resume"; + +export interface WorkflowRunRecord { + id: string; + name: string; + source: WorkflowRunSource; + scriptPath: string; + scriptHash: string; + workspaceRoot: string; + workspaceId?: string; + argsJson: string; + status: WorkflowRunStatus; + error?: string; + errorKind?: WorkflowErrorKind; + resultJson?: string; + pid?: number; + heartbeatAt?: string; + cancelRequested: boolean; + resumedFromRunId?: string; + /** Pinned at run start for isolation: worktree reproducibility. */ + baseSha?: string; + createdAt: string; + startedAt?: string; + completedAt?: string; + updatedAt: string; +} + +export interface WorkflowEventRecord { + runId: string; + seq: number; + type: WorkflowEventType; + phase?: string; + label?: string; + dataJson: string; + createdAt: string; +} + +export interface WorkflowAgentCallRecord { + runId: string; + callIndex: number; + cacheKey: string; + provider: string; + model?: string; + effort?: string; + label?: string; + phase?: string; + status: WorkflowAgentCallStatus; + fromCache: boolean; + providerSessionId?: string; + responseText?: string; + structuredJson?: string; + error?: string; + isolation: AgentIsolationMode; + worktreePath?: string; + dirty?: boolean; + createdAt: string; + startedAt?: string; + completedAt?: string; + updatedAt: string; +} + +// --------------------------------------------------------------------------- +// Cache key +// --------------------------------------------------------------------------- + +/** + * Canonical fields for agent() resume identity. + * Field order for JSON serialization is fixed by buildAgentCacheKeyInput. + */ +export interface AgentCacheKeyInput { + prompt: string; + provider: string; + model: string | null; + effort: string | null; + schema: object | null; + isolation: AgentIsolationMode; +} + +export function buildAgentCacheKeyInput(input: { + prompt: string; + provider: string; + model?: string | null; + effort?: string | null; + schema?: object | null; + isolation?: AgentIsolationMode | "worktree" | null; +}): AgentCacheKeyInput { + const isolation: AgentIsolationMode = + input.isolation === "worktree" ? "worktree" : "shared"; + return { + prompt: input.prompt, + provider: input.provider, + model: input.model ?? null, + effort: input.effort ?? null, + schema: input.schema ?? null, + isolation, + }; +} + +// --------------------------------------------------------------------------- +// Budget stub (CC-shaped) +// --------------------------------------------------------------------------- + +export interface WorkflowBudget { + readonly total: number | null; + spent(): number; + remaining(): number; +} + +export function createStubBudget(): WorkflowBudget { + return Object.freeze({ + total: null, + spent(): number { + return 0; + }, + remaining(): number { + return Infinity; + }, + }); +} + +// --------------------------------------------------------------------------- +// Concurrency helper +// --------------------------------------------------------------------------- + +export function defaultWorkflowConcurrency(availableParallelism: number): number { + return Math.min(16, Math.max(1, availableParallelism - 2)); +} + +export function resolveWorkflowConcurrency( + metaConcurrency: number | undefined, + availableParallelism: number, +): number { + const base = defaultWorkflowConcurrency(availableParallelism); + if (metaConcurrency === undefined || !Number.isFinite(metaConcurrency)) return base; + const n = Math.floor(metaConcurrency); + if (n < 1) return 1; + return Math.min(base, n); +} From f01a96ec3c3a37e2c80874604b948c8efd86872e Mon Sep 17 00:00:00 2001 From: Waishnav Date: Tue, 21 Jul 2026 17:05:26 +0530 Subject: [PATCH 5/6] refactor(agents): rename thinking to effort Normalize LocalAgentRunInput, profiles, CLI, store, and adapters on effort. CLI accepts --thinking as a one-release alias; profile YAML thinking: still maps to effort. DB migration v4 renames the column. Co-Authored-By: Claude --- src/cli.test.ts | 6 ++--- src/cli.ts | 20 ++++++++-------- src/db/migrations.ts | 19 +++++++++++++++ src/db/schema.ts | 2 +- src/local-agent-adapters.test.ts | 16 ++++++------- src/local-agent-adapters.ts | 21 ++++++++++------- src/local-agent-profiles.test.ts | 40 +++++++++++++++++++++++++++++--- src/local-agent-profiles.ts | 9 +++---- src/local-agent-runtime.test.ts | 2 +- src/local-agent-runtime.ts | 5 ++-- src/local-agent-store.test.ts | 10 ++++---- src/local-agent-store.ts | 18 +++++++------- src/local-agent-targets.test.ts | 36 +++++++++++++++++----------- src/local-agent-targets.ts | 38 +++++++++++++++++++----------- src/oauth-store.test.ts | 1 + src/server.ts | 8 +++---- 16 files changed, 164 insertions(+), 87 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 97b7084a..95048e64 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -35,7 +35,7 @@ try { "description: Read-only reviewer.", "provider: codex", "model: gpt-5.4", - "thinking: high", + "effort: high", "---", "", "Review only.", @@ -50,7 +50,7 @@ try { profileName: "reviewer", provider: "codex", model: "gpt-5.4", - thinking: "high", + effort: "high", }).id, { status: "idle" }, ); @@ -80,7 +80,7 @@ try { }, }); - assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4 thinking=high`)); + assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4 effort=high`)); assert.doesNotMatch(output, /profile reviewer/); assert.doesNotMatch(output, new RegExp(other.id)); diff --git a/src/cli.ts b/src/cli.ts index 7a1ac63f..88dffa90 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -379,7 +379,7 @@ async function runAgentsRun(args: string[]): Promise { store.update(existing.id, { status: "starting", model: parsed.model ?? existing.model, - thinking: parsed.thinking ?? existing.thinking, + effort: parsed.effort ?? existing.effort, latestResponse: undefined, error: undefined, }); @@ -388,13 +388,13 @@ async function runAgentsRun(args: string[]): Promise { ...existing, status: "running", model: parsed.model ?? existing.model, - thinking: parsed.thinking ?? existing.thinking, + effort: parsed.effort ?? existing.effort, })); return; } const profiles = await loadLocalAgentProfiles(config, workspaceRoot); - const target = resolveLocalAgentTarget(parsed.target, profiles, parsed.model, parsed.thinking); + const target = resolveLocalAgentTarget(parsed.target, profiles, parsed.model, parsed.effort); if (!target) { throw new Error( `Unknown subagent profile, provider, or id: ${parsed.target}. Available ${formatAvailableLocalAgentTargets(profiles)}`, @@ -409,7 +409,7 @@ async function runAgentsRun(args: string[]): Promise { profileName: target.name, provider: target.provider, model: target.model, - thinking: target.thinking, + effort: target.effort, }); spawnAgentWorker(record.id, promptFile); @@ -491,7 +491,7 @@ async function runLocalAgentProfile( providerSessionId: record.providerSessionId, writeMode: "allowed", model: record.model ?? profile.model, - thinking: record.thinking ?? profile.thinking, + effort: record.effort ?? profile.effort, }); } @@ -509,7 +509,7 @@ async function runRawLocalAgentProvider( providerSessionId: record.providerSessionId, writeMode: "allowed", model: record.model, - thinking: record.thinking, + effort: record.effort, }); } @@ -550,11 +550,11 @@ function resolveCurrentWorkspaceScope(): { workspaceId?: string; workspaceRoot: function formatAgentLine(agent: Pick< LocalAgentRecord, - "id" | "status" | "profileName" | "provider" | "model" | "thinking" + "id" | "status" | "profileName" | "provider" | "model" | "effort" >): string { const model = agent.model ? ` ${agent.model}` : ""; - const thinking = agent.thinking ? ` thinking=${agent.thinking}` : ""; - return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}${thinking}`; + const effort = agent.effort ? ` effort=${agent.effort}` : ""; + return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}${effort}`; } function sleep(ms: number): Promise { @@ -568,7 +568,7 @@ function printAgentsHelp(): void { "", "Usage:", " devspace agents ls", - " devspace agents run [--model ] [--thinking ] ", + " devspace agents run [--model ] [--effort ] ", " devspace agents show ", ].join("\n"), ); diff --git a/src/db/migrations.ts b/src/db/migrations.ts index 2bda20c2..4888fffe 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -22,6 +22,11 @@ const migrations: Migration[] = [ name: "local-agent-sessions", up: migrateLocalAgentSessions, }, + { + version: 4, + name: "local-agent-effort-rename", + up: migrateLocalAgentEffortRename, + }, ]; export function migrateDatabase(sqlite: Database.Database): void { @@ -174,6 +179,20 @@ function migrateLocalAgentSessions(sqlite: Database.Database): void { addColumnIfMissing(sqlite, "local_agent_sessions", "thinking", "text"); } +/** Rename thinking → effort on local_agent_sessions (SQLite 3.25+). */ +function migrateLocalAgentEffortRename(sqlite: Database.Database): void { + const columns = sqlite.prepare("pragma table_info(local_agent_sessions)").all() as Array<{ + name: string; + }>; + const names = new Set(columns.map((column) => column.name)); + if (names.has("effort")) return; + if (!names.has("thinking")) { + addColumnIfMissing(sqlite, "local_agent_sessions", "effort", "text"); + return; + } + sqlite.exec("alter table local_agent_sessions rename column thinking to effort"); +} + function addColumnIfMissing( sqlite: Database.Database, table: "workspace_sessions" | "local_agent_sessions", diff --git a/src/db/schema.ts b/src/db/schema.ts index 01d13fac..cb46ecf1 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -82,7 +82,7 @@ export const localAgentSessions = sqliteTable( profileName: text("profile_name").notNull(), provider: text("provider").notNull(), model: text("model"), - thinking: text("thinking"), + effort: text("effort"), providerSessionId: text("provider_session_id"), status: text("status").notNull(), latestResponse: text("latest_response"), diff --git a/src/local-agent-adapters.test.ts b/src/local-agent-adapters.test.ts index 0e4cbf0f..19c7dd87 100644 --- a/src/local-agent-adapters.test.ts +++ b/src/local-agent-adapters.test.ts @@ -9,7 +9,7 @@ import { extractPiStreamingText, piCommandEnvironment, resolveAcpModelConfigUpdate, - resolveAcpThinkingConfigUpdate, + resolveAcpEffortConfigUpdate, } from "./local-agent-adapters.js"; import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js"; import type { LocalAgentProvider } from "./local-agent-profiles.js"; @@ -111,7 +111,7 @@ assert.throws( ); assert.deepEqual( - resolveAcpThinkingConfigUpdate({ + resolveAcpEffortConfigUpdate({ sessionId: "session_1", newSessionResponse: { configOptions: [ @@ -131,7 +131,7 @@ assert.deepEqual( ); assert.deepEqual( - resolveAcpThinkingConfigUpdate({ + resolveAcpEffortConfigUpdate({ sessionId: "session_2", newSessionResponse: { configOptions: [ @@ -157,7 +157,7 @@ assert.deepEqual( ); assert.throws( - () => resolveAcpThinkingConfigUpdate({ + () => resolveAcpEffortConfigUpdate({ sessionId: "session_3", newSessionResponse: { configOptions: [ @@ -174,21 +174,21 @@ assert.throws( ); assert.throws( - () => resolveAcpThinkingConfigUpdate(undefined, "high", "copilot"), + () => resolveAcpEffortConfigUpdate(undefined, "high", "copilot"), /session metadata/, ); assert.throws( - () => resolveAcpThinkingConfigUpdate({ newSessionResponse: { configOptions: [] } }, "high", "copilot"), + () => resolveAcpEffortConfigUpdate({ newSessionResponse: { configOptions: [] } }, "high", "copilot"), /session id/, ); assert.throws( - () => resolveAcpThinkingConfigUpdate({ + () => resolveAcpEffortConfigUpdate({ sessionId: "session_4", newSessionResponse: { configOptions: [] }, }, "high", "copilot"), - /does not expose a thinking option/, + /does not expose a effort option/, ); { diff --git a/src/local-agent-adapters.ts b/src/local-agent-adapters.ts index 457b8e08..68e8b663 100644 --- a/src/local-agent-adapters.ts +++ b/src/local-agent-adapters.ts @@ -64,7 +64,7 @@ class ClaudeLocalAgentAdapter implements LocalAgentAdapter { options: { cwd: input.workspace, model: input.model, - ...(input.thinking ? { thinking: { type: "adaptive" } as const, effort: input.thinking as EffortLevel } : {}), + ...(input.effort ? { thinking: { type: "adaptive" } as const, effort: input.effort as EffortLevel } : {}), resume: input.providerSessionId, permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, @@ -209,8 +209,8 @@ class AcpLocalAgentAdapter implements LocalAgentAdapter { const config = resolveAcpModelConfigUpdate(session, input.model, this.provider); await context.request(methods.agent.session.setConfigOption, config); } - if (input.thinking) { - const config = resolveAcpThinkingConfigUpdate(session, input.thinking, this.provider); + if (input.effort) { + const config = resolveAcpEffortConfigUpdate(session, input.effort, this.provider); await context.request(methods.agent.session.setConfigOption, config); } const prompt = session.prompt(input.prompt); @@ -258,19 +258,22 @@ export function resolveAcpModelConfigUpdate( }); } -export function resolveAcpThinkingConfigUpdate( +export function resolveAcpEffortConfigUpdate( session: unknown, - thinking: string, + effort: string, provider: string, ): { sessionId: string; configId: string; value: string } { return resolveAcpSelectConfigUpdate(session, { category: "thought_level", - label: "thinking option", + label: "effort option", provider, - value: thinking, + value: effort, }); } +/** @deprecated Use resolveAcpEffortConfigUpdate */ +export const resolveAcpThinkingConfigUpdate = resolveAcpEffortConfigUpdate; + function resolveAcpSelectConfigUpdate( session: unknown, options: { @@ -336,7 +339,7 @@ class PiRpcLocalAgentAdapter implements LocalAgentAdapter { async run(input: LocalAgentRunInput): Promise { const args = ["--mode", "rpc"]; if (input.model) args.push("--model", input.model); - if (input.thinking) args.push("--thinking", input.thinking); + if (input.effort) args.push("--thinking", input.effort); if (input.providerSessionId) args.push("--session", input.providerSessionId); const child = spawn(process.env.PI_COMMAND ?? "pi", args, { cwd: input.workspace, @@ -524,7 +527,7 @@ async function promptOpencodeSession( prompt: { parts: [{ type: "text", text: input.prompt }] }, parts: [{ type: "text", text: input.prompt }], ...(input.model ? { model: parseOpencodeModel(input.model) } : {}), - ...(input.thinking ? { variant: input.thinking } : {}), + ...(input.effort ? { variant: input.effort } : {}), }; return session.prompt(promptInput, { throwOnError: true }); } diff --git a/src/local-agent-profiles.test.ts b/src/local-agent-profiles.test.ts index 7665b9f8..b30f9474 100644 --- a/src/local-agent-profiles.test.ts +++ b/src/local-agent-profiles.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadConfig } from "./config.js"; import { loadLocalAgentProfiles, summarizeLocalAgentProfile } from "./local-agent-profiles.js"; +import type { ServerConfig } from "./config.js"; const root = await mkdtemp(join(tmpdir(), "devspace-agent-profiles-test-")); @@ -35,7 +36,7 @@ try { 'description: "Project reviewer #1."', "provider: claude", "model: sonnet", - "thinking: high", + "effort: high", "---", "", "Project body.", @@ -70,14 +71,14 @@ try { assert.equal(profiles[0]?.description, "Project reviewer #1."); assert.equal(profiles[0]?.provider, "claude"); assert.equal(profiles[0]?.model, "sonnet"); - assert.equal(profiles[0]?.thinking, "high"); + assert.equal(profiles[0]?.effort, "high"); assert.equal(profiles[0]?.body, "Project body."); assert.deepEqual(summarizeLocalAgentProfile(profiles[0]!), { name: "reviewer", description: "Project reviewer #1.", provider: "claude", model: "sonnet", - thinking: "high", + effort: "high", }); await writeFile( @@ -106,3 +107,36 @@ try { } finally { await rm(root, { recursive: true, force: true }); } + +// legacy thinking: maps to effort +{ + const legacyRoot = await mkdtemp(join(tmpdir(), "devspace-profile-legacy-")); + try { + const dir = join(legacyRoot, "agents"); + await mkdir(dir, { recursive: true }); + await writeFile( + join(dir, "legacy.md"), + [ + "---", + "name: legacy", + "description: Legacy thinking key.", + "provider: codex", + "thinking: medium", + "---", + "", + "Body.", + "", + ].join("\n"), + ); + const profiles = await loadLocalAgentProfiles( + { + subagents: true, + devspaceAgentsDir: dir, + } as ServerConfig, + legacyRoot, + ); + assert.equal(profiles[0]?.effort, "medium"); + } finally { + await rm(legacyRoot, { recursive: true, force: true }); + } +} diff --git a/src/local-agent-profiles.ts b/src/local-agent-profiles.ts index a7fa0d87..f29316dd 100644 --- a/src/local-agent-profiles.ts +++ b/src/local-agent-profiles.ts @@ -20,7 +20,7 @@ export interface LocalAgentProfile { description: string; provider: LocalAgentProvider; model?: string; - thinking?: string; + effort?: string; filePath: string; body: string; disabled: boolean; @@ -31,7 +31,7 @@ export interface LocalAgentProfileSummary { description: string; provider: LocalAgentProvider; model?: string; - thinking?: string; + effort?: string; } interface ParsedFrontmatter { @@ -73,7 +73,7 @@ export function summarizeLocalAgentProfile( description: profile.description, provider: profile.provider, model: profile.model, - thinking: profile.thinking, + effort: profile.effort, }; } @@ -157,7 +157,8 @@ function profileFromFrontmatter( description, provider, model: readString(frontmatter, "model"), - thinking: readString(frontmatter, "thinking"), + // Prefer effort; accept legacy thinking for one release. + effort: readString(frontmatter, "effort") ?? readString(frontmatter, "thinking"), filePath, body, disabled: frontmatter.disabled === true, diff --git a/src/local-agent-runtime.test.ts b/src/local-agent-runtime.test.ts index 1d45d166..491623f6 100644 --- a/src/local-agent-runtime.test.ts +++ b/src/local-agent-runtime.test.ts @@ -63,7 +63,7 @@ await runtime.run({ workspace: "/tmp/project", writeMode: "allowed", model: "gpt-5.4", - thinking: "high", + effort: "high", }); assert.deepEqual(codex.started[1], { diff --git a/src/local-agent-runtime.ts b/src/local-agent-runtime.ts index 54130c2e..ebd99feb 100644 --- a/src/local-agent-runtime.ts +++ b/src/local-agent-runtime.ts @@ -15,7 +15,8 @@ export interface LocalAgentRunInput { providerSessionId?: string; writeMode?: LocalAgentWriteMode; model?: string; - thinking?: string; + /** Provider-native effort / reasoning level (was thinking). */ + effort?: string; } export interface LocalAgentRunResult { @@ -60,7 +61,7 @@ function threadOptionsFor(input: LocalAgentRunInput): ThreadOptions { sandboxMode: sandboxModeFor(input.writeMode), approvalPolicy: "never", model: input.model, - modelReasoningEffort: input.thinking as ModelReasoningEffort | undefined, + modelReasoningEffort: input.effort as ModelReasoningEffort | undefined, }; } diff --git a/src/local-agent-store.test.ts b/src/local-agent-store.test.ts index cf7265a9..05adcbcc 100644 --- a/src/local-agent-store.test.ts +++ b/src/local-agent-store.test.ts @@ -16,12 +16,12 @@ try { profileName: "reviewer", provider: "codex", model: "gpt-5.4", - thinking: "high", + effort: "high", }); assert.match(created.id, /^agt_[a-f0-9]{8}$/); assert.equal(created.status, "starting"); - assert.equal(store.get(created.id)?.thinking, "high"); + assert.equal(store.get(created.id)?.effort, "high"); assert.equal(store.get(created.id)?.profileName, "reviewer"); assert.equal(store.get(created.id.slice(0, 7))?.id, created.id); @@ -29,13 +29,13 @@ try { status: "idle", latestResponse: "done", providerSessionId: "thread_123", - thinking: "medium", + effort: "medium", }); assert.equal(updated.status, "idle"); - assert.equal(updated.thinking, "medium"); + assert.equal(updated.effort, "medium"); assert.equal(store.get("thread_123")?.id, created.id); - assert.equal(store.get(created.id)?.thinking, "medium"); + assert.equal(store.get(created.id)?.effort, "medium"); assert.equal(store.update(created.id, { latestResponse: undefined }).latestResponse, undefined); assert.deepEqual( store.list({ workspaceRoot: join(root, "project") }).map((agent) => agent.latestResponse), diff --git a/src/local-agent-store.ts b/src/local-agent-store.ts index a850ca9f..a87a0b1e 100644 --- a/src/local-agent-store.ts +++ b/src/local-agent-store.ts @@ -12,7 +12,7 @@ export interface LocalAgentRecord { profileName: string; provider: string; model?: string; - thinking?: string; + effort?: string; providerSessionId?: string; status: LocalAgentStatus; latestResponse?: string; @@ -27,7 +27,7 @@ export interface CreateLocalAgentRecordInput { profileName: string; provider: string; model?: string; - thinking?: string; + effort?: string; } export interface LocalAgentListScope { @@ -42,7 +42,7 @@ interface LocalAgentRow { profile_name: string; provider: string; model: string | null; - thinking: string | null; + effort: string | null; provider_session_id: string | null; status: string; latest_response: string | null; @@ -94,7 +94,7 @@ export class LocalAgentStore { profileName: input.profileName, provider: input.provider, model: input.model, - thinking: input.thinking, + effort: input.effort, status: "starting", createdAt: now, updatedAt: now, @@ -109,7 +109,7 @@ export class LocalAgentStore { profile_name, provider, model, - thinking, + effort, status, created_at, updated_at @@ -122,7 +122,7 @@ export class LocalAgentStore { record.profileName, record.provider, record.model ?? null, - record.thinking ?? null, + record.effort ?? null, record.status, record.createdAt, record.updatedAt, @@ -170,7 +170,7 @@ export class LocalAgentStore { profile_name = ?, provider = ?, model = ?, - thinking = ?, + effort = ?, provider_session_id = ?, status = ?, latest_response = ?, @@ -184,7 +184,7 @@ export class LocalAgentStore { updated.profileName, updated.provider, updated.model ?? null, - updated.thinking ?? null, + updated.effort ?? null, updated.providerSessionId ?? null, updated.status, updated.latestResponse ?? null, @@ -220,7 +220,7 @@ function rowToLocalAgentRecord(row: LocalAgentRow): LocalAgentRecord { profileName: row.profile_name, provider: row.provider, model: row.model ?? undefined, - thinking: row.thinking ?? undefined, + effort: row.effort ?? undefined, providerSessionId: row.provider_session_id ?? undefined, status: readStatus(row.status), latestResponse: row.latest_response ?? undefined, diff --git a/src/local-agent-targets.test.ts b/src/local-agent-targets.test.ts index 3f1ae08f..737f970a 100644 --- a/src/local-agent-targets.test.ts +++ b/src/local-agent-targets.test.ts @@ -12,7 +12,7 @@ const profiles: LocalAgentProfile[] = [ description: "Review changes.", provider: "codex", model: "gpt-5-codex", - thinking: "high", + effort: "high", filePath: "/workspace/.devspace/agents/reviewer.md", body: "Review carefully.", disabled: false, @@ -32,35 +32,43 @@ assert.deepEqual(parseLocalAgentRunArgs(["codex", "hello", "world"]), { target: "codex", prompt: "hello world", model: undefined, - thinking: undefined, + effort: undefined, }); assert.deepEqual(parseLocalAgentRunArgs(["codex", "--model", "gpt-5.1", "hello"]), { target: "codex", prompt: "hello", model: "gpt-5.1", - thinking: undefined, + effort: undefined, }); assert.deepEqual(parseLocalAgentRunArgs(["codex", "--model=gpt-5.1", "hello"]), { target: "codex", prompt: "hello", model: "gpt-5.1", - thinking: undefined, + effort: undefined, }); -assert.deepEqual(parseLocalAgentRunArgs(["codex", "--thinking", "high", "hello"]), { +assert.deepEqual(parseLocalAgentRunArgs(["codex", "--effort", "high", "hello"]), { target: "codex", prompt: "hello", model: undefined, - thinking: "high", + effort: "high", }); -assert.deepEqual(parseLocalAgentRunArgs(["codex", "--thinking=high", "hello"]), { +assert.deepEqual(parseLocalAgentRunArgs(["codex", "--effort=high", "hello"]), { + target: "codex", + prompt: "hello", + model: undefined, + effort: "high", +}); + +// Legacy --thinking alias maps to effort. +assert.deepEqual(parseLocalAgentRunArgs(["codex", "--thinking", "high", "hello"]), { target: "codex", prompt: "hello", model: undefined, - thinking: "high", + effort: "high", }); assert.throws( @@ -69,8 +77,8 @@ assert.throws( ); assert.throws( - () => parseLocalAgentRunArgs(["codex", "--thinking"]), - /Missing value for --thinking/, + () => parseLocalAgentRunArgs(["codex", "--effort"]), + /Missing value for --effort/, ); { @@ -79,14 +87,14 @@ assert.throws( assert.equal(target?.name, "reviewer"); assert.equal(target?.provider, "codex"); assert.equal(target?.model, "gpt-5-codex"); - assert.equal(target?.thinking, "high"); + assert.equal(target?.effort, "high"); } { const target = resolveLocalAgentTarget("reviewer", profiles, "gpt-5.2", "xhigh"); assert.equal(target?.kind, "profile"); assert.equal(target?.model, "gpt-5.2"); - assert.equal(target?.thinking, "xhigh"); + assert.equal(target?.effort, "xhigh"); } { @@ -95,14 +103,14 @@ assert.throws( assert.equal(target?.name, "opencode"); assert.equal(target?.provider, "opencode"); assert.equal(target?.model, undefined); - assert.equal(target?.thinking, undefined); + assert.equal(target?.effort, undefined); } { const target = resolveLocalAgentTarget("opencode", profiles, "kimi-k2", "deep"); assert.equal(target?.kind, "provider"); assert.equal(target?.model, "kimi-k2"); - assert.equal(target?.thinking, "deep"); + assert.equal(target?.effort, "deep"); } { diff --git a/src/local-agent-targets.ts b/src/local-agent-targets.ts index 917e2804..77d27930 100644 --- a/src/local-agent-targets.ts +++ b/src/local-agent-targets.ts @@ -9,7 +9,7 @@ export interface ParsedLocalAgentRunArgs { target: string; prompt: string; model?: string; - thinking?: string; + effort?: string; } export type LocalAgentTarget = @@ -18,7 +18,7 @@ export type LocalAgentTarget = name: string; provider: LocalAgentProvider; model?: string; - thinking?: string; + effort?: string; profile: LocalAgentProfile; } | { @@ -26,17 +26,20 @@ export type LocalAgentTarget = name: LocalAgentProvider; provider: LocalAgentProvider; model?: string; - thinking?: string; + effort?: string; }; +const USAGE = + 'Usage: devspace agents run [--model ] [--effort ] ""'; + export function parseLocalAgentRunArgs(args: string[]): ParsedLocalAgentRunArgs { const [target, ...rest] = args; if (!target) { - throw new Error('Usage: devspace agents run [--model ] [--thinking ] ""'); + throw new Error(USAGE); } let model: string | undefined; - let thinking: string | undefined; + let effort: string | undefined; const promptParts: string[] = []; for (let index = 0; index < rest.length; index += 1) { const part = rest[index]; @@ -53,17 +56,24 @@ export function parseLocalAgentRunArgs(args: string[]): ParsedLocalAgentRunArgs model = value; continue; } - if (part === "--thinking") { + if (part === "--effort" || part === "--thinking") { + const flag = part; const value = rest[index + 1]?.trim(); - if (!value) throw new Error("Missing value for --thinking."); - thinking = value; + if (!value) throw new Error(`Missing value for ${flag}.`); + effort = value; index += 1; continue; } + if (part?.startsWith("--effort=")) { + const value = part.slice("--effort=".length).trim(); + if (!value) throw new Error("Missing value for --effort."); + effort = value; + continue; + } if (part?.startsWith("--thinking=")) { const value = part.slice("--thinking=".length).trim(); if (!value) throw new Error("Missing value for --thinking."); - thinking = value; + effort = value; continue; } promptParts.push(part ?? ""); @@ -71,17 +81,17 @@ export function parseLocalAgentRunArgs(args: string[]): ParsedLocalAgentRunArgs const prompt = promptParts.join(" ").trim(); if (!prompt) { - throw new Error('Usage: devspace agents run [--model ] [--thinking ] ""'); + throw new Error(USAGE); } - return { target, prompt, model, thinking }; + return { target, prompt, model, effort }; } export function resolveLocalAgentTarget( target: string, profiles: LocalAgentProfile[], modelOverride?: string, - thinkingOverride?: string, + effortOverride?: string, ): LocalAgentTarget | undefined { const profile = profiles.find((candidate) => candidate.name === target); if (profile) { @@ -90,7 +100,7 @@ export function resolveLocalAgentTarget( name: profile.name, provider: profile.provider, model: modelOverride ?? profile.model, - thinking: thinkingOverride ?? profile.thinking, + effort: effortOverride ?? profile.effort, profile, }; } @@ -101,7 +111,7 @@ export function resolveLocalAgentTarget( name: target, provider: target, model: modelOverride, - thinking: thinkingOverride, + effort: effortOverride, }; } diff --git a/src/oauth-store.test.ts b/src/oauth-store.test.ts index e1c00338..032f5c03 100644 --- a/src/oauth-store.test.ts +++ b/src/oauth-store.test.ts @@ -44,6 +44,7 @@ async function testDatabaseConfiguration(stateDir: string): Promise { { version: 1, name: "workspace-state" }, { version: 2, name: "oauth-state" }, { version: 3, name: "local-agent-sessions" }, + { version: 4, name: "local-agent-effort-rename" }, ]); } finally { database.close(); diff --git a/src/server.ts b/src/server.ts index 7dd9629e..e47aeb1e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -205,16 +205,16 @@ function formatVisibleAgent(agent: { name: string; provider: string; model?: string; - thinking?: string; + effort?: string; providerAvailable?: boolean; providerUnavailableReason?: string; }): string { const model = agent.model ? `, model ${agent.model}` : ""; - const thinking = agent.thinking ? `, thinking ${agent.thinking}` : ""; + const effort = agent.effort ? `, effort ${agent.effort}` : ""; const availability = agent.providerAvailable === false ? `, unavailable: ${agent.providerUnavailableReason ?? "provider unavailable"}` : ""; - return `${agent.name} (${agent.provider}${model}${thinking}${availability})`; + return `${agent.name} (${agent.provider}${model}${effort}${availability})`; } function formatUnavailableAgentProvider(provider: LocalAgentProviderAvailability): string { @@ -248,7 +248,7 @@ const workspaceLocalAgentOutputSchema = z.object({ description: z.string(), provider: z.string(), model: z.string().optional(), - thinking: z.string().optional(), + effort: z.string().optional(), providerAvailable: z.boolean().optional(), providerUnavailableReason: z.string().optional(), }); From 5209347b5696c186050e9c96511fb0ae7e5b6e61 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Tue, 21 Jul 2026 17:05:26 +0530 Subject: [PATCH 6/6] docs(agents): document effort field and CLI flag Update profile schema, examples, and subagent skill for effort. Note legacy thinking alias and provider-native pi --thinking flag. Co-Authored-By: Claude --- docs/agent-profile-schema.md | 16 +++++++++------- examples/agents/claude-implementer.md | 2 +- examples/agents/codex-explorer.md | 2 +- examples/agents/codex-qa-tester.md | 2 +- examples/agents/opencode-explorer.md | 2 +- examples/agents/pi-reviewer.md | 2 +- skills/subagent-delegation/SKILL.md | 12 ++++++------ 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/agent-profile-schema.md b/docs/agent-profile-schema.md index 0dc3db95..5933bae5 100644 --- a/docs/agent-profile-schema.md +++ b/docs/agent-profile-schema.md @@ -20,7 +20,7 @@ name: reviewer description: Read-only reviewer for bugs, security risks, and missing tests. provider: codex model: gpt-5.4 -thinking: high +effort: high disabled: false --- @@ -87,26 +87,28 @@ model: gpt-5.4 model: sonnet ``` -### `thinking` +### `effort` Optional provider reasoning effort, thinking level, or model variant. If omitted, DevSpace lets the provider default apply. Values are provider-specific passthrough strings; DevSpace does not translate names between harnesses. ```yaml -thinking: low -thinking: high -thinking: xhigh +effort: low +effort: high +effort: xhigh ``` DevSpace passes this through to providers that expose a matching control: - `claude`: SDK effort with adaptive thinking. - `codex`: SDK model reasoning effort. -- `pi`: `--thinking`. +- `pi`: CLI `--thinking` (provider-native flag; DevSpace field is still `effort`). - `opencode`: model variant. - `cursor` and `copilot`: ACP thought-level config when supported. +Legacy profile frontmatter key `thinking:` is still accepted and maps to `effort`. + ### `disabled` Optional boolean. Disabled profiles are not exposed. @@ -145,7 +147,7 @@ devspace agents show "description": "Read-only reviewer for bugs, security risks, and missing tests.", "provider": "codex", "model": "gpt-5.4", - "thinking": "high" + "effort": "high" } ``` diff --git a/examples/agents/claude-implementer.md b/examples/agents/claude-implementer.md index b907659f..6f967b5f 100644 --- a/examples/agents/claude-implementer.md +++ b/examples/agents/claude-implementer.md @@ -4,7 +4,7 @@ name: claude-implementer description: Implementation profile for multi-file changes, careful refactors, and failing test repair. provider: claude model: sonnet -thinking: high +effort: high --- Take ownership of the requested implementation while keeping the change narrow. diff --git a/examples/agents/codex-explorer.md b/examples/agents/codex-explorer.md index 93a92db6..3645f209 100644 --- a/examples/agents/codex-explorer.md +++ b/examples/agents/codex-explorer.md @@ -4,7 +4,7 @@ name: codex-explorer description: Read-only profile for bounded codebase questions, architecture tracing, and risk discovery. provider: codex model: gpt-5.4-mini -thinking: high +effort: high --- Investigate without editing. Use this profile to answer bounded questions such diff --git a/examples/agents/codex-qa-tester.md b/examples/agents/codex-qa-tester.md index f8570619..ae24b1a2 100644 --- a/examples/agents/codex-qa-tester.md +++ b/examples/agents/codex-qa-tester.md @@ -4,7 +4,7 @@ name: codex-qa-tester description: Manual QA profile for browser testing, workflow verification, and regression checks. provider: codex model: gpt-5.4-mini -thinking: high +effort: high --- Verify the requested user workflow from the outside, like a QA pass before diff --git a/examples/agents/opencode-explorer.md b/examples/agents/opencode-explorer.md index 250a4d84..884be72d 100644 --- a/examples/agents/opencode-explorer.md +++ b/examples/agents/opencode-explorer.md @@ -4,7 +4,7 @@ name: opencode-explorer description: Read-only profile for fast relevant-file discovery and small architecture questions. provider: opencode model: opencode/deepseek-v4-flash-free -thinking: high +effort: high --- Find the answer quickly without editing. Use this profile when the main need is diff --git a/examples/agents/pi-reviewer.md b/examples/agents/pi-reviewer.md index 4ed2b9de..e5a4a8a6 100644 --- a/examples/agents/pi-reviewer.md +++ b/examples/agents/pi-reviewer.md @@ -4,7 +4,7 @@ name: pi-reviewer description: Read-only review profile for quick risk checks and targeted implementation questions. provider: pi model: openai-codex/gpt-5.5 -thinking: high +effort: high --- Review or investigate only the area requested. This profile is best for quick diff --git a/skills/subagent-delegation/SKILL.md b/skills/subagent-delegation/SKILL.md index fb269df5..0251f1a4 100644 --- a/skills/subagent-delegation/SKILL.md +++ b/skills/subagent-delegation/SKILL.md @@ -48,18 +48,18 @@ Choose profiles from the compact subagent profile catalog returned by profile fits and delegation is still appropriate, use a built-in provider name from `open_workspace`. -Profiles may declare a model and optional thinking level. To override the -configured/default provider model or thinking level for a run, pass `--model` -or `--thinking`: +Profiles may declare a model and optional effort level. To override the +configured/default provider model or effort for a run, pass `--model` +or `--effort` (legacy `--thinking` is accepted as an alias): ```bash devspace agents run --model "" -devspace agents run --thinking "" +devspace agents run --effort "" ``` -Use `--thinking` only when the user asks for a specific reasoning depth or when +Use `--effort` only when the user asks for a specific reasoning depth or when the task clearly needs a different effort than the configured profile default. -Thinking values are provider-specific passthrough values. Use names supported by +Effort values are provider-specific passthrough values. Use names supported by the selected local agent harness; DevSpace does not translate values between providers.