diff --git a/README.md b/README.md index 16c8817d..1b7394cc 100644 --- a/README.md +++ b/README.md @@ -65,34 +65,37 @@ Install the DevSpace CLI: npm install -g @waishnav/devspace ``` -Then initialize and start the server: +Then initialize DevSpace: ```bash devspace init -devspace serve ``` Or run it without a global install: ```bash npx @waishnav/devspace init -npx @waishnav/devspace serve ``` During setup, DevSpace asks for: -- the local project folders ChatGPT is allowed to open through DevSpace +- the local project folders DevSpace is allowed to open - the local port, usually `7676` -- your public HTTPS base URL from Cloudflare Tunnel, ngrok, Pinggy, Tailscale Funnel, or - another reverse proxy +- whether ChatGPT or Claude will connect remotely; only remote MCP users need a public HTTPS URL +- whether to enable subagents and Dynamic Workflows, and which available providers may run -Use the public origin without `/mcp` during setup: +Setup installs the `subagents` and `dynamic-workflows` skills in +`~/.devspace/skills` when agent tooling is enabled. Coding harnesses can use the +DevSpace CLI directly; MCP users invoke the same CLI through DevSpace's shell or +process tools. + +For remote MCP use, enter the public origin without `/mcp` during setup: ```text https://your-tunnel-host.example.com ``` -You will configure your MCP client with the public `/mcp` URL after setup. +Then configure your MCP client with the public `/mcp` URL after setup. When the client connects, DevSpace opens an Owner password approval page. Enter the Owner password printed by `devspace init`. It is also stored in: @@ -139,6 +142,7 @@ DevSpace gives ChatGPT tools to: - use isolated Git worktrees for parallel coding sessions - follow project instructions from `AGENTS.md` and `CLAUDE.md` - discover local agent skills from your skill folders +- delegate bounded work and run programmable multi-agent workflows through the DevSpace CLI - show tool cards and optional change summaries in ChatGPT Apps-compatible hosts ## Mental Model @@ -180,6 +184,7 @@ devspace doctor - [Setup Guide](https://github.com/Waishnav/devspace/blob/main/docs/setup.md) - [ChatGPT Coding Workflow](https://github.com/Waishnav/devspace/blob/main/docs/chatgpt-coding-workflow.md) - [Configuration Reference](https://github.com/Waishnav/devspace/blob/main/docs/configuration.md) +- [Subagents and Dynamic Workflows](https://github.com/Waishnav/devspace/blob/main/docs/dynamic-workflows.md) - [Security Model](https://github.com/Waishnav/devspace/blob/main/docs/security.md) - [Troubleshooting Gotchas](https://github.com/Waishnav/devspace/blob/main/docs/gotchas.md) diff --git a/docs/chatgpt-coding-workflow.md b/docs/chatgpt-coding-workflow.md index ad591163..93e7e418 100644 --- a/docs/chatgpt-coding-workflow.md +++ b/docs/chatgpt-coding-workflow.md @@ -85,18 +85,16 @@ DevSpace discovers standard Agent Skills from: - project `.agents/skills` - `~/.devspace/skills` -It also includes: +It also includes the managed `subagents` and `dynamic-workflows` skills that +setup installs in `~/.devspace/skills` when agent tooling is enabled, plus: -- the package-managed `subagents` skill when the Subagents capability is enabled -- the package-managed `dynamic-workflows` skill when the Dynamic Workflows capability is enabled - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` - additional paths from `DEVSPACE_SKILL_PATHS` -When Subagents are enabled, DevSpace discovers agent profiles -from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`. -`open_workspace` exposes a compact catalog with profile names, descriptions, -providers, and optional models/effort levels so the model can choose a configured agent -without seeing provider-specific launch details. +When agent tooling is enabled, DevSpace discovers agent profiles from +`~/.devspace/agents/*.md` and project `.devspace/agents/*.md`. +`open_workspace` exposes only usable provider names and profile names with +descriptions. Disabled or unavailable providers and their profiles are omitted. Example profiles are packaged under `examples/agents/` for users who want starter templates. Copy or adapt them into one of the active profile directories @@ -113,16 +111,15 @@ Skill paths may be outside the workspace. DevSpace only permits reading: - files under a skill directory after that skill's `SKILL.md` has been read Set `DEVSPACE_SKILLS=0` to hide skills from workspace output. Set -`DEVSPACE_SUBAGENTS=1` to expose the experimental subagent catalog and -`subagents` skill. That skill can use target information already supplied by the -host or discover it with `devspace agents targets`. `devspace agents ls` lists -existing subagent sessions for the current workspace. +`DEVSPACE_SUBAGENTS=1` to enable both direct subagents and Dynamic Workflows. +The skills invoke the DevSpace CLI through `bash` or `exec_command`; DevSpace +does not expose dedicated workflow-execution MCP tools. Models discover the +usable execution catalog with `devspace agents targets --json`. -Set `DEVSPACE_WORKFLOWS=1` to enable Dynamic Workflows independently. When the -variable is omitted, Dynamic Workflows follows the effective Subagents setting, -including persisted config and any environment override. Disabled features are -omitted from the `open_workspace` schema and response rather than returned as -empty capability arrays. +`DEVSPACE_AGENT_PROVIDERS` can narrow the configured provider allowlist. +`DEVSPACE_WORKFLOWS` remains an optional runtime override; normally workflows +follow the agent-tooling setting. Disabled features are omitted from the +`open_workspace` schema and response. ## Tool Names @@ -158,16 +155,10 @@ a PTY, or send Ctrl-C. Set `tty: true` only for commands that need a terminal. By default, `DEVSPACE_WIDGETS=full`. -In that mode, DevSpace attaches widget UI to the exposed workspace, workflow, -file, edit, and shell tools. The `open_workspace` dropdown presents the opened -root, loaded skills and instructions, available agent providers/profiles, and -currently active workflows for that workspace. - -Dynamic Workflow views are read-only. They refresh through app-only MCP tools -and show observed phases, agent calls, replay state, worktree isolation, errors, -and recent activity. When the host supports MCP Apps fullscreen display mode, -the card offers an **Open dashboard** presentation control. It does not add -cancel, resume, apply, or cleanup actions. +In that mode, DevSpace attaches widget UI to the exposed workspace, file, edit, +and shell tools. The `open_workspace` dropdown presents the opened root, loaded +skills and instructions, usable agent provider/profile names, and a compact +snapshot of active workflows with running, completed, and failed call counts. The aggregate `show_changes` tool is not exposed by default. diff --git a/docs/claude-code-dynamic-workflows.html b/docs/claude-code-dynamic-workflows.html deleted file mode 100644 index d7fa34ac..00000000 --- a/docs/claude-code-dynamic-workflows.html +++ /dev/null @@ -1,1150 +0,0 @@ - - - - - - - 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. -

-
- -
-
- - diff --git a/docs/configuration.md b/docs/configuration.md index a7b20b8d..bc5277d0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -83,17 +83,18 @@ sessions. | Value | Behavior | | --- | --- | -| `full` | Default. Widget UI is attached to exposed workspace, workflow, file, edit, and shell tools, including read-only live workflow dashboards. | +| `full` | Default. Widget UI is attached to exposed workspace, file, edit, and shell tools. The workspace card includes a compact active-workflow summary. | | `changes` | Enables the aggregate `show_changes` tool and attaches widget UI to `open_workspace` and `show_changes`. | | `off` | Disables widget UI. | -## Skills +## Agent Tooling And Skills | Variable | Purpose | | --- | --- | | `DEVSPACE_SKILLS` | Set to `0` to hide skills. Enabled by default. | -| `DEVSPACE_SUBAGENTS` | Set to `1` to expose configured agent profiles as Subagents. Experimental and disabled by default. | -| `DEVSPACE_WORKFLOWS` | Experimental Dynamic Workflows gate. When unset, it follows the effective Subagents setting, including persisted config and any environment override. | +| `DEVSPACE_SUBAGENTS` | Enables direct subagents and, by default, Dynamic Workflows. | +| `DEVSPACE_WORKFLOWS` | Optional runtime override for the workflow CLI. When unset, it follows `DEVSPACE_SUBAGENTS`. | +| `DEVSPACE_AGENT_PROVIDERS` | Optional comma-separated provider allowlist: `codex`, `claude`, `opencode`, `pi`, `cursor`, or `copilot`. | | `DEVSPACE_AGENT_DIR` | Defaults to `~/.codex`; its `skills` child is loaded for compatibility. | | `DEVSPACE_SKILL_PATHS` | Optional comma-separated additional skill directories. | @@ -105,33 +106,31 @@ DevSpace discovers standard Agent Skills from: It also includes: -- the package-managed `subagents` skill when the Subagents capability is enabled -- the package-managed `dynamic-workflows` skill when the Dynamic Workflows capability is enabled +- managed `subagents` and `dynamic-workflows` skills installed by setup in `~/.devspace/skills` - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` - additional paths from `DEVSPACE_SKILL_PATHS` -User and project skills with the same name take precedence over bundled skills. -DevSpace does not copy bundled skills into `~/.devspace/skills` during setup. +User and project skills with the same name take precedence. Setup updates only +copies marked as DevSpace-managed and preserves unmarked, user-owned skill +directories. -When Subagents are enabled, DevSpace discovers agent profiles -from: +When agent tooling is enabled, DevSpace discovers agent profiles from: - `~/.devspace/agents/*.md` - project `.devspace/agents/*.md` -`open_workspace` returns a compact catalog containing profile names, -descriptions, providers, and optional models/effort levels so the host model can choose an -agent without reading provider-specific launch details. `devspace agents ls` -lists existing subagent sessions for the current workspace, scoped by the -workspace environment injected into shell commands. The `subagents` -skill teaches the model to discover targets with `devspace agents targets`, -then use the minimal `devspace agents run`, `devspace agents show`, and -`devspace agents ls` workflow. - -Provider availability is detected at runtime. DevSpace does not persist probe -timestamps, availability snapshots, or an experimental provider enable-list in -`config.json`. Final provider policy and onboarding are deferred until the -Subagents and Dynamic Workflows features are finalized. +`open_workspace` returns only usable provider names and profile names with +descriptions. `devspace agents ls` lists existing subagent sessions for the +current workspace, scoped by the workspace environment injected into shell +commands. The `subagents` skill teaches the model to discover targets with +`devspace agents targets`, then use the minimal `devspace agents run`, +`devspace agents show`, and `devspace agents ls` workflow. + +Provider availability is detected at runtime. Setup persists the selected +provider names in `config.json`; unavailable and unselected providers and their +profiles are omitted from model-facing results. `devspace agents targets --json` +shows the complete usable CLI target catalog when a model needs provider, +model, or effort defaults for execution. Starter profile templates are available under `examples/agents/`. Copy or adapt them into one of the active profile directories before use. diff --git a/docs/dynamic-workflow/claude/README.md b/docs/dynamic-workflow/claude/README.md deleted file mode 100644 index b9ddb350..00000000 --- a/docs/dynamic-workflow/claude/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# 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 deleted file mode 100644 index 40fe3f97..00000000 --- a/docs/dynamic-workflow/claude/agent.md +++ /dev/null @@ -1,205 +0,0 @@ -# `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 deleted file mode 100644 index 180e2fb6..00000000 --- a/docs/dynamic-workflow/claude/architecture.md +++ /dev/null @@ -1,101 +0,0 @@ -# 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 deleted file mode 100644 index 7604ac57..00000000 --- a/docs/dynamic-workflow/claude/cheatsheet.md +++ /dev/null @@ -1,154 +0,0 @@ -# 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 deleted file mode 100644 index bf67020a..00000000 --- a/docs/dynamic-workflow/claude/concurrency.md +++ /dev/null @@ -1,191 +0,0 @@ -# `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 deleted file mode 100644 index b347469d..00000000 --- a/docs/dynamic-workflow/claude/control-and-io.md +++ /dev/null @@ -1,179 +0,0 @@ -# `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 deleted file mode 100644 index 4a2e41c6..00000000 --- a/docs/dynamic-workflow/claude/lifecycle.md +++ /dev/null @@ -1,108 +0,0 @@ -# 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 deleted file mode 100644 index 268dfb93..00000000 --- a/docs/dynamic-workflow/claude/limits.md +++ /dev/null @@ -1,76 +0,0 @@ -# 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 deleted file mode 100644 index 6a4b6b00..00000000 --- a/docs/dynamic-workflow/claude/opt-in.md +++ /dev/null @@ -1,67 +0,0 @@ -# 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 deleted file mode 100644 index 39c75d34..00000000 --- a/docs/dynamic-workflow/claude/orchestration.md +++ /dev/null @@ -1,138 +0,0 @@ -# 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 deleted file mode 100644 index 42dc59ec..00000000 --- a/docs/dynamic-workflow/claude/patterns.md +++ /dev/null @@ -1,254 +0,0 @@ -# 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 deleted file mode 100644 index c30fa49b..00000000 --- a/docs/dynamic-workflow/claude/primitives.md +++ /dev/null @@ -1,62 +0,0 @@ -# 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 deleted file mode 100644 index d0734b99..00000000 --- a/docs/dynamic-workflow/claude/resume.md +++ /dev/null @@ -1,94 +0,0 @@ -# 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 deleted file mode 100644 index dfd64fbc..00000000 --- a/docs/dynamic-workflow/claude/script-contract.md +++ /dev/null @@ -1,145 +0,0 @@ -# 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 deleted file mode 100644 index c6735f5d..00000000 --- a/docs/dynamic-workflow/claude/usecases.md +++ /dev/null @@ -1,180 +0,0 @@ -# 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 deleted file mode 100644 index b43a40ee..00000000 --- a/docs/dynamic-workflow/claude/workflow-tool.md +++ /dev/null @@ -1,139 +0,0 @@ -# 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) diff --git a/docs/dynamic-workflow/devspace/plan.md b/docs/dynamic-workflow/devspace/plan.md deleted file mode 100644 index 4a73627f..00000000 --- a/docs/dynamic-workflow/devspace/plan.md +++ /dev/null @@ -1,369 +0,0 @@ -# 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. | -| Provider policy | Runtime uses currently available providers in stable product order. Durable provider policy and onboarding are deferred. | -| 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 now; policy later - -Current experimental runtime: - -- Probe provider availability at execution time. -- Resolve `opts.provider` → `meta.defaultProvider` → first available provider - in stable product order. -- Keep probe timestamps and unavailable reasons in diagnostics only; do not - persist them in user configuration. -- Unknown or unavailable explicit providers fail that `agent()` call. - -The final onboarding release may add an ordered array of provider policy -objects with `id`, `enabled`, `defaultModel`, and `defaultEffort`. That contract -is deliberately deferred so the workflow stack does not publish an unfinished -configuration shape. - -### 4.4 Skills gating fix (required, not optional) - -Bundled `subagents` and `dynamic-workflows` skills remain package-managed. -User/project copies win on name collision. Setup does not copy bundled skills -into `~/.devspace/skills`, which prevents generated copies from shadowing later -package updates. The legacy `subagent-delegation` name is suppressed. - -### 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; document deferred provider policy | -| Cross-cutting | rename `thinking`→`effort` in profile/CLI/store/adapters (can land with M3–4) | -| Config | Keep provider availability runtime-only until final onboarding. | - ---- - -## 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` + workflow capability gate | tools only if workflows are enabled | -| `skills.ts` | independent package-managed skill gates | -| `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 live provider in stable product order. Final provider defaults and enablement are deferred to onboarding finalization. -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. diff --git a/docs/dynamic-workflow/devspace/primitives-spec.md b/docs/dynamic-workflow/devspace/primitives-spec.md deleted file mode 100644 index 219408f3..00000000 --- a/docs/dynamic-workflow/devspace/primitives-spec.md +++ /dev/null @@ -1,795 +0,0 @@ -# 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** behind the workflow capability gate — 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 | Runtime: `opts.provider` → `meta.defaultProvider` → first currently available provider in stable product order. Final provider policy is deferred. | -| 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 | Deterministic call-index prefix only (first miss closes replay) | -| File diffs per stage | **Not a primitive** | Same — no auto-diff | - ---- - -## 3. Provider availability now; policy after finalization - -### Current experimental contract - -There is no user-facing `agentProviders` block and no -`DEVSPACE_AGENT_PROVIDERS` environment variable. DevSpace probes implemented -providers at runtime, keeps availability details in memory, and orders usable -providers by `LOCAL_AGENT_PROVIDERS`: - -```text -codex → claude → opencode → pi → cursor → copilot -``` - -`devspace init` does not configure providers. `devspace doctor` may report live -availability but remains read-only. Probe timestamps and unavailable-provider -reasons are diagnostics, not durable user intent. - -Default resolution is: - -```text -explicit agent() provider - → workflow meta.defaultProvider - → first currently available provider -``` - -An explicit provider or profile whose harness is unavailable fails with a clear -typed error. Direct `devspace agents` calls and workflow `agent()` calls use the -same target resolver. - -### Deferred final provider policy - -When Subagents and Dynamic Workflows are finalized and incorporated into -onboarding, the intended durable shape is an ordered array of user choices: - -```ts -interface AgentProviderPolicy { - id: AgentProviderId - enabled: boolean - defaultModel?: string - defaultEffort?: string -} - -interface DevspaceUserConfig { - // ...existing fields... - agentProviders?: AgentProviderPolicy[] -} -``` - -Array order can define fallback preference. Resolution will then be: - -```text -call model/effort override - → profile model/effort - → provider defaultModel/defaultEffort - → provider-native defaults -``` - -Availability snapshots still must not be persisted inside this policy. The -onboarding, config commands, documentation, and provider-management UI should -land together rather than exposing another intermediate configuration shape. ---- - -## 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 the workflow capability) - -| 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` (package-managed; not copied 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 | Same callIndex + cache key while the prefix remains open. | -| Close | First failed, interrupted, changed, missing, corrupt, worktree, or unpersisted result executes live and closes replay for later calls. | -| Record | Cache hits written as new rows `from_cache=1` so chains chain. | -| Determinism | Bans make prompt construction stable if args fixed. | - -Document prefix-only resume (no consume-once key fallback) 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 (`isolation: 'worktree'`) | **Yes v1** (must-have; §7.1b) | -| 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` | deterministic call-index prefix | -| 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). diff --git a/docs/dynamic-workflows.md b/docs/dynamic-workflows.md new file mode 100644 index 00000000..8020b1ac --- /dev/null +++ b/docs/dynamic-workflows.md @@ -0,0 +1,94 @@ +# Subagents And Dynamic Workflows + +DevSpace exposes one agent execution layer through its CLI. Coding harnesses +such as Codex, Pi, OpenCode, or Cursor can call it directly. ChatGPT and Claude +can call the same commands through DevSpace's ordinary shell or process tools. +There are no dedicated subagent or workflow-execution MCP tools. + +## Setup + +Run `devspace init` and enable agent tooling. Setup probes the supported +providers, asks which ones DevSpace may use, and installs two skills in +`~/.devspace/skills`: + +- `subagents` for one bounded delegation and later follow-ups +- `dynamic-workflows` for programmed multi-agent orchestration + +Provider selection is stored as `agentProviders` in +`~/.devspace/config.json`. Runtime availability is checked again before a +provider is shown or used. + +## Project Scope + +Run agent commands from the intended project. When an MCP host invokes the CLI, +DevSpace injects the opened workspace identity. In a standalone harness, +DevSpace discovers the current Git repository or project directory. Lists, +lookups, continuations, status checks, and cancellations stay inside that +scope. + +## Direct Subagents + +```bash +devspace agents targets --json +devspace agents run "" --json +devspace agents show --json +devspace agents run "" --json +devspace agents ls --json +``` + +Use a direct subagent for one focused implementation, investigation, review, or +verification task. Profiles can supply role instructions and provider/model +defaults. The child runs independently and returns an id that the orchestrator +polls or continues. + +## Dynamic Workflows + +```bash +devspace workflow run --name --json +devspace workflow run --file --arg key=value --json +devspace workflow status --json +devspace workflow calls --json +devspace workflow call --json +devspace workflow cancel --json +devspace workflow ls --json +``` + +Named scripts live in `.devspace/workflows/.js`. A script can combine +`agent`, `parallel`, `pipeline`, `phase`, `log`, and one-level nested +`workflow` calls. Agent calls can request structured JSON or an isolated Git +worktree. + +Agent harnesses should prefer `--json`, retain the returned id, and poll status. +This avoids coupling a long workflow lifetime to one tool-call timeout. +`--follow` remains available for interactive terminals with long-running +process support. + +Failed and cancelled workflows are terminal. `workflow run --resume ` +creates a new run, reuses the unchanged successful prefix when safe, and +continues live from the first failed or changed call. + +## MCP Workspace Summary + +When agent tooling is enabled, `open_workspace` stays deliberately small: + +```json +{ + "agentProviders": ["codex", "claude"], + "agents": [ + { "name": "reviewer", "description": "Review changes and test gaps." } + ], + "activeWorkflows": [ + { + "id": "wfr_123", + "name": "review-auth", + "status": "running", + "calls": { "running": 2, "completed": 3, "failed": 0 } + } + ] +} +``` + +Provider capability metadata, models, effort semantics, session identifiers, +workflow phases, and internal counters are intentionally absent. Models obtain +execution details only when needed through `devspace agents targets --json` or +the workflow inspection commands. diff --git a/docs/gotchas.md b/docs/gotchas.md index 18ec1912..23a58ce2 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -201,22 +201,20 @@ DevSpace looks in standard Agent Skills locations: It also checks compatibility and custom paths: -- the package-managed `subagents` skill when the Subagents capability is enabled -- the package-managed `dynamic-workflows` skill when the Dynamic Workflows capability is enabled +- managed `subagents` and `dynamic-workflows` skills installed by `devspace init` - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` - additional paths from `DEVSPACE_SKILL_PATHS` -When the Subagents capability is enabled, DevSpace loads agent profiles from +When agent tooling is enabled, DevSpace loads agent profiles from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`, then exposes a -compact profile catalog through `open_workspace`. The bundled -`subagents` skill can also discover the same usable targets through +compact profile catalog through `open_workspace`. The `subagents` skill can +also discover the same usable targets through `devspace agents targets` in CLI-only hosts. `devspace agents ls` lists existing subagent sessions, not profile definitions. -Bundled skills remain package-managed and are not copied into -`~/.devspace/skills`. A user-owned skill with the same name intentionally -overrides the bundled copy. The legacy `subagent-delegation` name is no longer -advertised. +Run `devspace init --force` to install or refresh DevSpace-managed skill copies. +An unmarked, user-owned skill with the same name is preserved. The legacy +`subagent-delegation` name is no longer advertised. Packaged agent profile examples under `examples/agents/` are starter templates. Copy or adapt them into one of the active profile directories before use. diff --git a/docs/setup.md b/docs/setup.md index e332f216..9582d92d 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,7 +1,7 @@ # Setup Guide -This guide is for users who want ChatGPT or another MCP host to work in local -projects through DevSpace. +This guide covers both local coding harnesses that use the DevSpace CLI and MCP +hosts such as ChatGPT or Claude. ## Requirements @@ -9,10 +9,10 @@ projects through DevSpace. - npm - Git - Bash, including Git Bash or WSL on Windows -- a public HTTPS URL that forwards to the local DevSpace server +- a public HTTPS URL only when a remote MCP host must reach DevSpace -DevSpace does not create the public tunnel for you. Use Cloudflare Tunnel, -ngrok, Pinggy, Tailscale Funnel, or your own HTTPS reverse proxy. +DevSpace does not create a public tunnel. Remote MCP users can use Cloudflare +Tunnel, ngrok, Pinggy, Tailscale Funnel, or their own HTTPS reverse proxy. ## Install And Configure @@ -26,8 +26,7 @@ The setup flow asks one question at a time. ### Project Roots -Choose the folders ChatGPT is allowed to open through DevSpace. Keep this -narrow. +Choose the folders DevSpace is allowed to open. Keep this narrow. Examples: @@ -55,8 +54,8 @@ http://127.0.0.1:7676/mcp ### Public Base URL -Start your tunnel or reverse proxy before entering this value. Point the tunnel -at: +Setup first asks whether ChatGPT or Claude will connect over the internet. Say +no for CLI-only use. If yes, start your tunnel or reverse proxy and point it at: ```text http://127.0.0.1:7676 @@ -74,8 +73,29 @@ Configure the MCP client with the full MCP endpoint: https://your-tunnel-host.example.com/mcp ``` +### Agent Tooling + +Enable agent tooling to use both direct subagents and Dynamic Workflows. Setup +shows currently available providers and persists only the providers you select. +Unavailable and unselected providers are not exposed to models. + +The two model skills are installed in: + +```text +~/.devspace/skills/subagents +~/.devspace/skills/dynamic-workflows +``` + +DevSpace updates its managed copies on later forced setup runs and preserves a +same-named directory that does not carry the DevSpace management marker. + +Coding harnesses can now run `devspace agents` and `devspace workflow` from a +project directory without starting the MCP server. + ## Start The Server +This step is only required for MCP clients. + Run: ```bash diff --git a/package.json b/package.json index 3e1a25d2..a6f01226 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/cli-workspace.test.ts && tsx src/cli-output.test.ts && tsx src/open-workspace-capabilities.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-capabilities.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-resolution.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-contracts.test.ts && tsx src/workflow-errors.test.ts && tsx src/workflow-types.test.ts && tsx src/workflow-store.test.ts && tsx src/workflow-lifecycle.test.ts && tsx src/workflow-view.test.ts && tsx src/workflow-summary.test.ts && tsx src/workflow-tui.test.ts && tsx src/workflow-script.test.ts && tsx src/workflow-sandbox.test.ts && tsx src/workflow-engine.test.ts && tsx src/workflow-files.test.ts && tsx src/workflow-launch.test.ts && tsx src/workflow-replay.test.ts && tsx src/workflow-schema.test.ts", + "test": "tsx src/config.test.ts && tsx src/cli-workspace.test.ts && tsx src/cli-output.test.ts && tsx src/open-workspace-capabilities.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-capabilities.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-resolution.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skill-install.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-contracts.test.ts && tsx src/workflow-errors.test.ts && tsx src/workflow-types.test.ts && tsx src/workflow-store.test.ts && tsx src/workflow-lifecycle.test.ts && tsx src/workflow-view.test.ts && tsx src/workflow-summary.test.ts && tsx src/workflow-tui.test.ts && tsx src/workflow-script.test.ts && tsx src/workflow-sandbox.test.ts && tsx src/workflow-engine.test.ts && tsx src/workflow-files.test.ts && tsx src/workflow-launch.test.ts && tsx src/workflow-replay.test.ts && tsx src/workflow-schema.test.ts", "typecheck": "tsc -p tsconfig.json --noEmit" }, "keywords": [], diff --git a/src/cli.test.ts b/src/cli.test.ts index 5da2173a..fa01eeee 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -109,6 +109,30 @@ try { assert.equal(targets.profiles[0]?.provider, "codex"); assert.equal(targets.providers.some((provider) => provider.name === "codex"), true); + const filteredTargets = JSON.parse(execFileSync( + "node", + ["--import", "tsx", "src/cli.ts", "agents", "targets", "--json"], + { + cwd: process.cwd(), + encoding: "utf8", + env: { + ...process.env, + DEVSPACE_CONFIG_DIR: configDir, + DEVSPACE_ALLOWED_ROOTS: projectRoot, + DEVSPACE_STATE_DIR: stateDir, + DEVSPACE_WORKSPACE_ROOT: projectRoot, + DEVSPACE_SUBAGENTS: "1", + DEVSPACE_AGENT_PROVIDERS: "claude", + DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", + }, + }, + )) as { + profiles: Array<{ name: string }>; + providers: Array<{ name: string }>; + }; + assert.deepEqual(filteredTargets.profiles, []); + assert.deepEqual(filteredTargets.providers.map((provider) => provider.name), ["claude"]); + const agentsJson = JSON.parse(execFileSync( "node", ["--import", "tsx", "src/cli.ts", "agents", "ls", "--json"], diff --git a/src/cli.ts b/src/cli.ts index bf634201..83fd8b58 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,7 +18,9 @@ import { } from "./local-agent-catalog.js"; import { isLocalAgentProvider, + LOCAL_AGENT_PROVIDERS, loadLocalAgentProfiles, + type LocalAgentProvider, } from "./local-agent-profiles.js"; import { assertLocalAgentProviderAvailable, @@ -50,6 +52,7 @@ import { localAgentOutput, localAgentTargetsOutput, } from "./cli-output.js"; +import { installBundledAgentSkills } from "./skill-install.js"; import { runWorkflowCommand } from "./workflow-cli.js"; import { @@ -84,7 +87,7 @@ async function main(argv: string[]): Promise { case "agents": if (!loadConfig().subagents) { throw new Error( - "Subagents are disabled. Set DEVSPACE_SUBAGENTS=1 to enable the experimental feature.", + "Agent tooling is disabled. Run `devspace init --force` or set DEVSPACE_SUBAGENTS=1.", ); } await runAgentsCommand(args); @@ -170,31 +173,61 @@ async function runInit({ force }: { force: boolean }): Promise { }); const port = Number(portAnswer); - prompts.note( - [ - "DevSpace needs a public base URL so ChatGPT or Claude can reach this MCP server.", - "Create a tunnel or reverse proxy with Cloudflare Tunnel, ngrok, Pinggy, Tailscale Funnel, or your own HTTPS proxy.", - "Paste the public origin here, without /mcp.", - "", - "Example: https://your-tunnel-host.example.com", - ].join("\n"), - "Public URL required", - ); - const publicBaseUrl = normalizePublicBaseUrl(await textPrompt({ - message: files.config.publicBaseUrl - ? `What is the public base URL? Press Enter to keep ${files.config.publicBaseUrl}` - : "What is the public base URL?", - placeholder: files.config.publicBaseUrl ?? "https://your-tunnel-host.example.com", - defaultValue: files.config.publicBaseUrl ?? "", - validate: validateRequiredPublicBaseUrl, - })); + const remoteMcpAnswer = await prompts.confirm({ + message: "Will ChatGPT or Claude connect to DevSpace over the internet?", + initialValue: Boolean(files.config.publicBaseUrl), + }); + if (prompts.isCancel(remoteMcpAnswer)) throw new SetupCancelledError(); + const publicBaseUrl = remoteMcpAnswer + ? normalizePublicBaseUrl(await textPrompt({ + message: files.config.publicBaseUrl + ? `What is the public base URL? Press Enter to keep ${files.config.publicBaseUrl}` + : "What is the public base URL?", + placeholder: files.config.publicBaseUrl ?? "https://your-tunnel-host.example.com", + defaultValue: files.config.publicBaseUrl ?? "", + validate: validateRequiredPublicBaseUrl, + })) + : null; + + const agentToolingAnswer = await prompts.confirm({ + message: "Enable subagents and Dynamic Workflows?", + initialValue: resolveSubagentsFlag(files.config) ?? true, + }); + if (prompts.isCancel(agentToolingAnswer)) throw new SetupCancelledError(); + + const providerSnapshot = getLocalAgentProviderAvailabilitySnapshot(); + const availableProviders = providerSnapshot + .filter((provider) => provider.available) + .map((provider) => provider.name); + let agentProviders: LocalAgentProvider[] = []; + let subagents = agentToolingAnswer; + if (subagents && availableProviders.length === 0) { + prompts.log.warn("No supported agent providers are currently available; agent tooling was disabled."); + subagents = false; + } else if (subagents) { + const configuredProviders = files.config.agentProviders ?? [...LOCAL_AGENT_PROVIDERS]; + const providerAnswer = await prompts.multiselect({ + message: "Which agent providers should DevSpace use?", + options: availableProviders.map((provider) => ({ value: provider, label: provider })), + initialValues: configuredProviders.filter((provider) => + availableProviders.includes(provider) + ), + required: true, + }); + if (prompts.isCancel(providerAnswer)) throw new SetupCancelledError(); + agentProviders = providerAnswer; + } const config: DevspaceUserConfig = { + ...files.config, host: files.config.host ?? "127.0.0.1", port, allowedRoots, publicBaseUrl, - subagents: resolveSubagentsFlag(files.config), + subagents, + // Disabling the capability should not turn provider defaults into an + // explicit deny-all list if it is later enabled through the environment. + agentProviders: subagents ? agentProviders : files.config.agentProviders, }; const auth = { ownerToken: files.auth.ownerToken ?? generateOwnerToken(), @@ -202,11 +235,16 @@ async function runInit({ force }: { force: boolean }): Promise { const configPath = writeDevspaceConfig(config); const authPath = writeDevspaceAuth(auth); + const installedSkills = subagents ? installBundledAgentSkills() : undefined; const lines = [ `Config: ${configPath}`, `Auth: ${authPath}`, `Local MCP URL: http://${config.host}:${config.port}/mcp`, ...(publicBaseUrl ? [`Public MCP URL: ${publicBaseUrl}/mcp`] : []), + `Agent tooling: ${subagents ? `enabled (${agentProviders.join(", ")})` : "disabled"}`, + ...(installedSkills + ? [`Agent skills: ${installedSkills.directory}`] + : []), ]; prompts.note(lines.join("\n"), "DevSpace configured"); prompts.note( @@ -217,7 +255,16 @@ async function runInit({ force }: { force: boolean }): Promise { ].join("\n"), "Owner password", ); - prompts.outro("Run `devspace serve` to start the MCP server."); + if (installedSkills?.skipped.length) { + prompts.log.warn( + `Kept user-owned skills unchanged: ${installedSkills.skipped.join(", ")}`, + ); + } + prompts.outro( + remoteMcpAnswer + ? "Run `devspace serve` to start the MCP server." + : "Setup complete. Use `devspace agents` and `devspace workflow` from a project directory.", + ); } catch (error) { if (error instanceof SetupCancelledError) { prompts.cancel("Setup cancelled"); @@ -297,7 +344,10 @@ async function runDoctor(): Promise { console.log(`Subagents: ${config.subagents ? "enabled" : "disabled"}`); console.log(`Workflows: ${config.workflows ? "enabled" : "disabled"}`); if (config.subagents) { - const snapshot = getLocalAgentProviderAvailabilitySnapshot(); + const snapshot = getLocalAgentProviderAvailabilitySnapshot( + process.env, + config.agentProviders, + ); console.log( `Agent providers (live): ${formatLocalAgentProviderAvailabilitySummary(snapshot)}`, ); @@ -420,7 +470,7 @@ async function runAgentsTargets(args: string[]): Promise { const profiles = await loadLocalAgentProfiles(config, workspaceRoot); const catalog = buildLocalAgentCatalog( profiles, - getLocalAgentProviderAvailabilitySnapshot(), + getLocalAgentProviderAvailabilitySnapshot(process.env, config.agentProviders), ); console.log( args.includes("--json") @@ -443,7 +493,7 @@ async function runAgentsRun(args: string[]): Promise { if (!isLocalAgentProvider(existing.provider)) { throw new Error(`Unknown subagent provider for existing session: ${existing.provider}`); } - assertLocalAgentProviderAvailable(existing.provider); + assertLocalAgentProviderAvailable(existing.provider, process.env, config.agentProviders); const promptFile = writeAgentPromptFile(parsed.prompt); store.update(existing.id, { status: "starting", @@ -465,7 +515,10 @@ async function runAgentsRun(args: string[]): Promise { } const profiles = await loadLocalAgentProfiles(config, workspaceRoot); - const availableProviders = getAvailableLocalAgentProviders(); + const availableProviders = getAvailableLocalAgentProviders( + process.env, + config.agentProviders, + ); let target; try { target = resolveLocalAgentExecution({ @@ -556,7 +609,10 @@ async function runAgentsWorker(args: string[]): Promise { target: record.profileName, prompt, profiles, - availableProviders: getAvailableLocalAgentProviders(), + availableProviders: getAvailableLocalAgentProviders( + process.env, + config.agentProviders, + ), model: record.model, effort: record.effort, }); diff --git a/src/config.test.ts b/src/config.test.ts index ad916270..16d26713 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -27,6 +27,18 @@ assert.equal(loadConfig(baseEnv).devspaceSkillsDir, join(emptyConfigDir, "skills assert.equal(loadConfig(baseEnv).devspaceAgentsDir, join(emptyConfigDir, "agents")); assert.equal(loadConfig(baseEnv).subagents, false); assert.equal(loadConfig(baseEnv).workflows, false); +assert.deepEqual(loadConfig(baseEnv).agentProviders, [ + "codex", + "claude", + "opencode", + "pi", + "cursor", + "copilot", +]); +assert.deepEqual( + loadConfig({ ...baseEnv, DEVSPACE_AGENT_PROVIDERS: "codex,pi,codex" }).agentProviders, + ["codex", "pi"], +); assert.equal(loadConfig({ ...baseEnv, DEVSPACE_SKILLS: "0" }).skillsEnabled, false); assert.equal(loadConfig({ ...baseEnv, DEVSPACE_SKILLS: "1" }).skillsEnabled, true); assert.equal( @@ -66,6 +78,10 @@ assert.throws( () => loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "invalid" }), /Invalid DEVSPACE_TOOL_MODE: invalid/, ); +assert.throws( + () => loadConfig({ ...baseEnv, DEVSPACE_AGENT_PROVIDERS: "codex,unknown" }), + /Invalid agent provider: unknown/, +); assert.deepEqual(loadConfig(baseEnv).logging, { level: "info", @@ -169,6 +185,7 @@ writeFileSync( allowedRoots: [process.cwd()], publicBaseUrl: "https://devspace.example.com", subagents: true, + agentProviders: ["claude"], }), ); writeFileSync( @@ -183,9 +200,27 @@ assert.equal(fileConfig.port, 8787); assert.equal(fileConfig.oauth.ownerToken, "persisted-owner-token-long-enough"); assert.equal(fileConfig.publicBaseUrl, "https://devspace.example.com"); assert.equal(fileConfig.subagents, true); +assert.deepEqual(fileConfig.agentProviders, ["claude"]); assert.deepEqual(fileConfig.allowedHosts, [ "localhost", "127.0.0.1", "::1", "devspace.example.com", ]); + +const disabledAgentConfigDir = mkdtempSync(join(tmpdir(), "devspace-disabled-agent-config-test-")); +writeFileSync( + join(disabledAgentConfigDir, "config.json"), + JSON.stringify({ + allowedRoots: [process.cwd()], + subagents: false, + }), +); +writeFileSync( + join(disabledAgentConfigDir, "auth.json"), + JSON.stringify({ ownerToken: "persisted-owner-token-long-enough" }), +); +assert.deepEqual(loadConfig({ + DEVSPACE_CONFIG_DIR: disabledAgentConfigDir, + DEVSPACE_SUBAGENTS: "1", +}).agentProviders, ["codex", "claude", "opencode", "pi", "cursor", "copilot"]); diff --git a/src/config.ts b/src/config.ts index 974e7b88..98276058 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,6 +4,11 @@ import { expandHomePath } from "./roots.js"; import type { LoggingConfig, LogFormat, LogLevel } from "./logger.js"; import type { OAuthConfig } from "./oauth-provider.js"; import { devspaceAgentsDir, devspaceSkillsDir, loadDevspaceFiles } from "./user-config.js"; +import { + isLocalAgentProvider, + LOCAL_AGENT_PROVIDERS, + type LocalAgentProvider, +} from "./local-agent-profiles.js"; export type ToolMode = "minimal" | "full" | "codex"; export type WidgetMode = "off" | "changes" | "full"; @@ -27,6 +32,7 @@ export interface ServerConfig { devspaceAgentsDir: string; subagents: boolean; workflows: boolean; + agentProviders: LocalAgentProvider[]; agentDir: string; logging: LoggingConfig; } @@ -116,6 +122,22 @@ function parsePathList(value: string | undefined): string[] { ); } +function parseAgentProviders( + value: string | string[] | undefined, +): LocalAgentProvider[] { + if (value === undefined) return [...LOCAL_AGENT_PROVIDERS]; + const entries = (Array.isArray(value) ? value : value.split(",")) + .map((entry) => entry.trim()) + .filter(Boolean); + const invalid = entries.find((entry) => !isLocalAgentProvider(entry)); + if (invalid) { + throw new Error( + `Invalid agent provider: ${invalid}. Expected one of ${LOCAL_AGENT_PROVIDERS.join(", ")}.`, + ); + } + return Array.from(new Set(entries)) as LocalAgentProvider[]; +} + function parseStringList(value: string | undefined, fallback: string[]): string[] { const entries = value ?.split(",") @@ -219,8 +241,9 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig { env.DEVSPACE_SUBAGENTS === undefined ? files.config.subagents === true : parseBoolean(env.DEVSPACE_SUBAGENTS); - // Experimental compatibility: workflows follow the existing subagents gate - // unless explicitly overridden for runtime testing. + // Agent tooling is one user-facing capability: enabling direct subagents + // also enables workflows. Keep the environment override for deployments + // that need to expose only one CLI surface. const workflows = env.DEVSPACE_WORKFLOWS === undefined ? subagents @@ -243,6 +266,9 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig { devspaceAgentsDir: devspaceAgentsDir(env), subagents, workflows, + agentProviders: parseAgentProviders( + env.DEVSPACE_AGENT_PROVIDERS ?? files.config.agentProviders, + ), agentDir: resolve(expandHomePath(env.DEVSPACE_AGENT_DIR ?? files.config.agentDir ?? defaultAgentDir())), logging: parseLoggingConfig(env), }; diff --git a/src/local-agent-availability.test.ts b/src/local-agent-availability.test.ts index 5d56697c..5060068f 100644 --- a/src/local-agent-availability.test.ts +++ b/src/local-agent-availability.test.ts @@ -28,6 +28,14 @@ assert.equal(checkLocalAgentProviderAvailability("codex").available, true); assert.equal(snapshot.find((provider) => provider.name === "pi")?.available, false); } +{ + const snapshot = getLocalAgentProviderAvailabilitySnapshot( + process.env, + ["claude", "codex"], + ); + assert.deepEqual(snapshot.map((provider) => provider.name), ["claude", "codex"]); +} + assert.equal( formatLocalAgentProviderAvailabilitySummary([ { name: "codex", available: true }, diff --git a/src/local-agent-availability.ts b/src/local-agent-availability.ts index 495a463c..617bd17d 100644 --- a/src/local-agent-availability.ts +++ b/src/local-agent-availability.ts @@ -14,14 +14,16 @@ export interface LocalAgentProviderAvailability { export function getLocalAgentProviderAvailabilitySnapshot( env: NodeJS.ProcessEnv = process.env, + providers: readonly LocalAgentProvider[] = LOCAL_AGENT_PROVIDERS, ): LocalAgentProviderAvailability[] { - return LOCAL_AGENT_PROVIDERS.map((provider) => checkLocalAgentProviderAvailability(provider, env)); + return providers.map((provider) => checkLocalAgentProviderAvailability(provider, env)); } export function getAvailableLocalAgentProviders( env: NodeJS.ProcessEnv = process.env, + providers: readonly LocalAgentProvider[] = LOCAL_AGENT_PROVIDERS, ): LocalAgentProvider[] { - return getLocalAgentProviderAvailabilitySnapshot(env) + return getLocalAgentProviderAvailabilitySnapshot(env, providers) .filter((provider) => provider.available) .map((provider) => provider.name); } @@ -51,7 +53,11 @@ export function checkLocalAgentProviderAvailability( export function assertLocalAgentProviderAvailable( provider: LocalAgentProvider, env: NodeJS.ProcessEnv = process.env, + enabledProviders: readonly LocalAgentProvider[] = LOCAL_AGENT_PROVIDERS, ): void { + if (!enabledProviders.includes(provider)) { + throw new Error(`${provider} provider is disabled in DevSpace config.`); + } const availability = checkLocalAgentProviderAvailability(provider, env); if (availability.available) return; throw new Error( diff --git a/src/server.ts b/src/server.ts index 5248e0b4..1463fe49 100644 --- a/src/server.ts +++ b/src/server.ts @@ -724,7 +724,7 @@ function createMcpServer( "DevSpace App", WORKSPACE_APP_URI, { - description: "Interactive DevSpace workspace, workflow, and file-change views.", + description: "Interactive DevSpace workspace and file-change views.", _meta: { ui: { csp: appCsp(config), @@ -1632,7 +1632,7 @@ export function createServer(config = loadConfig()): RunningServer { const reviewCheckpoints = createReviewCheckpointManager(); const processSessions = new ProcessSessionManager(); const localAgentProviders = config.subagents - ? getLocalAgentProviderAvailabilitySnapshot() + ? getLocalAgentProviderAvailabilitySnapshot(process.env, config.agentProviders) : []; const workflowReaper = config.workflows ? startWorkflowReaper(config, { diff --git a/src/skill-install.test.ts b/src/skill-install.test.ts new file mode 100644 index 00000000..803eb6b0 --- /dev/null +++ b/src/skill-install.test.ts @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { + existsSync, + mkdtempSync, + readFileSync, + rmSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { installBundledAgentSkills } from "./skill-install.js"; + +const root = mkdtempSync(join(tmpdir(), "devspace-skill-install-test-")); +const env = { DEVSPACE_CONFIG_DIR: root }; + +try { + const first = installBundledAgentSkills(env); + assert.deepEqual(first.installed, ["subagents", "dynamic-workflows"]); + const subagentsDir = join(first.directory, "subagents"); + const subagentsFile = join(subagentsDir, "SKILL.md"); + assert.equal(existsSync(join(subagentsDir, ".devspace-managed")), true); + assert.match(readFileSync(subagentsFile, "utf8"), /devspace agents targets --json/); + + writeFileSync(subagentsFile, "stale managed copy\n"); + const updated = installBundledAgentSkills(env); + assert.deepEqual(updated.updated, ["subagents", "dynamic-workflows"]); + assert.match(readFileSync(subagentsFile, "utf8"), /devspace agents targets --json/); + + unlinkSync(join(subagentsDir, ".devspace-managed")); + writeFileSync(subagentsFile, "user-owned skill\n"); + const skipped = installBundledAgentSkills(env); + assert.deepEqual(skipped.skipped, ["subagents"]); + assert.equal(readFileSync(subagentsFile, "utf8"), "user-owned skill\n"); +} finally { + rmSync(root, { recursive: true, force: true }); +} + +console.log("skill-install.test.ts: ok"); diff --git a/src/skill-install.ts b/src/skill-install.ts new file mode 100644 index 00000000..28415231 --- /dev/null +++ b/src/skill-install.ts @@ -0,0 +1,76 @@ +import { randomUUID } from "node:crypto"; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { devspaceSkillsDir } from "./user-config.js"; + +const BUNDLED_AGENT_SKILLS = ["subagents", "dynamic-workflows"] as const; +const MANAGED_MARKER = ".devspace-managed"; + +export interface AgentSkillInstallResult { + installed: string[]; + updated: string[]; + skipped: string[]; + directory: string; +} + +export function installBundledAgentSkills( + env: NodeJS.ProcessEnv = process.env, +): AgentSkillInstallResult { + const sourceRoot = fileURLToPath(new URL("../skills", import.meta.url)); + const directory = devspaceSkillsDir(env); + mkdirSync(directory, { recursive: true }); + const result: AgentSkillInstallResult = { + installed: [], + updated: [], + skipped: [], + directory, + }; + + for (const name of BUNDLED_AGENT_SKILLS) { + const source = join(sourceRoot, name); + const destination = join(directory, name); + const marker = join(destination, MANAGED_MARKER); + if (existsSync(destination) && !existsSync(marker)) { + result.skipped.push(name); + continue; + } + + const staging = mkdtempSync(join(directory, `.install-${name}-`)); + try { + cpSync(source, staging, { recursive: true }); + writeFileSync( + join(staging, MANAGED_MARKER), + "Managed by `devspace init`; place custom overrides in ~/.agents/skills or a project skill directory.\n", + ); + if (!existsSync(destination)) { + renameSync(staging, destination); + result.installed.push(name); + continue; + } + + const backup = join(directory, `.backup-${name}-${randomUUID()}`); + renameSync(destination, backup); + try { + renameSync(staging, destination); + rmSync(backup, { recursive: true, force: true }); + } catch (error) { + if (!existsSync(destination)) renameSync(backup, destination); + throw error; + } + result.updated.push(name); + } finally { + rmSync(staging, { recursive: true, force: true }); + } + } + + return result; +} diff --git a/src/ui/workspace-app.css b/src/ui/workspace-app.css index db1503ff..560cca98 100644 --- a/src/ui/workspace-app.css +++ b/src/ui/workspace-app.css @@ -388,15 +388,13 @@ html[data-display-mode="fullscreen"] .tool-card { border-radius: 0; } -.workspace-dashboard, -.workflow-dashboard { +.workspace-dashboard { display: grid; gap: 0; color: var(--color-text-primary, #f5f5f6); } -.workspace-dashboard.fullscreen, -.workflow-dashboard.fullscreen { +.workspace-dashboard.fullscreen { min-height: calc(100vh - 83px); align-content: start; } @@ -435,32 +433,21 @@ html[data-display-mode="fullscreen"] .tool-card { height: 15px; } -.active-workflows, -.workflow-heading, -.workflow-phases, -.workflow-activity, -.workflow-error { +.active-workflows { padding: 16px; } -.active-workflows, -.workflow-heading, -.workflow-phases, -.workflow-activity { +.active-workflows { border-bottom: 1px solid var(--tool-card-divider); } -.active-workflows h3, -.workflow-phase h3, -.workflow-activity h3 { +.active-workflows h3 { margin: 0 0 12px; font-size: var(--font-text-sm-size, 13px); font-weight: 600; } -.active-workflow-row, -.workflow-title-row, -.workflow-call-main { +.active-workflow-row { display: flex; align-items: flex-start; gap: 10px; @@ -494,8 +481,6 @@ html[data-display-mode="fullscreen"] .tool-card { } .active-workflow-copy, -.workflow-title-copy, -.workflow-call-copy, .workspace-list-row { display: grid; min-width: 0; @@ -503,8 +488,6 @@ html[data-display-mode="fullscreen"] .tool-card { } .active-workflow-copy span, -.workflow-subtitle, -.workflow-call-copy span, .workspace-list-row span, .workspace-list-row code { overflow: hidden; @@ -571,100 +554,14 @@ html[data-display-mode="fullscreen"] .tool-card { line-height: 1.55; } -.workflow-heading { - display: grid; - gap: 12px; -} - -.workflow-counts { - display: flex; - flex-wrap: wrap; - gap: 7px; -} - -.workflow-counts span { - padding: 4px 8px; - border-radius: 999px; - background: var(--tool-card-hover-bg); - color: var(--color-text-secondary, #d6d6dc); - font-size: var(--font-text-sm-size, 12px); -} - -.workflow-phases { - display: grid; - gap: 20px; -} - -.workflow-call-list { - display: grid; - gap: 8px; -} - -.workflow-call { - padding: 10px 12px; - border-radius: 9px; - background: color-mix(in srgb, var(--tool-card-hover-bg) 54%, transparent); -} - -.call-status { - width: 16px; - flex: 0 0 auto; - color: var(--color-text-tertiary, #a3a3aa); - text-align: center; -} - -.call-status.completed, -.call-status.from_cache { - color: var(--color-success-text, #6fda83); -} - -.call-status.failed { - color: var(--color-danger-text, #ee7676); -} - -.workflow-call-error, -.workflow-error p { - margin: 8px 0 0 26px; - color: var(--color-danger-text, #ee7676); - font-size: var(--font-text-sm-size, 12px); - line-height: 1.45; -} - -.workflow-activity { - display: grid; - gap: 8px; -} - -.workflow-event { - display: grid; - grid-template-columns: max-content minmax(0, 1fr); - gap: 10px; - color: var(--color-text-secondary, #d6d6dc); - font-size: var(--font-text-sm-size, 12px); -} - -.workflow-event time { - color: var(--color-text-tertiary, #a3a3aa); - font-family: var(--font-mono, ui-monospace, SFMono-Regular, monospace); -} - -.workflow-error { - color: var(--color-danger-text, #ee7676); -} - .dashboard-empty { color: var(--color-text-secondary, #b7b7bf); font-size: var(--font-text-sm-size, 13px); } @media (min-width: 860px) { - .workspace-dashboard.fullscreen, - .workflow-dashboard.fullscreen { + .workspace-dashboard.fullscreen { width: min(1120px, 100%); margin: 0 auto; } - - .workflow-dashboard.fullscreen .workflow-phases { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } } diff --git a/src/ui/workspace-app.tsx b/src/ui/workspace-app.tsx index b0bdeec2..98887f61 100644 --- a/src/ui/workspace-app.tsx +++ b/src/ui/workspace-app.tsx @@ -25,7 +25,7 @@ import { getToolHeaderSummary, type ToolDisplay, } from "./tool-display.js"; -import { renderWorkspaceDashboard } from "./workflow-dashboard.js"; +import { renderWorkspaceDashboard } from "./workspace-dashboard.js"; import "./workspace-app.css"; interface MountedPayload { diff --git a/src/ui/workflow-dashboard.ts b/src/ui/workspace-dashboard.ts similarity index 100% rename from src/ui/workflow-dashboard.ts rename to src/ui/workspace-dashboard.ts diff --git a/src/user-config.ts b/src/user-config.ts index 970685cf..9ba47eca 100644 --- a/src/user-config.ts +++ b/src/user-config.ts @@ -8,6 +8,7 @@ import { import { homedir } from "node:os"; import { join, resolve } from "node:path"; import { expandHomePath } from "./roots.js"; +import type { LocalAgentProvider } from "./local-agent-profiles.js"; export interface DevspaceUserConfig { host?: string; @@ -19,6 +20,7 @@ export interface DevspaceUserConfig { worktreeRoot?: string; agentDir?: string; subagents?: boolean; + agentProviders?: LocalAgentProvider[]; } export interface DevspaceAuthConfig { diff --git a/src/workflow-cli.ts b/src/workflow-cli.ts index 2e79046a..90c95c0f 100644 --- a/src/workflow-cli.ts +++ b/src/workflow-cli.ts @@ -47,7 +47,7 @@ export async function runWorkflowCommand( throw new InvalidWorkflowInputError({ code: "invalid_argument", message: - "Dynamic workflows are disabled. Set DEVSPACE_WORKFLOWS=1 to enable the experimental feature.", + "Dynamic Workflows are disabled. Run `devspace init --force` or set DEVSPACE_WORKFLOWS=1.", }); } switch (subcommand) { diff --git a/src/workflow-providers.ts b/src/workflow-providers.ts index 31836fd3..2aa60c85 100644 --- a/src/workflow-providers.ts +++ b/src/workflow-providers.ts @@ -3,10 +3,16 @@ import { LOCAL_AGENT_PROVIDERS, type LocalAgentProvider, } from "./local-agent-profiles.js"; +import type { ServerConfig } from "./config.js"; /** Live providers in stable product order for workflow agent() resolution. */ -export function resolveWorkflowLiveProviders(): LocalAgentProvider[] { - const snapshot = getLocalAgentProviderAvailabilitySnapshot(); +export function resolveWorkflowLiveProviders( + config: Pick, +): LocalAgentProvider[] { + const snapshot = getLocalAgentProviderAvailabilitySnapshot( + process.env, + config.agentProviders, + ); const live = new Set(snapshot.filter((row) => row.available).map((row) => row.name)); - return LOCAL_AGENT_PROVIDERS.filter((id) => live.has(id)); + return LOCAL_AGENT_PROVIDERS.filter((id) => config.agentProviders.includes(id) && live.has(id)); } diff --git a/src/workflow-worker.ts b/src/workflow-worker.ts index 619b5ba0..ec442508 100644 --- a/src/workflow-worker.ts +++ b/src/workflow-worker.ts @@ -54,7 +54,7 @@ export async function runWorkflowWorker( try { const source = await readFile(claimed.scriptPath, "utf8"); const parsed = parseWorkflowScript(source, { filename: claimed.scriptPath }); - const availableProviders = resolveWorkflowLiveProviders(); + const availableProviders = resolveWorkflowLiveProviders(config); const agentProfiles = await loadLocalAgentProfiles(config, claimed.workspaceRoot); const concurrency = resolveWorkflowConcurrency( parsed.meta.concurrency,