From 559901bfa505e7b232dfb8760208386c2bca4bb1 Mon Sep 17 00:00:00 2001 From: fanhuanjie Date: Wed, 29 Jul 2026 20:03:53 +0800 Subject: [PATCH 1/4] feat(workflow): generate verified task DAGs --- docs/compose/spec/workflow-auto-dag.md | 66 +++++++++++++++++ packages/opencode/src/command/index.ts | 50 +++++++++++++ .../opencode/src/session/prompt/default.txt | 4 +- .../mimocode-docs/reference/workflows.md | 27 ++++++- packages/opencode/src/tool/workflow.txt | 9 ++- packages/opencode/src/workflow/sandbox.ts | 65 +++++++++++++++++ .../test/command/workflow-command.test.ts | 37 ++++++++++ .../opencode/test/workflow/sandbox.test.ts | 71 ++++++++++++++++++- 8 files changed, 323 insertions(+), 6 deletions(-) create mode 100644 docs/compose/spec/workflow-auto-dag.md create mode 100644 packages/opencode/test/command/workflow-command.test.ts diff --git a/docs/compose/spec/workflow-auto-dag.md b/docs/compose/spec/workflow-auto-dag.md new file mode 100644 index 000000000..2aa287fbd --- /dev/null +++ b/docs/compose/spec/workflow-auto-dag.md @@ -0,0 +1,66 @@ +--- +feature: workflow-auto-dag +status: in-progress +updated: 2026-07-29 +branch: feat/workflow-auto-dag +commits: 57ff02ed5ea76b448893b7c80faaf3a8de4d19ff.. +--- + +# Workflow Auto DAG + +## Report + +## [S1] Problem + +The dynamic Workflow Runtime can run JavaScript agents concurrently and resume them from its persisted journal, but users must currently author the orchestration script themselves or choose a fixed built-in workflow. There is no `/workflow ` entry point that asks the primary agent to turn a natural-language task into a customizable, validated task DAG with an independent acceptance gate and bounded rework. + +## [S2] Design + +### Slash-command contract + +Add the experimental `/workflow ` command beside `/deep-research`. Its expanded prompt instructs the current primary agent to: + +1. interpret `$ARGUMENTS` as natural language, optionally containing a JSON object that overrides node roles, prompts, models, tools, dependencies, verifier behavior, or the rework bound; +2. generate one self-contained inline JavaScript workflow whose static `meta` describes the run; +3. express implementation work as nodes passed to the sandbox `dag()` primitive, with each node carrying a stable ID, role, prompt, model, tools, and `dependsOn` list as needed; +4. launch each node through `agent()`, passing dependency results into its prompt and preserving the node's explicit role/model/tool choices; +5. run a fresh verifier agent only after the implementation DAG settles; the verifier judges the original task and concrete outputs independently, returns structured acceptance evidence, and cannot be the same call as an implementer; +6. perform at most the requested number of rework rounds (default 2, hard prompt-level cap 3), re-running the verifier after every round; return an honest non-accepted result when the bound is exhausted; +7. invoke `workflow({ operation: "run", script, args })` and relay its terminal result and run ID without claiming acceptance unless the verifier accepted it. + +The command remains behind `MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL`, because its required tool is behind the same flag. Invalid or empty user input is handled by the primary agent through one concise clarification rather than launching a guessed workflow. + +### Standard DAG primitive + +Expose `dag(nodes, run)` in every Workflow Sandbox alongside `parallel()` and `pipeline()`. + +- Each node is an object with a non-empty, unique string `id` and optional `dependsOn: string[]`. +- Validate the complete graph before invoking `run`: reject malformed nodes, duplicate IDs, duplicate dependencies, self-dependencies, unknown dependency IDs, and cycles with actionable `dag:` errors. +- Use Kahn topological sorting to produce deterministic ready batches in declaration order. +- Execute every ready batch concurrently with `Promise.all`; begin a node only after all dependencies completed. +- Invoke `run(node, dependencies)`, where `dependencies` is an ordered array of `{ id, result }` following the node's `dependsOn` order. +- Resolve to an array of `{ id, result }` in original node declaration order. The primitive does not interpret `null` or domain-specific verifier output; workflow scripts own those policies. + +Concurrency remains bounded by the Runtime's existing process-wide and per-run semaphores because DAG nodes call the existing `agent()` host hook. Persistence and recovery remain content-journal based: completed `agent()` calls replay on `resume`, while unfinished DAG nodes execute again. No new database schema is required. + +### Safety and failure behavior + +Graph validation runs entirely before node execution, so an invalid DAG launches zero agents. Script logic errors, including DAG structural errors, fail the workflow loudly through the existing sandbox error path. Agent transport failures retain the existing `agent() => null` and bounded transport retry contract; generated scripts must treat a required node's `null` result as a failed deliverable and must not report verifier acceptance. + +The existing nested-workflow lineage guard remains unchanged. It detects recursion between workflow scripts; `dag()` separately detects cycles between task nodes within one script. + +## [S3] Out of Scope + +- A visual DAG editor or new TUI detail layout. +- A new persisted DAG/node database schema; recovery continues to use the existing script and agent-result journal. +- Executing arbitrary JavaScript supplied directly by the user without primary-agent mediation. +- Automatically merging branches, opening pull requests, or approving destructive operations. +- Replacing `/compose-next` or the legacy built-in `compose` workflow. +- Treating a verifier verdict as a security boundary; it is an independent quality gate within the workflow. + +## Tasks + +- [ ] T1: add and test the sandbox `dag(nodes, run)` primitive — acceptance: valid dependency graphs execute in deterministic concurrent batches and return declaration-ordered results; every malformed-reference and cycle case rejects before any callback runs (covers: S2) +- [ ] T2: register and test `/workflow ` behind the workflow feature flag — acceptance: command autocomplete/listing exposes `workflow` only when the tool is enabled, and its expanded prompt requires inline DAG generation, role/prompt/model/dependency customization, independent verifier evidence, and bounded rework (covers: S2) +- [ ] T3: update model-facing workflow documentation — acceptance: the workflow tool contract names `dag()` and explains how scheduling, persistence recovery, task-cycle detection, verifier separation, and rework bounds compose (covers: S2) +- [ ] T4: run focused tests, workflow regression tests, typecheck, and diff checks — acceptance: all relevant commands pass from `packages/opencode` (covers: S2) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 0d7025afe..93cef56b2 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -66,6 +66,7 @@ export const Default = { DREAM: "dream", DISTILL: "distill", GOAL: "goal", + WORKFLOW: "workflow", DEEP_RESEARCH: "deep-research", LOOPS: "loops", REBUILD: "rebuild", @@ -89,6 +90,44 @@ export function deepResearchTemplate(): string { ].join("\n") } +export function workflowTemplate(): string { + return [ + "Turn the user's task into a deterministic multi-agent JavaScript DAG and run it with the workflow tool.", + "", + "Task and customization request:", + "$ARGUMENTS", + "", + "The input is a natural-language task and may include optional JSON overrides. Honor requested node roles, prompts,", + "models, tool allowlists, dependencies, verifier settings, and rework bounds when they are valid and safe.", + "If the task itself is empty or materially ambiguous, ask one concise clarification before generating anything.", + "", + "Generate one self-contained inline JavaScript workflow beginning with a pure-data `export const meta = { ... }`.", + "Create a `nodes` array with stable unique ids. Each node may define `agentType`, `prompt`, `model`, `tools`,", + "`dependsOn`, `schema`, and `isolation`. Execute implementation nodes with valid JavaScript shaped like:", + " const outputs = await dag(nodes, (node, dependencies) => agent(", + ' node.prompt + "\\nDependency outputs:\\n" + JSON.stringify(dependencies),', + " { agentType: node.agentType, model: node.model, tools: node.tools, schema: node.schema,", + ' isolation: node.isolation, label: node.id, phase: "Implement" },', + " ))", + "Do not hand-roll topological sorting. `dag(nodes, run)` validates all references and cycles before executing,", + "runs ready nodes concurrently, and preserves dependency ordering. Treat a required node result of null as failure.", + "", + "After implementation settles, dispatch an independent verifier using a separate agent() call and a strict schema.", + "The verifier must judge the original task against concrete DAG outputs and return `{ accepted, findings, evidence }`.", + "It must use an explicit verifier/reviewer agentType and, when available, a model at least as capable as the", + "strongest implementer. It must not be an implementation node or merely repeat an implementer's self-assessment.", + "", + "If verification rejects the result, perform bounded rework with the findings injected into the affected nodes,", + "then invoke a fresh independent verifier again. Use default 2 rework rounds with a hard maximum 3,", + "including when optional JSON asks for more. Stop honestly with `accepted: false` when the bound is exhausted.", + "Do not use agent transport retry as semantic rework; `agent({ retry })` is only for transient execution failures.", + "", + "Run the generated script with:", + ' workflow({ operation: "run", script, args: { task: } })', + "Relay the terminal result and run id. Never claim acceptance unless the final verifier returned accepted=true with evidence.", + ].join("\n") +} + export interface Interface { readonly get: (name: string) => Effect.Effect readonly list: () => Effect.Effect @@ -191,6 +230,17 @@ export const layer = Layer.effect( } if (Flag.MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL) { + commands[Default.WORKFLOW] = { + name: Default.WORKFLOW, + description: "generate and run a customizable verified multi-agent DAG", + source: "command", + bundled: true, + subtask: false, + get template() { + return workflowTemplate() + }, + hints: ["$ARGUMENTS"], + } commands[Default.DEEP_RESEARCH] = { name: Default.DEEP_RESEARCH, description: "deep multi-source, fact-checked research report (runs the deep-research workflow)", diff --git a/packages/opencode/src/session/prompt/default.txt b/packages/opencode/src/session/prompt/default.txt index 9d959d49c..c1e6342e7 100644 --- a/packages/opencode/src/session/prompt/default.txt +++ b/packages/opencode/src/session/prompt/default.txt @@ -94,7 +94,7 @@ Three orchestration primitives that look similar but serve different goals — p - **Tasks** (the `task_*` tools, registry in `src/task/`) are plan-state, not execution. Hierarchical IDs (`T1`, `T1.1`, `T1.2`...) persisted in SQLite. Use one per non-trivial unit of work; mark `in_progress` when you start and `completed` the moment it's done — never batch. - **Subagent dispatch** (the Agent tool, backed by Actor) spawns ONE subagent inline. Cheap, immediate, returns a single result. Use for focused delegations: exploration, review, isolated analysis. -- **Workflows** (the Workflow tool, runtime in `src/workflow/`) run a deterministic JavaScript script that orchestrates many subagents with `phase()`, `parallel()`, `pipeline()`, `agent()`. Hard limits enforced by the runtime: 12h script deadline, ≤1000 lifecycle agents per run, default concurrency of 16, shared token budget with the parent. Resume-from-journal is supported via `resumeFromRunId`. Only use workflows when the user explicitly opts into multi-agent orchestration, or for tasks too large for one subagent. +- **Workflows** (the Workflow tool, runtime in `src/workflow/`) run a deterministic JavaScript script that orchestrates many subagents with `phase()`, `parallel()`, `pipeline()`, `dag()`, `agent()`. Hard limits enforced by the runtime: 12h script deadline, ≤1000 lifecycle agents per run, default concurrency of 16, shared token budget with the parent. Resume-from-journal is supported via `resumeFromRunId`. Only use workflows when the user explicitly opts into multi-agent orchestration, or for tasks too large for one subagent. ### Skills @@ -169,4 +169,4 @@ In code: default to writing no comments. Never write multi-paragraph docstrings ## Session-specific guidance - Use the Agent tool with specialized agents when the task at hand matches the agent's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but they should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing - if you delegate research to a subagent, do not also perform the same searches yourself. - For broad codebase exploration or research that'll take more than 3 queries, spawn Agent with subagent_type=Explore. Otherwise use the Glob or Grep directly. - - When the user types `/`, invoke it via Skill. Only use skills listed in the user-invocable skills section — don't guess. \ No newline at end of file + - When the user types `/`, invoke it via Skill. Only use skills listed in the user-invocable skills section — don't guess. diff --git a/packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/workflows.md b/packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/workflows.md index 31636d095..56b0f015f 100644 --- a/packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/workflows.md +++ b/packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/workflows.md @@ -1,6 +1,6 @@ # MiMoCode Dynamic Workflows Reference -Dynamic workflows let you orchestrate many subagents **deterministically** from a small JavaScript script — fan-out, pipelines, nested workflows — instead of driving each subagent by hand. The script runs in a sandbox; you call `agent()` to spawn work and combine results with plain JS. +Dynamic workflows let you orchestrate many subagents **deterministically** from a small JavaScript script — fan-out, dependency DAGs, pipelines, nested workflows — instead of driving each subagent by hand. The script runs in a sandbox; you call `agent()` to spawn work and combine results with plain JS. ## Where to write workflow files @@ -46,6 +46,7 @@ Only `name` and `description` are required. The body is ordinary async JS with t | `agent(prompt, opts?)` | spawn one subagent | Promise → its deliverable (text, or a validated object if `opts.schema`), or **`null`** on failure. Never throws. | | `parallel(thunks)` | run an array of `() => Promise` concurrently | `Promise` (a throwing thunk rejects the batch) | | `pipeline(items, ...stages)` | run each item through sequential stages, items in parallel | `Promise` | +| `dag(nodes, run)` | validate and execute dependency nodes in ready batches | `Promise>` in declaration order | | `workflow(nameOrScript, args?, opts?)` | run a child workflow as its own sub-run and await it | Promise → child result, or `null` on runtime failure | | `readFile(path)` | read a workspace file | `Promise` (null if absent) | | `writeFile(path, content)` | write a workspace file (auto-creates dirs) | `Promise` | @@ -73,6 +74,30 @@ const out = await agent("Summarize src/parser.ts", { With `schema`, the deliverable is a **validated object** (never prose). Without it, you get the agent's final text. +### `dag()` nodes + +Use `dag()` when work has explicit prerequisites: + +```js +const outputs = await dag( + [ + { id: "api", role: "implementer", prompt: "Build the API", model: "standard" }, + { id: "ui", role: "implementer", prompt: "Build the UI", model: "standard" }, + { id: "integration", role: "integrator", prompt: "Connect both", dependsOn: ["api", "ui"] }, + ], + (node, dependencies) => agent( + node.prompt + "\nDependency outputs:\n" + JSON.stringify(dependencies), + { agentType: node.role, model: node.model, label: node.id }, + ), +) +``` + +Every node needs a unique non-empty string `id`; `dependsOn` defaults to `[]`. The complete graph is validated before the callback runs. Unknown references, duplicate IDs or dependencies, self-dependencies, and cycles throw actionable `dag:` errors and launch zero nodes. Nodes ready at the same time run concurrently. The callback receives dependency outputs in `dependsOn` order, and the final array preserves declaration order. + +`dag()` is a deterministic guest scheduler, not a second persistence engine. Calls to `agent()` inside the callback use the normal host semaphore and result journal. Resuming replays completed calls and executes unfinished calls, so keep the script and node-derived agent parameters deterministic. + +For workflows that modify or deliver artifacts, add a separate verifier `agent()` after `dag()` completes. Give it the original requirements, concrete outputs, and a strict structured schema such as `{ accepted, findings, evidence }`. If it rejects the result, feed its findings into a bounded rework pass and invoke a fresh verifier; do not confuse semantic rework with `agent({ retry })`, which only retries transient execution failures. + ## Determinism constraints The sandbox removes non-deterministic APIs so a run can be replayed/resumed identically: no `Date`, `crypto`, `fetch`, timers, or `process`; `Math.random` is a seeded PRNG. Do network/time-dependent work **inside** `agent()` (a real subagent), not in the orchestration script. diff --git a/packages/opencode/src/tool/workflow.txt b/packages/opencode/src/tool/workflow.txt index ab20b5a06..fe2e20836 100644 --- a/packages/opencode/src/tool/workflow.txt +++ b/packages/opencode/src/tool/workflow.txt @@ -1,4 +1,4 @@ -Execute a workflow script that orchestrates multiple subagents deterministically. The script is plain JavaScript that runs in a sandbox and fans out subagents via agent(), parallel(), and pipeline(). The "run" operation BLOCKS until the workflow reaches a terminal status (completed/failed/cancelled) and returns the full transcript (phase transitions + log() messages) followed by the script's return value. Phase and log lines stream into the conversation as the workflow runs. +Execute a workflow script that orchestrates multiple subagents deterministically. The script is plain JavaScript that runs in a sandbox and fans out subagents via agent(), parallel(), pipeline(), and dag(). The "run" operation BLOCKS until the workflow reaches a terminal status (completed/failed/cancelled) and returns the full transcript (phase transitions + log() messages) followed by the script's return value. Phase and log lines stream into the conversation as the workflow runs. operation "run": start a workflow and block until it terminates. Provide either `name` (a built-in workflow, see the catalog below) or `script` (inline JS; must begin with `export const meta = { name, description }`). Returns the transcript + result. Optionally provide `workspace` (a dir the script's file primitives are jailed to; defaults to the worktree). Note: long workflows (e.g. deep-research) hold the agent's turn for the full duration — that is intentional, mirroring skill semantics. operation "status": check a run's status by run_id. @@ -8,8 +8,9 @@ operation "resume": re-launch a persisted workflow by run_id under the same run_ Inside the script you can call: - agent(prompt, opts?) -> Promise: spawn one subagent; resolves to its result (a validated object if opts.schema given, else its final text) or null on failure. Never throws. -- parallel(thunks) -> Promise: run thunks concurrently; a thunk that throws resolves to null. +- parallel(thunks) -> Promise: run thunks concurrently; a thunk that throws rejects the batch. - pipeline(items, ...stages) -> Promise: run each item through all stages; no barrier between stages. +- dag(nodes, run) -> Promise<{id,result}[]>: validate and execute a dependency DAG. Each node needs a unique non-empty `id` and may have `dependsOn: string[]`. Invalid references, duplicate ids/dependencies, self-dependencies, and cycles throw BEFORE run is called. Every ready batch runs concurrently; run(node, dependencies) receives ordered `{id,result}` dependency outputs. Results preserve node declaration order. Use agent() inside run, so host concurrency limits and journal recovery still apply. - workflow(nameOrScript, args?, opts?) -> Promise: run a CHILD workflow as its own sub-run and await its result. nameOrScript is either an inline script (starts with `export const meta`) or a saved workflow name resolved from .mimocode/workflows/ or .claude/workflows/. args is passed to the child as its `args`. opts: { workspace?, maxConcurrentAgents? }. A failed child resolves to null (never throws); an unknown name or a cycle/over-depth throws (fails the run). Children share the process-wide concurrency ceiling and inherit the parent workspace by default. - readFile(path) -> Promise: read a file in the workspace (null if absent). - writeFile(path, content) -> Promise: write a file in the workspace (parent dirs auto-created). @@ -20,6 +21,10 @@ Inside the script you can call: Concurrency is capped by the host (default min(16, 2x cores)); excess agent() calls queue automatically. +Persistence is attached to agent() calls, not to a separate DAG table. On resume the saved script runs again: completed agent calls replay from the content journal and unfinished calls execute. Keep node ids, prompts, roles, models, schemas, phases, and call order deterministic. A changed script SHA starts with a fresh journal. + +For acceptance-gated workflows, invoke a fresh independent verifier agent after the implementation DAG settles. Give it the original task, concrete outputs, a strict `{accepted, findings, evidence}` schema, and a separate role/model from implementers. Semantic verifier rejection is NOT an agent transport retry: feed findings into bounded rework (recommended default 2, maximum 3), then run a new verifier. Report `accepted: false` when the bound is exhausted. + Communicate between workflows by dataflow: return a value from a child and pass it as args to the next (or write a shared file via writeFile and read it in a later phase). Workflows do not message each other directly. File primitives are jailed to the workspace root (the project worktree by default, or the `workspace` you pass to the run op) by a LEXICAL name check — it blocks `..` and absolute escapes, but does not resolve symlinks, so treat it as scoping, not a hard security boundary. workflow() cycle detection is asymmetric: a SAVED name is checked by name (so A calling A is a cycle regardless of args), while an INLINE script is checked by content+args (so an inline body that calls itself with DIFFERENT args is not flagged as a cycle and is bounded only by maxDepth). A cycle, over-maxDepth, or unknown name THROWS at the workflow() call site, which fails the run if uncaught — but a try/catch around workflow() in the guest will silence those throws, so don't wrap workflow() in try/catch unless you intend to ignore configuration bugs. diff --git a/packages/opencode/src/workflow/sandbox.ts b/packages/opencode/src/workflow/sandbox.ts index f4b8fa9e7..c8895be86 100644 --- a/packages/opencode/src/workflow/sandbox.ts +++ b/packages/opencode/src/workflow/sandbox.ts @@ -72,6 +72,71 @@ globalThis.parallel = (thunks) => globalThis.pipeline = (items, ...stages) => Promise.all(items.map((item, index) => stages.reduce((acc, stage) => acc.then((prev) => stage(prev, item, index)), Promise.resolve(item)))); +globalThis.dag = async (nodes, run) => { + if (typeof run !== "function") throw new TypeError("dag: run must be a function"); + if (!Array.isArray(nodes)) throw new TypeError("dag: nodes must be an array"); + + const byId = Object.create(null); + const dependencies = Object.create(null); + for (let index = 0; index < nodes.length; index++) { + const node = nodes[index]; + if (!node || typeof node !== "object" || Array.isArray(node)) { + throw new TypeError("dag: node at index " + index + " must be an object"); + } + if (typeof node.id !== "string" || !node.id.trim()) { + throw new TypeError("dag: node at index " + index + " needs a non-empty string id"); + } + if (byId[node.id]) throw new TypeError("dag: duplicate node id " + JSON.stringify(node.id)); + if (node.dependsOn !== undefined && !Array.isArray(node.dependsOn)) { + throw new TypeError("dag: node " + JSON.stringify(node.id) + " dependsOn must be an array"); + } + const deps = node.dependsOn || []; + const seen = Object.create(null); + for (const dependency of deps) { + if (typeof dependency !== "string" || !dependency.trim()) { + throw new TypeError("dag: node " + JSON.stringify(node.id) + " dependency ids must be non-empty strings"); + } + if (seen[dependency]) { + throw new TypeError("dag: node " + JSON.stringify(node.id) + " has duplicate dependency " + JSON.stringify(dependency)); + } + if (dependency === node.id) throw new TypeError("dag: node " + JSON.stringify(node.id) + " cannot depend on itself"); + seen[dependency] = true; + } + byId[node.id] = node; + dependencies[node.id] = deps; + } + + for (const node of nodes) { + for (const dependency of dependencies[node.id]) { + if (!byId[dependency]) { + throw new TypeError("dag: node " + JSON.stringify(node.id) + " depends on unknown node " + JSON.stringify(dependency)); + } + } + } + + const pending = nodes.map((node) => node.id); + const completed = Object.create(null); + const batches = []; + while (pending.length) { + const ready = pending.filter((id) => dependencies[id].every((dependency) => completed[dependency])); + if (!ready.length) throw new TypeError("dag: cycle detected among " + pending.join(", ")); + batches.push(ready); + for (const id of ready) completed[id] = true; + for (let index = pending.length - 1; index >= 0; index--) { + if (completed[pending[index]]) pending.splice(index, 1); + } + } + + const results = Object.create(null); + for (const batch of batches) { + const values = await Promise.all(batch.map((id) => Promise.resolve().then(() => run( + byId[id], + dependencies[id].map((dependency) => ({ id: dependency, result: results[dependency] })), + )))); + for (let index = 0; index < batch.length; index++) results[batch[index]] = values[index]; + } + return nodes.map((node) => ({ id: node.id, result: results[node.id] })); +}; // Minimal, deterministic URL for dedup/host-extraction in workflow scripts. // The bare QuickJS guest has no Web URL. Covers protocol/hostname/pathname/ // search/hash — enough for normURL-style dedup — and THROWS on inputs without diff --git a/packages/opencode/test/command/workflow-command.test.ts b/packages/opencode/test/command/workflow-command.test.ts new file mode 100644 index 000000000..a6058c4b4 --- /dev/null +++ b/packages/opencode/test/command/workflow-command.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test" +import { Command, workflowTemplate } from "../../src/command" + +describe("/workflow command", () => { + test("Default has the workflow name", () => { + expect(Command.Default.WORKFLOW).toBe("workflow") + }) + + test("template asks the primary agent to generate and run a customizable JavaScript DAG", () => { + const template = workflowTemplate() + expect(template).toContain("$ARGUMENTS") + expect(template).toContain("dag(nodes") + expect(template).toContain("agentType") + expect(template).toContain("prompt") + expect(template).toContain("model") + expect(template).toContain("dependsOn") + expect(template).toContain('operation: "run"') + expect(template).toContain("script") + }) + + test("template requires an independent verifier and bounded rework", () => { + const template = workflowTemplate() + expect(template).toContain("independent verifier") + expect(template).toContain("verifier/reviewer agentType") + expect(template).toContain("at least as capable") + expect(template).toContain("default 2") + expect(template).toContain("hard maximum 3") + expect(template).toContain("accepted") + expect(template).toContain("evidence") + }) + + test("template supports optional JSON customization without requiring it", () => { + const template = workflowTemplate() + expect(template).toContain("optional JSON") + expect(template).toContain("natural-language task") + }) +}) diff --git a/packages/opencode/test/workflow/sandbox.test.ts b/packages/opencode/test/workflow/sandbox.test.ts index 1f5643ea4..0e5125bb1 100644 --- a/packages/opencode/test/workflow/sandbox.test.ts +++ b/packages/opencode/test/workflow/sandbox.test.ts @@ -169,7 +169,7 @@ describe("Sandbox resource guards + hygiene", () => { }) }) -describe("Sandbox prelude (parallel/pipeline/args)", () => { +describe("Sandbox prelude (parallel/pipeline/dag/args)", () => { test("parallel is provided without a guest helper", async () => { const hooks = { agent: async (n: unknown) => `done-${n}` } const body = `return (await parallel([() => agent("a"), () => agent("b")]))` @@ -200,6 +200,75 @@ describe("Sandbox prelude (parallel/pipeline/args)", () => { await expect(evalScript(body, {})).rejects.toThrow(/boom-stage/) }) + test("dag runs ready nodes concurrently and passes ordered dependency results", async () => { + const events: string[] = [] + const hooks = { + agent: async (id: unknown) => { + events.push(`start:${id}`) + await new Promise((resolve) => setTimeout(resolve, id === "A" ? 20 : 5)) + events.push(`end:${id}`) + return `result:${id}` + }, + } + const body = ` + return await dag([ + { id: "A", dependsOn: [] }, + { id: "B" }, + { id: "C", dependsOn: ["B", "A"] }, + ], async (node, dependencies) => ({ + value: await agent(node.id), + dependencies, + })) + ` + const result = await evalScript(body, hooks) + expect(events.slice(0, 2).sort()).toEqual(["start:A", "start:B"]) + expect(events.indexOf("start:C")).toBeGreaterThan(events.indexOf("end:A")) + expect(events.indexOf("start:C")).toBeGreaterThan(events.indexOf("end:B")) + expect(result).toEqual([ + { id: "A", result: { value: "result:A", dependencies: [] } }, + { id: "B", result: { value: "result:B", dependencies: [] } }, + { + id: "C", + result: { + value: "result:C", + dependencies: [ + { id: "B", result: { value: "result:B", dependencies: [] } }, + { id: "A", result: { value: "result:A", dependencies: [] } }, + ], + }, + }, + ]) + }) + + test.each([ + ["nodes must be an array", `{}`], + ["node at index 0 must be an object", `[null]`], + ["node at index 0 needs a non-empty string id", `[{ id: "" }]`], + ["duplicate node id", `[{ id: "A" }, { id: "A" }]`], + ["dependsOn must be an array", `[{ id: "A", dependsOn: "B" }]`], + ["dependency ids must be non-empty strings", `[{ id: "A", dependsOn: [""] }]`], + ["duplicate dependency", `[{ id: "A" }, { id: "B", dependsOn: ["A", "A"] }]`], + ["cannot depend on itself", `[{ id: "A", dependsOn: ["A"] }]`], + ["depends on unknown node", `[{ id: "A", dependsOn: ["missing"] }]`], + ["cycle detected", `[{ id: "A", dependsOn: ["B"] }, { id: "B", dependsOn: ["A"] }]`], + ])("dag rejects %s before running a node", async (message, nodes) => { + let calls = 0 + const hooks = { + agent: async () => { + calls++ + return "unexpected" + }, + } + await expect(evalScript(`return await dag(${nodes}, (node) => agent(node.id))`, hooks)).rejects.toThrow( + new RegExp(`dag:.*${message}`), + ) + expect(calls).toBe(0) + }) + + test("dag requires a run callback before validating or executing nodes", async () => { + await expect(evalScript(`return await dag([], null)`, {})).rejects.toThrow(/dag: run must be a function/) + }) + test("args is injected as a guest global", async () => { const result = await evalScript(`return args.foo`, {}, { args: { foo: 42 } }) expect(result).toBe(42) From 704b3ee51feedcac7f2df52bf73fc02865c864e9 Mon Sep 17 00:00:00 2001 From: fanhuanjie Date: Wed, 29 Jul 2026 20:04:34 +0800 Subject: [PATCH 2/4] docs(workflow): finalize auto DAG delivery --- docs/compose/spec/workflow-auto-dag.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/compose/spec/workflow-auto-dag.md b/docs/compose/spec/workflow-auto-dag.md index 2aa287fbd..1700eabbf 100644 --- a/docs/compose/spec/workflow-auto-dag.md +++ b/docs/compose/spec/workflow-auto-dag.md @@ -1,15 +1,29 @@ --- feature: workflow-auto-dag -status: in-progress +status: delivered updated: 2026-07-29 branch: feat/workflow-auto-dag -commits: 57ff02ed5ea76b448893b7c80faaf3a8de4d19ff.. +commits: 57ff02ed5ea76b448893b7c80faaf3a8de4d19ff..559901bf45a2d95b0759c677c8d2eec1318885d6 --- # Workflow Auto DAG ## Report +**What was built** — `/workflow ` now expands into a primary-agent contract that generates and launches one inline JavaScript dependency DAG. Natural language remains the default input, while optional JSON can customize each node's role, prompt, model, tools, schema, isolation, and dependencies. The generated workflow runs an independent structured Verifier after implementation and permits two rework rounds by default, with a hard maximum of three. + +The sandbox now provides `dag(nodes, run)`. It validates the complete graph before invoking any node, rejects malformed nodes, duplicate IDs or dependencies, self-dependencies, unknown references, and cycles, then executes each topological ready batch concurrently. Dependency outputs and final results retain deterministic ordering. Existing `agent()` semaphores and content journals continue to provide concurrency limits and resume behavior without a new database schema. + +**Verification** — `bun test test/command/workflow-command.test.ts test/command/deep-research-command.test.ts test/workflow/sandbox.test.ts test/workflow/builtin.test.ts test/tool/describe-workflow.test.ts` — PASS, 51 tests / 258 assertions. `bun test test/workflow/runtime-nested.test.ts test/workflow/retry.test.ts test/workflow/persistence.test.ts` — PASS, 26 passed / 1 skipped / 78 assertions. Final changed-file rerun (`workflow-command` + `sandbox`) — PASS, 43 tests / 239 assertions. `bun typecheck` — PASS. `git diff --check` — PASS. `test/workflow/runtime.test.ts` remains PRE-EXISTING/ENVIRONMENTAL: isolated rerun had four existing 5-second timeouts; the focused sandbox and stable runtime/persistence bands pass. + +**Journey log** + +1. The existing runtime already had the difficult stateful pieces: process-wide agent throttling, script persistence, content-journal replay, and nested-workflow cycle checks. A deterministic guest scheduler was enough; a second DAG persistence model would have duplicated invariants. +2. The legacy `compose.js` Kahn scheduler silently drops unknown dependencies. The reusable primitive instead validates the whole graph before execution so malformed generated plans cannot launch partial work. +3. Verifier rejection is semantic feedback, not a transient transport error. The command contract keeps bounded rework separate from `agent({ retry })`. +4. Independent review found no critical, high, or medium findings and identified one stale primitive list in the primary-agent prompt; the list was updated before final verification. +5. The broader `runtime.test.ts` band has timing/process instability independent of this change; focused sandbox tests and stable runtime/persistence suites provide direct evidence for the modified boundary. + ## [S1] Problem The dynamic Workflow Runtime can run JavaScript agents concurrently and resume them from its persisted journal, but users must currently author the orchestration script themselves or choose a fixed built-in workflow. There is no `/workflow ` entry point that asks the primary agent to turn a natural-language task into a customizable, validated task DAG with an independent acceptance gate and bounded rework. @@ -60,7 +74,7 @@ The existing nested-workflow lineage guard remains unchanged. It detects recursi ## Tasks -- [ ] T1: add and test the sandbox `dag(nodes, run)` primitive — acceptance: valid dependency graphs execute in deterministic concurrent batches and return declaration-ordered results; every malformed-reference and cycle case rejects before any callback runs (covers: S2) -- [ ] T2: register and test `/workflow ` behind the workflow feature flag — acceptance: command autocomplete/listing exposes `workflow` only when the tool is enabled, and its expanded prompt requires inline DAG generation, role/prompt/model/dependency customization, independent verifier evidence, and bounded rework (covers: S2) -- [ ] T3: update model-facing workflow documentation — acceptance: the workflow tool contract names `dag()` and explains how scheduling, persistence recovery, task-cycle detection, verifier separation, and rework bounds compose (covers: S2) -- [ ] T4: run focused tests, workflow regression tests, typecheck, and diff checks — acceptance: all relevant commands pass from `packages/opencode` (covers: S2) +- [x] T1: add and test the sandbox `dag(nodes, run)` primitive — acceptance: valid dependency graphs execute in deterministic concurrent batches and return declaration-ordered results; every malformed-reference and cycle case rejects before any callback runs (covers: S2) +- [x] T2: register and test `/workflow ` behind the workflow feature flag — acceptance: command autocomplete/listing exposes `workflow` only when the tool is enabled, and its expanded prompt requires inline DAG generation, role/prompt/model/dependency customization, independent verifier evidence, and bounded rework (covers: S2) +- [x] T3: update model-facing workflow documentation — acceptance: the workflow tool contract names `dag()` and explains how scheduling, persistence recovery, task-cycle detection, verifier separation, and rework bounds compose (covers: S2) +- [x] T4: run focused tests, workflow regression tests, typecheck, and diff checks — acceptance: all relevant commands pass from `packages/opencode` (covers: S2) From 5e062ec2c9420a9ce650b72282c649cf14f5d26f Mon Sep 17 00:00:00 2001 From: fanhuanjie Date: Wed, 29 Jul 2026 20:17:43 +0800 Subject: [PATCH 3/4] feat(workflow): enable workflows by default --- docs/compose/spec/workflow-auto-dag.md | 2 +- packages/opencode/src/flag/flag.ts | 8 +++----- packages/opencode/test/flag/workflow-tool.test.ts | 9 +++++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/compose/spec/workflow-auto-dag.md b/docs/compose/spec/workflow-auto-dag.md index 1700eabbf..86b15287d 100644 --- a/docs/compose/spec/workflow-auto-dag.md +++ b/docs/compose/spec/workflow-auto-dag.md @@ -42,7 +42,7 @@ Add the experimental `/workflow ` command beside `/deep-research`. Its exp 6. perform at most the requested number of rework rounds (default 2, hard prompt-level cap 3), re-running the verifier after every round; return an honest non-accepted result when the bound is exhausted; 7. invoke `workflow({ operation: "run", script, args })` and relay its terminal result and run ID without claiming acceptance unless the verifier accepted it. -The command remains behind `MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL`, because its required tool is behind the same flag. Invalid or empty user input is handled by the primary agent through one concise clarification rather than launching a guessed workflow. +The command and its required tool share `MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL`, which is enabled by default and accepts `false` or `0` as an explicit opt-out. Invalid or empty user input is handled by the primary agent through one concise clarification rather than launching a guessed workflow. ### Standard DAG primitive diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index f1bf53217..7e81dc3a6 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -233,11 +233,9 @@ export const Flag = { // global singleton workspace and child permission-approval routing. Enable with // MIMOCODE_EXPERIMENTAL_ORCHESTRATOR=true (or the umbrella MIMOCODE_EXPERIMENTAL). MIMOCODE_EXPERIMENTAL_ORCHESTRATOR: MIMOCODE_EXPERIMENTAL || truthy("MIMOCODE_EXPERIMENTAL_ORCHESTRATOR"), - // Defaults to OFF (opt-in): dynamic workflows and built-in workflows. - // Enable with MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL=true (or the umbrella - // MIMOCODE_EXPERIMENTAL flag). - MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL: - MIMOCODE_EXPERIMENTAL || truthy("MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL"), + // Defaults to ON: dynamic workflows, built-in workflows, and `/workflow`. + // Set MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL=false to opt out. + MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL: !falsy("MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL"), // Defaults to true: cron + self-paced loop scheduling are on by default. // Set MIMOCODE_EXPERIMENTAL_CRON=false to opt out. Runtime kill switch is // MIMOCODE_DISABLE_CRON (checked live every tick). diff --git a/packages/opencode/test/flag/workflow-tool.test.ts b/packages/opencode/test/flag/workflow-tool.test.ts index 2ca444226..13b8f8a22 100644 --- a/packages/opencode/test/flag/workflow-tool.test.ts +++ b/packages/opencode/test/flag/workflow-tool.test.ts @@ -20,18 +20,19 @@ function read(value?: string, experimental?: string) { } describe("MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL", () => { - test("is disabled by default and accepts explicit truthy values", () => { - expect(read()).toBe("false") + test("is enabled by default and accepts explicit truthy values", () => { + expect(read()).toBe("true") expect(read("true")).toBe("true") expect(read("1")).toBe("true") }) - test("is enabled by the umbrella experimental flag", () => { + test("remains enabled with the umbrella experimental flag", () => { expect(read(undefined, "true")).toBe("true") }) - test("false and zero keep the tool disabled", () => { + test("false and zero explicitly disable the tool", () => { expect(read("false")).toBe("false") expect(read("0")).toBe("false") + expect(read("false", "true")).toBe("false") }) }) From bd7554290a2a645b82c8e4e4046ad482086a25b1 Mon Sep 17 00:00:00 2001 From: fanhuanjie Date: Wed, 29 Jul 2026 21:48:57 +0800 Subject: [PATCH 4/4] fix(tui): rename /workflows to /worklist to avoid /workflow clash Clarify the run-list slash command and localize descriptions for both /worklist and /workflow. Co-authored-by: Cursor --- packages/opencode/src/cli/cmd/tui/app.tsx | 3 ++- .../opencode/src/cli/cmd/tui/component/dialog-workflows.tsx | 4 +++- packages/opencode/src/cli/cmd/tui/i18n/en.ts | 6 +++++- packages/opencode/src/cli/cmd/tui/i18n/es.ts | 6 +++++- packages/opencode/src/cli/cmd/tui/i18n/fr.ts | 6 +++++- packages/opencode/src/cli/cmd/tui/i18n/ja.ts | 4 +++- packages/opencode/src/cli/cmd/tui/i18n/ru.ts | 6 +++++- packages/opencode/src/cli/cmd/tui/i18n/slash-command.ts | 2 +- packages/opencode/src/cli/cmd/tui/i18n/zh.ts | 4 +++- packages/opencode/src/cli/cmd/tui/i18n/zht.ts | 4 +++- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 2 +- .../builtin/.bundle/mimocode-docs/reference/commands.md | 3 ++- packages/opencode/src/tool/workflow.ts | 2 +- 13 files changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 6416f1941..7198b903b 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -590,11 +590,12 @@ function App(props: { onSnapshot?: () => Promise }) { }, { title: t("tui.command.workflow.list.title"), + description: t("tui.command.workflow.list.description"), value: "workflow.list", category: "session", enabled: Flag.MIMOCODE_EXPERIMENTAL_WORKFLOW_TOOL, slash: { - name: "workflows", + name: "worklist", }, onSelect: () => { dialog.replace(() => ) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workflows.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workflows.tsx index 8c857fab4..9da328032 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workflows.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workflows.tsx @@ -2,12 +2,14 @@ import { useDialog } from "@tui/ui/dialog" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" +import { useLanguage } from "@tui/context/language" import { createMemo, onCleanup, onMount } from "solid-js" export function DialogWorkflows() { const dialog = useDialog() const route = useRoute() const sync = useSync() + const { t } = useLanguage() const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) @@ -47,5 +49,5 @@ export function DialogWorkflows() { })) }) - return + return } diff --git a/packages/opencode/src/cli/cmd/tui/i18n/en.ts b/packages/opencode/src/cli/cmd/tui/i18n/en.ts index c7ef61c62..86efd8654 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/en.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/en.ts @@ -200,6 +200,8 @@ export const dict: Record = { "find repeated workflows in recent work and package them into skills, subagents, or commands", "tui.slash.goal.description": "set a stop-condition goal; runs until a judge says it's met. /goal clear to abort", + "tui.slash.workflow.description": + "generate and run a customizable verified multi-agent DAG for a task", "tui.slash.deep-research.description": "deep multi-source, fact-checked research report (runs the deep-research workflow)", @@ -260,7 +262,9 @@ export const dict: Record = { // App-level commands "tui.command.session.list.title": "Switch session", "tui.command.session.new.title": "New session", - "tui.command.workflow.list.title": "Workflows", + "tui.command.workflow.list.title": "Workflow run list", + "tui.command.workflow.list.description": + "Browse and open workflow runs in this session (not /workflow)", "tui.command.model.list.title": "Switch model", "tui.command.model.cycle_recent.title": "Model cycle", "tui.command.model.cycle_recent_reverse.title": "Model cycle reverse", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/es.ts b/packages/opencode/src/cli/cmd/tui/i18n/es.ts index 67ba7fffd..fb8363edb 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/es.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/es.ts @@ -274,6 +274,8 @@ export const dict = { "encontrar flujos repetidos en el trabajo reciente y empaquetarlos en skills, subagentes o comandos", "tui.slash.goal.description": "definir un objetivo con condición de parada; se ejecuta hasta que un juez confirme. /goal clear para abortar", + "tui.slash.workflow.description": + "generar y ejecutar un DAG multiagente personalizable y verificado para una tarea", "tui.slash.deep-research.description": "informe de investigación profunda multi-fuente y verificado (ejecuta el workflow deep-research)", @@ -335,7 +337,9 @@ export const dict = { // App-level commands "tui.command.session.list.title": "Cambiar sesión", "tui.command.session.new.title": "Nueva sesión", - "tui.command.workflow.list.title": "Flujos de trabajo", + "tui.command.workflow.list.title": "Lista de ejecuciones de workflow", + "tui.command.workflow.list.description": + "Explorar y abrir ejecuciones de workflow de esta sesión (no es /workflow)", "tui.command.model.list.title": "Cambiar modelo", "tui.command.model.cycle_recent.title": "Ciclo de modelos", "tui.command.model.cycle_recent_reverse.title": "Ciclo de modelos (inverso)", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/fr.ts b/packages/opencode/src/cli/cmd/tui/i18n/fr.ts index 28b6269ca..7249c36e4 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/fr.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/fr.ts @@ -262,6 +262,8 @@ export const dict = { "trouver les workflows répétés dans le travail récent et les empaqueter en skills, sous-agents ou commandes", "tui.slash.goal.description": "définir un objectif avec condition d'arrêt ; s'exécute jusqu'à ce qu'un juge confirme. /goal clear pour annuler", + "tui.slash.workflow.description": + "générer et exécuter un DAG multi-agents personnalisable et vérifié pour une tâche", "tui.slash.deep-research.description": "rapport de recherche approfondi multi-sources et vérifié (exécute le workflow deep-research)", @@ -323,7 +325,9 @@ export const dict = { // App-level commands "tui.command.session.list.title": "Changer de session", "tui.command.session.new.title": "Nouvelle session", - "tui.command.workflow.list.title": "Workflows", + "tui.command.workflow.list.title": "Liste des exécutions de workflow", + "tui.command.workflow.list.description": + "Parcourir et ouvrir les exécutions de workflow de cette session (pas /workflow)", "tui.command.model.list.title": "Changer de modèle", "tui.command.model.cycle_recent.title": "Modèles récents", "tui.command.model.cycle_recent_reverse.title": "Modèles récents (inverse)", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/ja.ts b/packages/opencode/src/cli/cmd/tui/i18n/ja.ts index 020189a11..1410f52e3 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/ja.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/ja.ts @@ -213,6 +213,7 @@ export const dict = { "tui.slash.dream.description": "memory ファイルと生の軌跡からプロジェクトメモリを手動で統合", "tui.slash.distill.description": "最近の作業から繰り返しワークフローを見つけ、skill・サブエージェント・コマンドにパッケージ化", "tui.slash.goal.description": "停止条件付きゴールを設定;判定が達成と言うまで実行。/goal clear で中止", + "tui.slash.workflow.description": "タスクから検証付きのカスタム複数エージェント DAG を生成して実行", "tui.slash.deep-research.description": "深い多ソース・ファクトチェック済み調査レポート(deep-research ワークフローを実行)", // Built-in bundled skill descriptions (user-facing, decoupled from SKILL.md description which targets the LLM) @@ -267,7 +268,8 @@ export const dict = { // App-level commands "tui.command.session.list.title": "セッションを切り替え", "tui.command.session.new.title": "新規セッション", - "tui.command.workflow.list.title": "ワークフロー", + "tui.command.workflow.list.title": "ワークフロー実行一覧", + "tui.command.workflow.list.description": "このセッションのワークフロー実行を閲覧・開く(/workflow ではない)", "tui.command.model.list.title": "モデルを切り替え", "tui.command.model.cycle_recent.title": "モデルを循環", "tui.command.model.cycle_recent_reverse.title": "モデルを逆循環", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/ru.ts b/packages/opencode/src/cli/cmd/tui/i18n/ru.ts index 781c3964d..d8ad9685b 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/ru.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/ru.ts @@ -276,6 +276,8 @@ export const dict = { "найти повторяющиеся workflow в недавней работе и упаковать их в skills, субагентов или команды", "tui.slash.goal.description": "задать цель с условием остановки; выполняется, пока судья не подтвердит. /goal clear для отмены", + "tui.slash.workflow.description": + "сгенерировать и запустить настраиваемый проверенный multi-agent DAG для задачи", "tui.slash.deep-research.description": "глубокий многоисточниковый проверенный отчёт (запускает workflow deep-research)", @@ -338,7 +340,9 @@ export const dict = { // App-level commands "tui.command.session.list.title": "Сменить сессию", "tui.command.session.new.title": "Новая сессия", - "tui.command.workflow.list.title": "Рабочие процессы", + "tui.command.workflow.list.title": "Список запусков workflow", + "tui.command.workflow.list.description": + "Просмотреть и открыть запуски workflow в этой сессии (не /workflow)", "tui.command.model.list.title": "Сменить модель", "tui.command.model.cycle_recent.title": "Цикл моделей", "tui.command.model.cycle_recent_reverse.title": "Цикл моделей (в обратном порядке)", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/slash-command.ts b/packages/opencode/src/cli/cmd/tui/i18n/slash-command.ts index d796d4272..198268953 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/slash-command.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/slash-command.ts @@ -1,4 +1,4 @@ -const BUILTIN = new Set(["init", "review", "dream", "distill", "goal", "deep-research"]) +const BUILTIN = new Set(["init", "review", "dream", "distill", "goal", "workflow", "deep-research"]) export function slashCommandDescription( t: (key: string) => string, diff --git a/packages/opencode/src/cli/cmd/tui/i18n/zh.ts b/packages/opencode/src/cli/cmd/tui/i18n/zh.ts index 1f3281ce6..818938981 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/zh.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/zh.ts @@ -190,6 +190,7 @@ export const dict = { "tui.slash.dream.description": "从 memory 文件与原始轨迹中手动整合项目记忆", "tui.slash.distill.description": "在最近工作中发现重复流程,打包为 skill、子智能体或命令", "tui.slash.goal.description": "设置终止条件目标;运行直到判定达成。使用 /goal clear 中止", + "tui.slash.workflow.description": "根据任务生成并运行可自定义的、带验证的多智能体 DAG", "tui.slash.deep-research.description": "深度多来源、事实核查的研究报告(运行 deep-research 工作流)", // Built-in bundled skill descriptions (user-facing, decoupled from SKILL.md description which targets the LLM) @@ -283,7 +284,8 @@ export const dict = { // App-level commands "tui.command.session.list.title": "切换会话", "tui.command.session.new.title": "新建会话", - "tui.command.workflow.list.title": "工作流", + "tui.command.workflow.list.title": "工作流运行列表", + "tui.command.workflow.list.description": "浏览并打开本会话中的工作流运行(不是 /workflow)", "tui.command.model.list.title": "切换模型", "tui.command.model.cycle_recent.title": "循环切换模型", "tui.command.model.cycle_recent_reverse.title": "反向循环切换模型", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/zht.ts b/packages/opencode/src/cli/cmd/tui/i18n/zht.ts index 8ce57f130..70f5b426d 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/zht.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/zht.ts @@ -190,6 +190,7 @@ export const dict = { "tui.slash.dream.description": "從 memory 檔案與原始軌跡中手動整合專案記憶", "tui.slash.distill.description": "在最近工作中發現重複流程,打包為 skill、子智慧代理或命令", "tui.slash.goal.description": "設定終止條件目標;執行直到判定達成。使用 /goal clear 中止", + "tui.slash.workflow.description": "根據任務產生並執行可自訂、帶驗證的多智能體 DAG", "tui.slash.deep-research.description": "深度多來源、事實核查的研究報告(執行 deep-research 工作流程)", // Built-in bundled skill descriptions (user-facing, decoupled from SKILL.md description which targets the LLM) @@ -283,7 +284,8 @@ export const dict = { // App-level commands "tui.command.session.list.title": "切換工作階段", "tui.command.session.new.title": "新增工作階段", - "tui.command.workflow.list.title": "工作流程", + "tui.command.workflow.list.title": "工作流程執行清單", + "tui.command.workflow.list.description": "瀏覽並開啟本工作階段中的工作流程執行(不是 /workflow)", "tui.command.model.list.title": "切換模型", "tui.command.model.cycle_recent.title": "循環切換模型", "tui.command.model.cycle_recent_reverse.title": "反向循環切換模型", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 577bdfd9c..949b6f6e6 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -2435,7 +2435,7 @@ function Workflow(props: ToolProps) { // Counters/phase prefer the live metadata streamed by the tool's 250ms flush // loop, falling back to the bus run row. The bus row only learns counters via - // loadWorkflows polling (which only the /workflows dialog runs), so during a run + // loadWorkflows polling (which only the /worklist dialog runs), so during a run // the inline panel would otherwise sit at 0✓ 0✗ 0⟳ — the streamed metadata is // the authoritative live source for this in-conversation view. const counters = createMemo(() => { diff --git a/packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/commands.md b/packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/commands.md index 46860bc13..ab916f539 100644 --- a/packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/commands.md +++ b/packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/commands.md @@ -42,7 +42,7 @@ Most client commands run only when the whole input is the command. `/btw ` | Run deep multi-source research; the prompt-command implementation requires the workflow experiment | +| `/workflow ` | Generate and run a customizable verified multi-agent DAG; requires the workflow experiment | | `/loops [cancel ]` | List or cancel scheduled jobs; requires the cron experiment | ### Skills and other dynamic commands diff --git a/packages/opencode/src/tool/workflow.ts b/packages/opencode/src/tool/workflow.ts index ba15dd244..3bbbc4e72 100644 --- a/packages/opencode/src/tool/workflow.ts +++ b/packages/opencode/src/tool/workflow.ts @@ -88,7 +88,7 @@ export const parameters = z.discriminatedUnion("operation", [ type TranscriptEntry = { kind: "phase" | "log"; text: string } // `counters` and `currentPhase` are streamed in each flush so the inline // conversation panel shows live progress without polling: the bus run row only -// carries counters via loadWorkflows (which only the /workflows dialog polls), so +// carries counters via loadWorkflows (which only the /worklist dialog polls), so // without this the in-conversation panel would sit at 0✓ 0✗ 0⟳ for the whole run. type Metadata = { runID?: string