diff --git a/docs/agents/dag-routing-evolve-summary.md b/docs/agents/dag-routing-evolve-summary.md new file mode 100644 index 0000000000..57a299e46a --- /dev/null +++ b/docs/agents/dag-routing-evolve-summary.md @@ -0,0 +1,94 @@ +# AHE 提示词修订 — DAG Orchestration Router + +## 1. 范围与职责 + +让父对话从实时模板库选择一个主参考和自治档位,再把必要的跨领域保障组合进同一个 YAML DAG。 + +## 2. 组件地图(修改前) + +- 常驻 Router 决定是否使用 DAG 和积木顺序,但不知道配置仓库已经收敛为七个领域及 `full`/`lite`。 +- 配置仓库 README 保存了档位规则,但 README 不在运行时模板发布包内。 +- `guide(topic="patterns")` 仍提供 Deep Review、Large Engineering 等另一套路线名称。 +- `workflow(action="list")` 只显示名称、标题和大小,隐藏了模板的目标。 + +## 3. 评估笔记 + +YAML 字段契约和文件式 authoring 已经明确;缺口在选择信息的所有权和可见性。修订不增加 Schema、积木种类或运行时状态,只删除平行路由语言并让现有模板目标进入选择面。 + +## 4. 失败模式类 + +### P1 — 路由权威分裂 + +- 证据:新模板目录只在配置 README 中,常驻 Router 和 patterns guide 使用两套旧分类。 +- 根因:模板目录更新没有同步到模型真正常驻的工作流指导层。 +- 组件层级:工作流指导。 + +### P2 — 参考库被新建路径绕过 + +- 证据:`/dag-flow` 明示优先 fresh blocks;用户反馈表现为模型猜测 YAML 和路线。 +- 根因:命令入口没有先读取实时模板目录。 +- 组件层级:命令工作流指导。 + +### P3 — 候选信息不足 + +- 证据:`list` 输出缺少 `config.objective`,必须逐个 `read` 才能判断目标。 +- 根因:工具输出契约没有携带模板已经声明的选择证据。 +- 组件层级:工具输出契约。 + +### P4 — lite 子节点越权选路 + +- 证据:七个 lite 模板曾要求第一个子节点自行“升级 full”,但子节点既不拥有路由权,也不能阻止后续写入。 +- 根因:档位前提只写成自然语言提示,没有连接到现有 verdict condition 与父会话 wake。 +- 组件层级:配置模板拓扑、配置 CI 契约。 + +## 5. 变更清单 + +### chg-1 — 一个主参考加一条风险升级规则 + +- 失败证据:P1。 +- 根因:常驻 Router 不认识领域交付物和档位边界。 +- 针对性修复:按最终交付物选择一个主参考;仅在全部低风险条件成立时使用 `lite`,任一高风险信号选择 `full`。 +- 预测影响:领域和档位选择稳定;风险是边界任务升级为 `full`,由模板裁剪抵消成本。 +- 组件层级:工作流指导。 + +### chg-2 — patterns 只处理跨领域冲突 + +- 失败证据:P1。 +- 根因:按需 guide 又定义了一套完整路线。 +- 针对性修复:删除六个旧 playbook;只说明如何在一个主参考中加入最小 secondary assurance。 +- 预测影响:同一目标不再拼接两套完整路线或启动多个 workflow。 +- 组件层级:按需工作流指导。 + +### chg-3 — 库优先并显示 objective + +- 失败证据:P2、P3。 +- 根因:命令偏向从零生成,候选列表缺少目标。 +- 针对性修复:常驻 Router 先 `list`、再 `read`;`/dag-flow` 只委托 Router;列表显示每个模板的 `objective`。 +- 预测影响:已发布参考会先于自由生成被采用;无匹配参考时仍可使用 blocks 逃生口。 +- 组件层级:命令工作流指导、工具输出契约。 + +### chg-4 — lite 前提失效时先阻断 + +- 失败证据:P4。 +- 根因:子节点同时承担取证和重新选路,且没有结构化停止条件。 +- 针对性修复:七个 lite 模板在取证后增加 reporting review gate;所有后续路径均受 gate 支配,非 `ACCEPT` 会唤醒父会话并跳过后续。子节点和按需 guide 只要求 verdict、证据及 required actions;父 Router 在 workflow 完成后决定是否用新节点 ID `extend`。 +- 预测影响:运行中发现迁移、并发、安全或发布边界时不会继续写入,也不会由子节点猜测 full 路线。 +- 组件层级:配置模板拓扑、配置 CI 契约。 + +### chg-5 — Router 单独拥有执行模式与控制选择 + +- 失败证据:按需 policy/interface guides 重复 direct、task、workflow 选择规则,并曾要求 child 输出 `next_action`。 +- 根因:路由规则被放进三个组件,加载按需 guide 会覆盖 resident Router 的较新判断。 +- 针对性修复:policy/interface 只引用 Router 并保留各自的 tier、YAML、checkpoint 与恢复契约;工具字段和示例只接受 `list` 返回的精确名称或 YAML 路径;catalog 拒绝 child 中的 route 名、`next_action` 和具体控制操作。 +- 预测影响:加载任何 guide 不会改变已选择的执行模式;child 只能报告证据和 required actions。 +- 组件层级:resident Router、按需 guides、配置 CI 契约。 + +## 6. 证伪计划 + +- 小型明确功能应选 development-lite;出现跨模块迁移时应翻转为 development-full。 +- 可复现单点缺陷应选 debug-lite;未知根因或并发/持久化边界应翻转为 debug-full。 +- 需要修改代码的安全问题应保持 development/debug 主参考并加入安全保障;只要安全报告时才以 security-audit 为主。 +- 删除 gate→qualification 边、把 writer 放在 gate 前、绕过 gate 支配链或让 child 选择 full route 时,catalog 契约测试必须失败。 +- 删除 `list` 的 objective 输出后,列表契约测试必须失败;删除 Router 的领域或风险规则后,提示词契约测试必须失败。 +- 删除 lite gate 的 parent report、让后续节点绕过 gate,或重新加入子节点选路文本时,配置目录门禁必须失败。 +- 若实测仍跳过模板库,先回滚并重写 chg-3 的入口约束,不在 Router 叠加更多同义规则。 diff --git a/packages/core/src/plugin/command/dag-flow.txt b/packages/core/src/plugin/command/dag-flow.txt index 32c57329ad..595f1f2029 100644 --- a/packages/core/src/plugin/command/dag-flow.txt +++ b/packages/core/src/plugin/command/dag-flow.txt @@ -9,12 +9,10 @@ Otherwise apply the resident Orchestration Router and route the request through one consolidated graph. `/dag-flow` explicitly selects DAG execution; the router still owns any material Decision Checkpoint. -Prefer composable blocks for a fresh flow. Load -`workflow(action="guide", topic="blocks")` only if the block contract is not -already in context. Write one-off work to a task-local YAML file and pass its -`spec_path`; a matching saved workflow name is also a valid `spec_path`. Preserve -the task, user constraints, named roles, read-only limits, acceptance checks, -and confirmed decisions in the objective and block instructions. +Apply the Router's selected reference or fresh-block path. Preserve the task, +user constraints, named roles, read-only limits, acceptance checks, and +confirmed decisions in the objective and block instructions, then pass the +task-local YAML file's `spec_path`. Validate the YAML path, then call the workflow tool with `action=start` in the first response after the route is ready. Printing a plan or YAML does not start it. Never invent diff --git a/packages/core/src/plugin/command/orchestration-domains.md b/packages/core/src/plugin/command/orchestration-domains.md index ca78d4f916..cc3657bbf1 100644 --- a/packages/core/src/plugin/command/orchestration-domains.md +++ b/packages/core/src/plugin/command/orchestration-domains.md @@ -1,140 +1,55 @@ -# Orchestration Domains - -Productized workflow playbooks for recurring heavy-task domains. Each playbook -composes the existing primitives — profiles, review lifecycle, actionable -checkpoints, bounded repair, and the pause-first replan protocol — into a -repeatable graph shape. Resolve every role below as a capability slot per Role -Resolution: prefer a configured agent whose contract matches (an explore-style -scout, a reasoner-style logic prober, a review-style verdict gate, a -verify-style test runner), fall back to `explore`, `build`, or `general`. - -Every playbook is a mix of the two accuracy axes from the Tiered Orchestration -Doctrine — **breadth** (concurrent independent slices, standard tier) and -**depth** (verdict-gated iteration, advanced-tier judge) — at a different -ratio. Each heading names its ratio. Place decomposition, gate, verification, -and arbitration nodes on the advanced tier (`required: true` or a -`review`/`review-*` worker); leave the fan-out volume on the standard tier. - -## The Simulated Audit Loop - -Iteration in a DAG is NOT a cyclic edge and NOT a harness loop. It is a -verdict-driven replan wave — the depth axis in its pure form: - -1. An audit node declares `output_schema` with a normalized `verdict` and - `report_to_parent: true`. -2. On `REJECT` or `REVISE`, the wake delivers findings to the parent. Per the - Verdict Disposal Contract the parent MUST act in that turn: it issues - `control(pause)`, then `control(replan)` appending a correction node and a - NEW audit node under NEW ids (terminal nodes are immutable), wires - `depends_on` forward, then `control(resume)`. If the audit node was the - terminal leaf, `extend` a fresh audit wave instead. -3. Repeat until the audit returns `ACCEPT`. The loop is bounded by - `max_node_replan_attempts` and `max_total_nodes` — on ceiling breach stop - with `BLOCKED` and report the residual findings instead of retrying the - identical plan. - -Every playbook below that says "audit loop" means exactly this mechanism. - -## Playbook: Deep Review - -Ratio: breadth then depth. Multi-role adversarial review of whether a code -structure or design is sound, scaled by the Depth Ladder. - -- **Breadth wave** — fan out 3+ reviewers with genuinely conflicting mandates: - a prosecutor (argues the structure is wrong — coupling, hidden invariants, - failure modes), a defender (argues the current shape is justified — - constraints, history, cost of change), and dimension specialists - (architecture, correctness, testability) as scope demands. Every reviewer - MUST cite file:line evidence and list what it could not confirm as - `unverified_claims`. -- **Verification wave (mandatory for module scope and larger)** — one or more - verify-style nodes check the disputed and `unverified_claims` items against - the actual code before any verdict. This is what separates a review from a - poll of opinions; skipping it lets an unproven assertion become a finding. -- **Arbitration (advanced tier)** — fan in to one arbiter that rules - finding-by-finding on the VERIFIED evidence, not merely concatenating - reviews, and emits the actionable checkpoint shape (`verdict`, `findings`, - `required_actions`, `next_action`). -- Pre-implementation structure reviews are `design` phase. Reviewing an actual - change requires the diff-phase hard contract: - `implementation → verification(PASS) → diff review` with fingerprint echo. -- **Depth wave** — on `REVISE`/`REJECT`, drive corrections and concurrent - deep-dives into the confirmed problem areas through the audit loop. The - arbiter's report is the start of this wave, never the end of the task. - -## Playbook: Deep Speculation - -Ratio: breadth of parallel probes, then depth through the revision loop. -Prophesy a whole design document — stress-test it end to end and emit an -automated verdict with zero human gates in the middle. - -- Internalized grill method, run as graph roles instead of user Q&A: parallel - nodes over the same document — a logic simulator (walk the described system, - surface contradictions and boundary gaps), an adversarial interrogator - (produce the hardest material questions: hidden assumptions, falsifiers, - failure modes, evidence quality), and an alternatives prober (steelman one - competing shape). -- A responder node answers the interrogation strictly from the document plus - codebase evidence, marking each question ANSWERED / GAP / CONTRADICTION. -- An arbiter synthesizes everything into a structured prophecy: verdict, - ranked risks, unresolved gaps, and a concrete revision list — then the audit - loop applies revisions and re-speculates until ACCEPT. -- Fully automated: no admission QA rounds with the user mid-flight. Reserve - interactive `GRILL` admission for before the workflow starts. - -## Playbook: Large Engineering - -Ratio: iterated breadth and depth — parallel packages, each gated, plus a -final audited review. Turn an execution document (todo list, work ledger, or -spec) into audited, parallel-safe delivery. - -1. **Deep analysis** — scout nodes map the affected surface; an analyst node - decomposes the document into work packages with explicit dependency edges - and disjoint write sets (the tickets: each package states its blocking - edges, not a bare list). -2. **Orchestrate** — compile the packages into a graph: independent packages - fan out in parallel, dependent ones serialize, propose-then-assemble where - write sets may overlap. -3. **Audit the plan** — a plan-audit node checks the decomposition itself: - missing edges, false parallelism, unstated assumptions, acceptance criteria - per package. `REJECT` re-orchestrates via the audit loop until the plan - passes. -4. **Execute** — run the audited graph with the develop-profile phases each - package still needs; verification consumes each implementation before any - diff review. -5. **Final adversarial review** — the Deep Review playbook over the assembled - result, with its own audit loop. -6. **Deliverable** — a final assembler emits the outcome report: shipped - packages, evidence, residual risks. - -## Playbook: Solution Bake-off - -Ratio: pure breadth — N samples of the same goal, one advanced-tier judge. -N competing approaches implemented or prototyped in parallel against the same -acceptance criteria; a verify-style node exercises each candidate; one arbiter -picks the winner on evidence and records why the losers lost. - -## Playbook: Root-Cause Diagnosis - -Ratio: breadth of hypotheses first, then depth on the leading survivor. -Fan out one node per plausible hypothesis, each tasked to falsify its own -hypothesis with concrete evidence; an arbiter eliminates, ranks survivors, and -either declares the root cause or replans a deeper probe wave on the leading -survivor. - -## Playbook: Audit Sweeps - -Ratio: pure breadth per sweep cell, with the audit loop supplying depth on -hits. The same fan-out/arbiter/audit-loop shape covers recurring sweep -domains: security surface audit (per-surface reviewers: input handling, -authz, secrets, dependencies), regression matrix fan-out (one verify node per -axis cell), and docs-code drift audit (per-document checkers comparing claims -against the code, with fix waves through the audit loop). - -## Choosing and Combining - -Playbooks compose inside one live DAG: Large Engineering embeds Deep Review at -its gate; Deep Speculation can front-load any of them. Selection still obeys -Execution Mode Selection and the Depth Ladder — its wave count meets the -ladder's minimum for the target size, and explicit user constraints always -override the playbook shape. +# Cross-domain Workflow Composition + +The resident Orchestration Router and the live workflow library own route and +`full`/`lite` selection. This guide resolves only requests where several domain +references appear relevant. Keep one primary reference and one workflow. + +## Pick the backbone by the final artifact + +- The requested deliverable is a product decision: keep product planning as + the backbone; technical feasibility is evidence, not a second design route. +- The requested deliverable is an implementation-ready design: keep technical + design as the backbone; product context supplies constraints. +- The requested deliverable is changed code: keep project development as the + backbone unless an unknown defect first requires causal diagnosis. +- The requested deliverable is a defect repair: keep debug and repair as the + backbone; the repair, regression proof, and review stay in that graph. +- The requested deliverable is a verdict: use code review for a pinned + implementation change, security or performance audit for those evidence + domains, and technical design when the object is a proposed system or + migration. + +Security, performance, and review are secondary assurance when the requested +artifact remains implementation or repair. They become primary only when the +requested artifact is their report or verdict. A secondary concern is never a +second workflow for the same objective. + +## Add the smallest assurance slice + +Read the primary reference first. Read a secondary reference only to identify +the minimum evidence lane or gate that changes acceptance. Put those blocks in +one task-local YAML and retarget every copied instruction to the same scope and +acceptance criteria. Do not append a complete second reference. + +- A security-sensitive feature keeps the development backbone and adds scoped + threat, authorization, secret, or supply-chain checks before final review. +- A security defect keeps the debug backbone and adds exploitability and + boundary verification around the causal repair. +- A performance repair keeps the debug or development backbone and adds a + repeatable baseline plus before/after measurement. +- A review of a dependency or release change keeps the review backbone and adds + only the relevant upstream provenance and reachability evidence. + +Reuse one exploration result across consumers. Keep one verification fan-in +for the final implementation fingerprint and one final review or synthesis; +duplicate explore, verify, and verdict blocks are evidence drift, not extra +assurance. Unordered writers still share one workspace, so give them disjoint +write sets or serialize them with real dependencies. + +## Preserve lifecycle contracts + +Composition does not redefine block fields, verdicts, repair, or recovery. +Load `guide(topic="blocks")` for YAML shapes and block semantics, and +`guide(topic="policy")` for admission, verdict disposal, pause-first replan, +and bounded repair. A non-ACCEPT verdict remains actionable in the same wake +turn; do not invent a domain-specific retry loop. diff --git a/packages/core/src/plugin/command/orchestration-policy.md b/packages/core/src/plugin/command/orchestration-policy.md index f0c557f52f..becee83c62 100644 --- a/packages/core/src/plugin/command/orchestration-policy.md +++ b/packages/core/src/plugin/command/orchestration-policy.md @@ -1,7 +1,8 @@ # Orchestration Policy -This file is the operating procedure. Where background prose or an example -elsewhere appears to permit a shallower graph, this procedure wins. +This guide owns tiering, admission, checkpoints, and recovery after the +resident Orchestration Router selects an execution mode. It does not revisit +that selection. ## Tiered Orchestration Doctrine @@ -50,41 +51,17 @@ like "review X" never does. - **Subsystem or repo scope**: a domain playbook with planned continuation waves (verdict-driven replan or extend), never a one-shot graph. -## Execution Mode Selection +## Parent and Child Ownership The parent conversation owns user interaction, requirement and admission decisions, the macro plan, workflow controls, checkpoint interpretation, and the final user-facing synthesis. Once work is classified for delegation, the parent MUST NOT perform executable leaf work itself. -Choose the smallest child execution mode that can safely complete the request: - -1. Use direct execution only for conversation, trivial state inspection, - workflow control, final synthesis, or an explicit user opt-out. -2. Use one `task` subagent for one independent non-trivial leaf assignment - outside a project-level source or test change when no graph-level - coordination is needed. The parent launches it once, consumes its result, - and does not duplicate the leaf work. -3. Use one live `workflow` DAG for project-level source or test changes, even - when only one project file is expected, and whenever one user objective - contains staged dependencies, two or more related workstreams, a quality - gate, unknown-size discovery, adaptive repair, or an explicit multi-role or - multi-model requirement. - -"Smallest" is measured against the Depth Ladder: a mode or graph that cannot -deliver the ladder's hard minimum for the target size is not safe, merely -small. - -Related flows for one user objective belong to one live DAG. Represent them as -nodes and dependency edges; use `extend` or `control(replan)` when discovery or -a verdict adds work. Start another DAG only after a terminal boundary prevents -live adaptation, and carry the prior outputs into the continuation explicitly. - -Explicit user constraints override profile defaults: - -- "single agent", "do not use DAG", and "answer directly" disable implicit workflow selection. -- "Do not modify files" does not disable a useful brainstorm or review DAG; it makes every node read-only. -- Preserve named roles, exact model assignments, scope limits, and prohibited actions in every node prompt. +The resident Router owns direct, `task`, and `workflow` selection, explicit +opt-outs, and consolidation under one workflow ID. This guide only constrains +the selected graph. Preserve read-only scope, named roles, exact model +assignments, scope limits, and prohibited actions in every node prompt. ## Deep Admission QA @@ -307,14 +284,14 @@ Normal leaf workers use `report_to_parent: false`. Gates, arbiters, and final au "verdict": "ACCEPT | REVISE | REJECT | BLOCKED", "summary": "string", "findings": [], - "required_actions": [], - "next_action": { - "operation": "continue | extend | replan | complete | stop", - "targets": [] - } + "required_actions": [] } ``` +The child reports evidence and required actions only. The parent interprets +the verdict and chooses any workflow control action under the Verdict Disposal +Contract. + Do not poll `status` merely to wait. Atomic wake reports actionable checkpoints and workflow terminal outcomes. Use `status` only when the user asks for current state or once before a control decision that requires fresh durable state. ## Bounded Repair diff --git a/packages/core/src/plugin/command/workflow-routing.md b/packages/core/src/plugin/command/workflow-routing.md index e2112b7c6d..ad7c08dd91 100644 --- a/packages/core/src/plugin/command/workflow-routing.md +++ b/packages/core/src/plugin/command/workflow-routing.md @@ -1,17 +1,14 @@ # Orchestration Router -The user-facing parent owns workflow qualification and block composition. A -slash command or external Skill is not required. A DAG child executes its -assigned block directly and never creates a nested workflow. +The parent owns workflow qualification, saved-reference selection, and block +composition. Children execute assigned blocks and never start nested workflows. -Do not discover, load, or apply an external Skill to select the workflow route -or compose its blocks. Installed routing Skills do not override this product -contract and must not change the selected graph or generated block prompts. +Do not discover, load, or apply an external Skill to select the workflow route. ## Execution mode -- Direct execution: conversation, a small read-only lookup, or one or two - isolated utility scripts outside a project-level change. +- Direct execution: conversation, a small read-only lookup, or isolated utility + scripts outside a project-level change. - One `task` child: one independent non-trivial leaf assignment. - One `workflow` DAG: project-level source or test changes, even one project file; cross-module work; repository-backed product or architecture work; or @@ -21,60 +18,73 @@ An explicit request for one agent, direct work, or no DAG selects direct work. Related work for one objective stays under one workflow ID; extend or replan that workflow when evidence adds work. -## Qualify before composing +A read-only request keeps every selected child read-only but does not by itself +change the execution mode. Preserve named roles, exact model assignments, +scope limits, and prohibited actions in every child prompt. -Inspect repository instructions, code, tests, history, and runtime evidence -before asking. Classify what remains as confirmed facts, safe inferences, -runnable uncertainties, user-owned decisions, and executable work. +## Qualify before composing -When a user-owned choice materially changes behavior, scope, acceptance, or an -irreversible boundary, present one **Decision Checkpoint** before executable -blocks start. Its **Workflow Brief** contains the recommended answer and why, -scope, acceptance evidence, assumptions, risks, and only materially different -alternatives. Ask for one combined confirmation. A request that already -contains an equivalent confirmed brief needs no checkpoint. Child nodes never -ask the user to make product or scope decisions. +Inspect repository evidence before asking. Separate confirmed facts, runnable +uncertainties, user-owned decisions, and executable work. + +When a product or architecture decision materially changes behavior, scope, +acceptance, or an irreversible boundary, present one **Decision Checkpoint** +before executable blocks start. Its **Workflow Brief** states the recommendation, +scope, acceptance evidence, assumptions, risks, and materially different +alternatives. Ask for one combined confirmation; skip it when the request +already confirms an equivalent brief. Children never ask product or scope questions. + +## Select one reference + +Unless the user named an exact saved `spec_path`, call `workflow(action="list")` before authoring. Select only a returned name; never guess a route name. Choose +exactly one primary saved reference by the deliverable: + +- product planning — decide what or why to build; +- technical design — produce an implementation-ready system or migration design; +- project development — deliver a confirmed project change; +- debug and repair — reproduce a defect, prove its cause, and repair it; +- code review — return a verdict on a pinned implementation change or diff; +- security audit — return a code, trust-boundary, authorization, or supply-chain verdict; +- performance audit — return a measured resource or scale verdict. + +When the list contains a matching pair, apply these tiers. Use `lite` only when all +are true: goal and acceptance evidence are confirmed, one module and write owner +suffice, work is reversible, and no high-risk boundary is involved. Use `full` +when any are true: requirements or design are uncertain; work crosses modules +or write owners; a public contract, concurrency, persistence, migration, +identity, authorization, upstream executable dependencies, CI/release, or +production behavior is in scope. A single matching custom workflow has no tier +to infer: read and retarget it directly. + +If a lite reporting gate returns non-`ACCEPT`, let that graph finish and use +additive `extend` with new node IDs after reassessing the live library. Do not +pause or replan a completed workflow; the parent owns this control decision. + +The primary reference follows the final artifact, not every concern. For code +or repairs, review, security, and performance are secondary assurance in that +DAG; for a verdict, the matching audit is primary. Do not concatenate two complete references; copy only secondary blocks that change acceptance. ## Compose the smallest justified graph -Use `workflow(action="guide", topic="blocks")` when block fields are not in -context. Choose blocks from evidence, not from a fixed all-phases pipeline: - -- feature: optional evidence → plan/design → coding packages → verify → review; -- bug without a proven cause: debug → coding → verify → review; -- runnable uncertainty: prototype → update the plan; -- product or architecture decision: evidence lanes → plan options → review or - synthesize; -- existing implementation review: scope evidence → verify when required → - review. - -Omit exploration when facts are already sufficient, omit prototype when -inspection resolves the question, and add synthesize only when outputs need -reconciliation. High-level block contracts are self-contained; block -instructions specialize the task and never name external Skills. - -When a saved route matches the topology, read it, retarget its objective and -block instructions, and prune or add justified blocks before starting the -edited YAML file. Start the saved `spec_path` directly only when its target -already matches exactly. Use low-level nodes only for bindings, conditions, -output schemas, or lifecycle metadata blocks cannot express. - -Write the composed or edited graph to YAML and validate that `spec_path` before -start. Fix every diagnostic in the same file and validate again; validation -creates no workflow. A successful start returns the -exact workflow ID. The parent owns the brief, graph, user interaction, -checkpoints, controls, and final report; children own bounded executable work. -End after start and let the workflow wake the parent. Do not poll merely to -wait, and never claim an unstarted graph is running. +Read the selected reference, retarget its objective and instructions, and +remove phases current evidence already covers. Start its saved `spec_path` +directly only when target and acceptance evidence match. If none fits, load +`guide(topic="blocks")` and compose a task-local YAML. Load +`guide(topic="patterns")` only when domains overlap. Use low-level nodes only +for fields blocks cannot express. + +Write the graph to YAML and validate that `spec_path` before start. Fix every +diagnostic in the same file and revalidate; validation creates no workflow. A +successful start returns the exact workflow ID. The parent owns the graph, +controls, and final report; children own bounded work. End after start and let +the workflow wake the parent. Do not poll merely to wait or claim an unstarted +graph is running. ## Progressive guidance -- `guide` without `topic`: compact index. -- `guide(topic="blocks")`: block shape and composition semantics. -- `guide(topic="interface")`: low-level node and tool fields. -- `guide(topic="policy")`: gates, recovery, and bounded repair. -- `guide(topic="patterns")`: larger domain playbooks. +`guide` without a topic is the index. Topics: `blocks` for block shape, +`interface` for low-level fields, `policy` for recovery, and `patterns` for +cross-domain conflicts. Load only the needed topic. -The tool parameter schema owns action fields and requires `spec_path`; the -on-demand block/interface guides own author-written YAML fields, and validation -is the final authority for the file. +The tool parameter schema owns action fields and `spec_path`; on-demand guides +own YAML fields; validation is the file authority. diff --git a/packages/core/src/plugin/command/workflow.md b/packages/core/src/plugin/command/workflow.md index 4429b37c7d..6afcc414b6 100644 --- a/packages/core/src/plugin/command/workflow.md +++ b/packages/core/src/plugin/command/workflow.md @@ -5,29 +5,13 @@ # Workflow Orchestration -The `workflow` tool orchestrates heavy tasks as dependency-graph multi-agent workflows. Each node runs as a real child session with its own agent and tools. This skill covers when to start a workflow, how to structure it, and how to adapt it at runtime. +The `workflow` tool orchestrates dependency-graph multi-agent workflows. Each +node runs as a real child session with its own agent and tools. The resident +Orchestration Router owns execution-mode and saved-reference selection; this +guide owns the YAML/tool interface after a workflow has been selected. Compile every graph under the Tiered Orchestration Doctrine and Depth Ladder in the orchestration policy below: advanced-tier judgment nodes conduct and check, standard-tier nodes carry the volume, and accuracy is bought with breadth (concurrent fan-out) and depth (verdict-gated waves) rather than with a single trusted pass. -## When to start a workflow - -Use one live workflow for project-level source or test changes, even when only -one project file is expected, and when a user objective has any of these -structural signals: - -- **Staged**: clear phase boundaries where later phases depend on earlier outputs (explore → plan → implement → verify). -- **Parallelizable**: ≥2 related sub-units can execute concurrently (same fix across 5 packages). -- **Quality gate**: intermediate output must pass review before downstream work begins (architecture review before implementation). -- **Adaptive scope**: discovery may reveal an unknown number of work packages or require a bounded repair wave. - -Use one `task` subagent for one independent non-trivial leaf assignment outside -a project-level source or test change. Keep related staged, parallel, gated, or -adaptive flows under one workflow ID; use `extend` or `control(replan)` instead -of starting disconnected DAGs. An explicit `/dag-flow` request always selects -a workflow. Explicit “single agent”, “do not use DAG”, and direct-execution -requests opt out. Direct tools in the parent are reserved for conversation, -trivial state inspection, workflow control, and final synthesis. - ## Standard and deep workflow entry Omitting the top-level start parameter `mode` preserves `standard` behavior. Use `deep` for explicit @@ -85,20 +69,15 @@ A `spec_path` with no path separator and no `.yaml`/`.yml` extension is a 1. `.opencode/workflows/.yaml` — project scope, committed with the repo 2. `/workflows/.yaml` — global scope, available in every project +3. bundled builtin templates shipped with the runtime -The project scope wins when both hold the name. `workflow(action: "list")` -reports the saved names with their scope, title, and node count; a name that -resolves nowhere fails with the directories that were searched. - -Prefer a saved workflow when the user names a recurring procedure ("run the -code review workflow") and the saved target/inputs already match: starting it -is one call, and its graph has already been reviewed. When only its topology -matches, call `{ action: "read", spec_path: "code-review" }`, retarget its objective and block instructions to the current task, prune or add lanes, then -write the edited value to a task-local YAML file and start its `spec_path`. -`read` never starts a workflow. Compose a fresh task-local YAML file when the -task is one-off or no reference fits. To turn a working one-off spec into a -saved workflow, move it into one of the two workflow-library directories under -a descriptive name. +Project shadows global, and both shadow builtin. Call `workflow(action: +"list")` and use only an exact returned name; never infer one. To inspect a +saved graph without starting it, call +`{ action: "read", spec_path: "" }`. Retarget its +objective and block instructions in the parent, then write the edited value to +a task-local YAML file. `read` never starts a workflow. A name that resolves +nowhere fails with the searched locations. ## Orchestration Lifecycle @@ -288,7 +267,7 @@ config: report_to_parent: true output_schema: type: object - required: [verdict, summary, findings, required_actions, next_action] + required: [verdict, summary, findings, required_actions] properties: verdict: type: string @@ -296,16 +275,8 @@ config: summary: { type: string } findings: { type: array } required_actions: { type: array } - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: { type: array } prompt_template: - inline: "Three reviewers produced findings. Submit one structured ACCEPT, REVISE, REJECT, or BLOCKED decision with deduplicated findings, required actions, and the next bounded workflow action." + inline: "Three reviewers produced findings. Submit one structured ACCEPT, REVISE, REJECT, or BLOCKED decision with deduplicated findings and required actions. The parent chooses any workflow control action." - id: deep-dive name: deep-dive @@ -410,12 +381,12 @@ appears as `failed` with error_reason `cancelled via replan` and NO `error_class` — deliberate action, no triage needed. Triage on the class before acting: -| error_class | What it means | Correct response | -|---|---|---| -| `timeout` | The node exceeded `timeout_ms`; the runtime cancelled its child session at the deadline. Environmental — the task is NOT wrong. | Replace and rerun ONLY that node with a larger `worker_config.timeout_ms`. Check its `child_session_id` for partial artifacts before rerunning. | -| `exec_failed` | Runtime/session-level failure. Gate on `error_reason`: (a) unknown/wrong model, auth, rate-limit, connection, template-resolution or condition-expression errors → config/prompt errors; (b) recovery reasons ("no child session on recovery", "child session failed (recovered)") → crash ownership loss; (c) workflow-collateral reasons (`required node(s) failed: ...`, `unresolved review outcome(s): ...`, `orchestrator_unresponsive`) → the node itself was fine; it was failed because the workflow failed. | (a) Fix the config first (`dag.jsonc` tier, provider credentials, model id, template/input mapping), then replace and rerun ONLY that node. (b) Inspect the child session's artifacts, then replace and rerun. (c) Do not rerun these collateral nodes. The wake surfaces no workflow-level reason — triage from the Failed-nodes block: `required node(s) failed: ` names the culprit nodes directly (repair them); `orchestrator_unresponsive` carries NO attribution (see the recipe below). | -| `verdict_fail` | Two shapes. Ran-but-broke-contract: missing `submit_result`, schema rejection, review fingerprint mismatch. Never-ran: pre-spawn contract failures (unresolved template placeholders, review input contract). | Ran-but-broke-contract → rerun the node with the contract stated explicitly; keep the topology. Never-ran → fix the template, input_mapping, or dependency wiring first, then rerun; prompt emphasis alone does not fix broken interpolation. | -| (cascade — see below) | Dependents of a failed node. No dedicated class. | Repair the ROOT node first, then restore the dependent subtree. | +| error_class | What it means | Correct response | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `timeout` | The node exceeded `timeout_ms`; the runtime cancelled its child session at the deadline. Environmental — the task is NOT wrong. | Replace and rerun ONLY that node with a larger `worker_config.timeout_ms`. Check its `child_session_id` for partial artifacts before rerunning. | +| `exec_failed` | Runtime/session-level failure. Gate on `error_reason`: (a) unknown/wrong model, auth, rate-limit, connection, template-resolution or condition-expression errors → config/prompt errors; (b) recovery reasons ("no child session on recovery", "child session failed (recovered)") → crash ownership loss; (c) workflow-collateral reasons (`required node(s) failed: ...`, `unresolved review outcome(s): ...`, `orchestrator_unresponsive`) → the node itself was fine; it was failed because the workflow failed. | (a) Fix the config first (`dag.jsonc` tier, provider credentials, model id, template/input mapping), then replace and rerun ONLY that node. (b) Inspect the child session's artifacts, then replace and rerun. (c) Do not rerun these collateral nodes. The wake surfaces no workflow-level reason — triage from the Failed-nodes block: `required node(s) failed: ` names the culprit nodes directly (repair them); `orchestrator_unresponsive` carries NO attribution (see the recipe below). | +| `verdict_fail` | Two shapes. Ran-but-broke-contract: missing `submit_result`, schema rejection, review fingerprint mismatch. Never-ran: pre-spawn contract failures (unresolved template placeholders, review input contract). | Ran-but-broke-contract → rerun the node with the contract stated explicitly; keep the topology. Never-ran → fix the template, input_mapping, or dependency wiring first, then rerun; prompt emphasis alone does not fix broken interpolation. | +| (cascade — see below) | Dependents of a failed node. No dedicated class. | Repair the ROOT node first, then restore the dependent subtree. | Cascade detection has two shapes: @@ -542,14 +513,15 @@ All nodes share the same workspace. Write conflicts are an orchestration concern **start** — Create a workflow from `config` and optional `title`, `mode`, and admission input stored in YAML. Pass a task-local YAML path for one-off work or -a saved workflow name such as -`{ action: "start", spec_path: "code-review" }`. +a saved workflow name returned by `list`, such as +`{ action: "start", spec_path: "" }`. Returns the workflow ID. Nodes declare `depends_on` (node IDs); layers and execution order are computed automatically. -**list** — Show the saved workflow specs in the library (project and global -scope) with their names, titles, and node counts. This lists reusable specs, -not running workflows; use `status` for a workflow's live state. +**list** — Show saved workflow specs in project, global, and builtin scopes +with names, titles, bounded objectives, block or node counts, paths, and +validation status. This lists reusable specs, not running workflows; use +`status` for a workflow's live state. **read** — Return one saved workflow as structured JSON without starting it. Pass `spec_path`, then retarget generic objectives and block instructions in @@ -583,21 +555,21 @@ omitted content from its preview. ### Node Fields -| Field | Required | Description | -|-------|----------|-------------| -| `id` | yes | Unique node identifier, used in `depends_on` | -| `name` | yes | Human-readable name | -| `worker_type` | yes | Agent type (`explore`, `build`, `general`, `plan`, or custom) | -| `depends_on` | yes | Array of node IDs this node waits for (`[]` for root) | -| `required` | no | If true and this node fails, the workflow terminalizes as failed. Default: false | -| `prompt_template` | yes | `{ id: "..." }` or `{ inline: "...", input: {...} }` | -| `condition` | no | Expression evaluated before spawn; node is skipped if false | -| `input_mapping` | no | Map upstream node outputs into template variables | -| `report_to_parent` | no | If true, the parent agent is woken when this node completes or fails. The workflow's terminal status always wakes the parent regardless of this flag | -| `worker_config` | no | `{ timeout_ms }` — bounds node execution (defaults to 10 minutes if omitted) | -| `output_schema` | no | JSON Schema; when declared, the child agent must call `submit_result` to submit structured output — failure to submit results in node failure | -| `restart` | no | (replan only) Re-spawn this running node with new prompt | -| `cancel` | no | (replan only) Cancel this node | +| Field | Required | Description | +| ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | yes | Unique node identifier, used in `depends_on` | +| `name` | yes | Human-readable name | +| `worker_type` | yes | Agent type (`explore`, `build`, `general`, `plan`, or custom) | +| `depends_on` | yes | Array of node IDs this node waits for (`[]` for root) | +| `required` | no | If true and this node fails, the workflow terminalizes as failed. Default: false | +| `prompt_template` | yes | `{ id: "..." }` or `{ inline: "...", input: {...} }` | +| `condition` | no | Expression evaluated before spawn; node is skipped if false | +| `input_mapping` | no | Map upstream node outputs into template variables | +| `report_to_parent` | no | If true, the parent agent is woken when this node completes or fails. The workflow's terminal status always wakes the parent regardless of this flag | +| `worker_config` | no | `{ timeout_ms }` — bounds node execution (defaults to 10 minutes if omitted) | +| `output_schema` | no | JSON Schema; when declared, the child agent must call `submit_result` to submit structured output — failure to submit results in node failure | +| `restart` | no | (replan only) Re-spawn this running node with new prompt | +| `cancel` | no | (replan only) Cancel this node | ### What NOT to expect diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index 63627074e7..2ce6be2d73 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -63,8 +63,10 @@ describe("CommandPlugin.Plugin", () => { expect(CommandPlugin.WorkflowContent).toContain("Direct execution:") expect(CommandPlugin.WorkflowContent).toContain("One `task` child") expect(CommandPlugin.WorkflowContent).toContain("Related work for one objective") - expect(CommandPlugin.WorkflowFactsContent).toContain("project-level source or test changes") - expect(CommandPlugin.WorkflowFactsContent).toMatch(/even when only\s+one project file/) + expect(CommandPlugin.WorkflowContent).toContain("project-level source or test changes") + expect(CommandPlugin.WorkflowContent).toMatch(/even one project\s+file/) + expect(CommandPlugin.WorkflowFactsContent).toContain("resident\nOrchestration Router owns execution-mode") + expect(CommandPlugin.WorkflowFactsContent).not.toContain("## When to start a workflow") expect(CommandPlugin.WorkflowFactsContent).not.toContain("when ANY") expect(CommandPlugin.WorkflowFactsContent).not.toContain("- **Multi-model**:") expect(CommandPlugin.DagFlowContent).toContain("`action=start`") @@ -73,10 +75,10 @@ describe("CommandPlugin.Plugin", () => { it.effect("keeps always-on guidance small and loads detailed topics progressively", () => Effect.sync(() => { - expect(CommandPlugin.WorkflowContent.length).toBeLessThan(5_000) + expect(Buffer.byteLength(CommandPlugin.WorkflowContent)).toBeLessThan(5_000) expect(CommandPlugin.WorkflowContent).toContain("project-level source or test changes") expect(CommandPlugin.WorkflowContent).toMatch(/even one project\s+file/) - expect(CommandPlugin.WorkflowContent).toContain("isolated utility scripts") + expect(CommandPlugin.WorkflowContent).toMatch(/isolated utility\s+scripts/) expect(CommandPlugin.WorkflowContent).toContain("# Orchestration Router") expect(CommandPlugin.WorkflowContent).toContain("Workflow Brief") expect(CommandPlugin.WorkflowContent).toContain("smallest justified graph") @@ -95,14 +97,37 @@ describe("CommandPlugin.Plugin", () => { }), ) + it.effect("selects one saved reference by outcome and escalates risk to full", () => + Effect.sync(() => { + expect(CommandPlugin.WorkflowContent).toMatch(/call\s+`workflow\(action="list"\)` before authoring/) + expect(CommandPlugin.WorkflowContent).toContain("never guess a route name") + expect(CommandPlugin.WorkflowContent).toContain("exactly one primary saved reference") + ;[ + "product planning", + "technical design", + "project development", + "debug and repair", + "code review", + "security audit", + "performance audit", + ].forEach((domain) => expect(CommandPlugin.WorkflowContent).toContain(domain)) + expect(CommandPlugin.WorkflowContent).toMatch(/Use `lite` only when all/) + expect(CommandPlugin.WorkflowContent).toMatch(/Use\s+`full`\s+when any/) + expect(CommandPlugin.WorkflowContent).toContain("upstream executable dependencies") + expect(CommandPlugin.WorkflowContent).toContain("single matching custom workflow") + expect(CommandPlugin.WorkflowContent).toContain("Do not concatenate two complete references") + expect(CommandPlugin.WorkflowContent).toContain("additive `extend` with new node IDs") + expect(CommandPlugin.WorkflowContent).toContain("Do not\npause or replan a completed workflow") + }), + ) + it.effect("keeps the parent at macro level and consolidates related work", () => Effect.sync(() => { expect(CommandPlugin.OrchestrationPolicyContent).toContain("The parent conversation owns") expect(CommandPlugin.OrchestrationPolicyContent).toContain("MUST NOT perform executable leaf work") - expect(CommandPlugin.OrchestrationPolicyContent).toContain("one `task` subagent") - expect(CommandPlugin.OrchestrationPolicyContent).toContain("one live `workflow` DAG") - expect(CommandPlugin.OrchestrationPolicyContent).toContain("outside a project-level source or test change") - expect(CommandPlugin.OrchestrationPolicyContent).toContain("one user objective") + expect(CommandPlugin.OrchestrationPolicyContent).not.toContain("## Execution Mode Selection") + expect(CommandPlugin.WorkflowContent).toContain("One `task` child") + expect(CommandPlugin.WorkflowContent).toContain("One `workflow` DAG") expect(CommandPlugin.DagFlowContent).toMatch(/one consolidated\s+graph/) }), ) @@ -116,8 +141,10 @@ describe("CommandPlugin.Plugin", () => { // schema (change repair-workflow-authoring-validation). expect(CommandPlugin.WorkflowContent).not.toContain("## Actions") expect(CommandPlugin.WorkflowContent).toContain("parameter schema") - expect(CommandPlugin.WorkflowFactsContent).toContain('{ action: "read", spec_path: "code-review" }') - expect(CommandPlugin.WorkflowFactsContent).toContain("retarget its objective and block instructions") + expect(CommandPlugin.WorkflowFactsContent).toContain('{ action: "read", spec_path: "" }') + expect(CommandPlugin.WorkflowFactsContent).toContain("use only an exact returned name") + expect(CommandPlugin.WorkflowFactsContent).not.toContain('spec_path: "code-review"') + expect(CommandPlugin.WorkflowFactsContent).toMatch(/Retarget its\s+objective and block instructions/) expect(CommandPlugin.WorkflowFactsContent).not.toContain("pass `spec` inline") expect(CommandPlugin.DagFlowContent).toContain("task-local YAML file") expect(CommandPlugin.DagFlowContent).toContain("`spec_path`") @@ -152,10 +179,8 @@ describe("CommandPlugin.Plugin", () => { it.effect("preserves opt-outs read-only scope and explicit role assignments", () => Effect.sync(() => { - expect(CommandPlugin.OrchestrationPolicyContent).toContain("single agent") - expect(CommandPlugin.OrchestrationPolicyContent).toContain("do not use DAG") - expect(CommandPlugin.OrchestrationPolicyContent).toContain("answer directly") - expect(CommandPlugin.OrchestrationPolicyContent).toContain('"Do not modify files"') + expect(CommandPlugin.WorkflowContent).toContain("one agent, direct work, or no DAG") + expect(CommandPlugin.WorkflowContent).toContain("A read-only request") expect(CommandPlugin.OrchestrationPolicyContent).toContain("explicit `@agent` assignment") expect(CommandPlugin.OrchestrationPolicyContent).toContain("MUST NOT invent a `worker_type`") }), @@ -201,7 +226,6 @@ describe("CommandPlugin.Plugin", () => { expect(CommandPlugin.OrchestrationPolicyContent).toContain("## Depth Ladder") expect(CommandPlugin.OrchestrationPolicyContent).toContain("A single wave of parallel opinions is not a") expect(CommandPlugin.WorkflowFactsContent).toContain("Tiered Orchestration Doctrine") - expect(CommandPlugin.OrchestrationDomainsContent).toContain("two accuracy axes") }), ) @@ -211,10 +235,6 @@ describe("CommandPlugin.Plugin", () => { expect(CommandPlugin.OrchestrationPolicyContent).toContain("unverified_claims") expect(CommandPlugin.OrchestrationPolicyContent).toContain("claim-verification wave") expect(CommandPlugin.OrchestrationPolicyContent).toContain("MUST NOT be a silent end of the graph") - expect(CommandPlugin.OrchestrationDomainsContent).toContain( - "**Verification wave (mandatory for module scope and larger)**", - ) - expect(CommandPlugin.OrchestrationDomainsContent).toContain("never the end of the task") }), ) @@ -228,7 +248,6 @@ describe("CommandPlugin.Plugin", () => { expect(CommandPlugin.OrchestrationPolicyContent).toContain("escapes that guard") expect(CommandPlugin.OrchestrationPolicyContent).toContain("Silence is not a stop decision") expect(CommandPlugin.WorkflowFactsContent).toContain("Verdict Disposal Contract") - expect(CommandPlugin.OrchestrationDomainsContent).toContain("Verdict Disposal Contract") }), ) @@ -248,7 +267,8 @@ describe("CommandPlugin.Plugin", () => { Effect.sync(() => { expect(CommandPlugin.OrchestrationPolicyContent).toContain("report_to_parent: false") expect(CommandPlugin.OrchestrationPolicyContent).toContain("report_to_parent: true") - expect(CommandPlugin.OrchestrationPolicyContent).toContain('"next_action"') + expect(CommandPlugin.OrchestrationPolicyContent).not.toContain('"next_action"') + expect(CommandPlugin.OrchestrationPolicyContent).toContain("The child reports evidence and required actions only") expect(CommandPlugin.OrchestrationPolicyContent).toContain("Do not poll") expect(CommandPlugin.OrchestrationPolicyContent).toContain("`extend` or `control(replan)`") expect(CommandPlugin.OrchestrationPolicyContent).toContain("MUST NOT create cyclic `depends_on`") @@ -270,26 +290,22 @@ describe("CommandPlugin.Plugin", () => { }), ) - it.effect("defines productized orchestration domain playbooks", () => + it.effect("keeps cross-domain composition separate from route selection", () => Effect.sync(() => { expect(CommandPlugin.WorkflowContent).not.toContain("# Orchestration Domains") - expect(CommandPlugin.OrchestrationDomainsContent).toContain("## The Simulated Audit Loop") - expect(CommandPlugin.OrchestrationDomainsContent).toContain("NOT a cyclic edge and NOT a harness loop") - expect(CommandPlugin.OrchestrationDomainsContent).toContain("NEW ids (terminal nodes are") - for (const playbook of [ - "## Playbook: Deep Review", - "## Playbook: Deep Speculation", - "## Playbook: Large Engineering", - "## Playbook: Solution Bake-off", - "## Playbook: Root-Cause Diagnosis", - "## Playbook: Audit Sweeps", + expect(CommandPlugin.OrchestrationDomainsContent).toContain("# Cross-domain Workflow Composition") + for (const outcome of [ + "The requested deliverable is a product decision", + "The requested deliverable is an implementation-ready design", + "The requested deliverable is changed code", + "The requested deliverable is a defect repair", + "The requested deliverable is a verdict", ]) { - expect(CommandPlugin.OrchestrationDomainsContent).toContain(playbook) + expect(CommandPlugin.OrchestrationDomainsContent).toContain(outcome) } - expect(CommandPlugin.OrchestrationDomainsContent).toContain("prosecutor") - expect(CommandPlugin.OrchestrationDomainsContent).toContain("zero human gates in the middle") - expect(CommandPlugin.OrchestrationDomainsContent).toContain("max_node_replan_attempts") - expect(CommandPlugin.OrchestrationDomainsContent).toContain("capability slot per Role") + expect(CommandPlugin.OrchestrationDomainsContent).toContain("secondary assurance") + expect(CommandPlugin.OrchestrationDomainsContent).toMatch(/never a\s+second workflow/) + expect(CommandPlugin.OrchestrationDomainsContent).not.toContain("## Playbook:") }), ) @@ -426,14 +442,17 @@ describe("CommandPlugin.Plugin", () => { ) expect(reviewExample).toContain("report_to_parent: true") expect(reviewExample).toContain("output_schema:") - expect(reviewExample).toContain("required: [verdict, summary, findings, required_actions, next_action]") - expect(reviewExample).toContain("required: [operation, targets]") - expect(reviewExample).toContain("enum: [continue, extend, replan, complete, stop]") + expect(reviewExample).toContain("required: [verdict, summary, findings, required_actions]") + expect(reviewExample).not.toContain("next_action") + expect(reviewExample).toContain("The parent chooses any workflow control action") // The arbiter must not be a silent terminal leaf: a conditioned // continuation node keeps non-ACCEPT verdicts from dead-ending the graph. expect(reviewExample).toContain("condition: 'arbitrate.output.verdict != \"ACCEPT\"'") expect(CommandPlugin.WorkflowFactsContent).toContain("an early\n`control(complete)` workflow remains terminal") expect(CommandPlugin.DagFlowContent).toContain("must contain the requested result") + expect(CommandPlugin.WorkflowFactsContent).toContain("project, global, and builtin scopes") + expect(CommandPlugin.WorkflowFactsContent).toContain("bounded objectives") + expect(CommandPlugin.WorkflowFactsContent).toContain("validation status") }), ) }) diff --git a/packages/opencode/src/dag/workflows.ts b/packages/opencode/src/dag/workflows.ts index 27ca8bc9fd..4706e7ac46 100644 --- a/packages/opencode/src/dag/workflows.ts +++ b/packages/opencode/src/dag/workflows.ts @@ -46,6 +46,8 @@ export interface Entry { readonly content?: string /** Workflow title from the spec, when the file declares one. */ readonly title?: string + /** Intended outcome from config.objective, used to select before reading. */ + readonly objective?: string /** Node count, for a one-glance sense of the graph's size. */ readonly nodes?: number /** Block count when the saved spec uses the high-level interface. */ @@ -152,28 +154,43 @@ function scopes(projectDir: string) { } /** Best-effort listing metadata from a file-backed spec. */ -async function describe(file: string): Promise<{ title?: string; nodes?: number; blocks?: number }> { +async function describe( + file: string, +): Promise<{ title?: string; objective?: string; nodes?: number; blocks?: number }> { const text = await Bun.file(file) .text() .catch(() => undefined) return text === undefined ? {} : parseMeta(text) } -/** Parse title/node metadata from spec content (shared with builtin entries). +/** Parse bounded selection metadata from spec content (shared with builtins). * A malformed spec still lists — hiding it would make a typo look like a * missing file; the start path reports the real parse error. */ -async function parseMeta(text: string): Promise<{ title?: string; nodes?: number; blocks?: number }> { +async function parseMeta( + text: string, +): Promise<{ title?: string; objective?: string; nodes?: number; blocks?: number }> { const parsed = await Promise.resolve(text) .then((value) => Bun.YAML.parse(value)) .catch(() => undefined) if (!isRecord(parsed)) return {} const config = isRecord(parsed["config"]) ? parsed["config"] : undefined - const title = typeof parsed["title"] === "string" ? parsed["title"] : undefined + const rawTitle = typeof parsed["title"] === "string" ? parsed["title"] : undefined + const rawObjective = config && typeof config["objective"] === "string" ? config["objective"] : undefined + const title = rawTitle ? preview(rawTitle, 160) : undefined + const objective = rawObjective ? preview(rawObjective, 240) : undefined const nodes = config && Array.isArray(config["nodes"]) ? config["nodes"].length : undefined const blocks = config && Array.isArray(config["blocks"]) ? config["blocks"].length : undefined return { ...(title ? { title } : {}), + ...(objective ? { objective } : {}), ...(nodes === undefined ? {} : { nodes }), ...(blocks === undefined ? {} : { blocks }), } } + +function preview(value: string, limit: number) { + const normalized = value.replace(/\s+/g, " ").trim() + const codepoints = [...normalized] + if (codepoints.length <= limit) return normalized + return `${codepoints.slice(0, limit - 1).join("")}…` +} diff --git a/packages/opencode/src/tool/workflow.ts b/packages/opencode/src/tool/workflow.ts index ad400ebe34..0458421903 100644 --- a/packages/opencode/src/tool/workflow.ts +++ b/packages/opencode/src/tool/workflow.ts @@ -51,7 +51,7 @@ export { Parameters as WorkflowParameters } // ============================================================================ const specPathDescription = - '(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory' + '(start/extend/control replan/read/validate) An exact saved workflow name returned by workflow(action="list"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory' const StartPath = Schema.Struct({ action: Schema.Literal("start").annotate({ description: "Create a workflow" }), @@ -92,7 +92,7 @@ const Result = Schema.Struct({ }) const List = Schema.Struct({ action: Schema.Literal("list").annotate({ - description: "Show saved workflow specs in the library with their validation status", + description: "Show saved workflow names, objectives, sizes, scopes, and validation status", }), }) const Read = Schema.Struct({ @@ -274,7 +274,7 @@ export const WorkflowTool = Tool.define< "- blocks: compose explore/plan/prototype/debug/coding/verify/review/synthesize blocks", "- interface: low-level node fields, bindings, model resolution, and tool actions", "- policy: deep admission, gates, checkpoints, recovery, and bounded repair", - "- patterns: larger review, engineering, diagnosis, and audit topologies", + "- patterns: cross-domain composition and route conflicts", ].join("\n"), metadata: {}, } @@ -312,7 +312,8 @@ export const WorkflowTool = Tool.define< : entry.blocks !== undefined ? ` (${entry.blocks} blocks)` : "", - `\n ${entry.path}`, + entry.objective ? `\n objective: ${entry.objective}` : "", + `\n path: ${entry.path}`, check.valid ? "" : `\n ${check.summary}`, ].join(""), ) diff --git a/packages/opencode/test/command/command.test.ts b/packages/opencode/test/command/command.test.ts index 958ee3129d..0b3ef71855 100644 --- a/packages/opencode/test/command/command.test.ts +++ b/packages/opencode/test/command/command.test.ts @@ -109,7 +109,9 @@ describe("legacy command registry", () => { ) expect(expanded).toContain("resident Orchestration Router") - expect(expanded).toMatch(/Preserve\s+the task, user constraints/) + expect(expanded).not.toContain('workflow(action="list")') + expect(expanded).not.toContain('workflow(action="read"') + expect(expanded).toMatch(/Preserve\s+the task,\s+user constraints/) expect(expanded).toContain("worker types or model IDs") expect(expanded).toContain("configured capability or model") expect(expanded).toContain("real error") diff --git a/packages/opencode/test/dag/blocks.test.ts b/packages/opencode/test/dag/blocks.test.ts index 038ccc4217..108d20074d 100644 --- a/packages/opencode/test/dag/blocks.test.ts +++ b/packages/opencode/test/dag/blocks.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "bun:test" +import { WorkflowRuntime } from "@opencode-ai/core/dag/core/scheduling" import { DagBlocks } from "@/dag/blocks" import { DagConfig } from "@/dag/config" @@ -160,6 +161,59 @@ describe("workflow blocks", () => { expect(nodes.find((node) => node.id === "report")?.condition).toBe('decision.output.verdict == "ACCEPT"') }) + it("keeps every downstream branch behind a reporting scope gate", () => { + const nodes = DagBlocks.compileWorkflowBlocks({ + objective: "Deliver only while the bounded route remains valid", + blocks: [ + { id: "evidence", kind: "explore" }, + { id: "scope-gate", kind: "review", depends_on: ["evidence"], report_to_parent: true }, + { id: "implementation", kind: "coding", depends_on: ["scope-gate"] }, + { id: "verification", kind: "verify", depends_on: ["implementation"] }, + { id: "decision", kind: "review", depends_on: ["verification"] }, + { id: "report", kind: "synthesize", depends_on: ["decision"] }, + ], + }) + + expect(nodes.find((node) => node.id === "scope-gate")).toMatchObject({ + report_to_parent: true, + output_schema: { + properties: { verdict: { enum: ["ACCEPT", "REVISE", "REJECT", "BLOCKED"] } }, + }, + }) + expect(nodes.find((node) => node.id === "implementation")?.condition).toBe('scope-gate.output.verdict == "ACCEPT"') + expect(nodes.find((node) => node.id === "decision--standards")?.condition).toBe( + 'verification.output.verdict == "PASS"', + ) + expect(nodes.find((node) => node.id === "decision--intent")?.condition).toBe( + 'verification.output.verdict == "PASS"', + ) + expect(nodes.find((node) => node.id === "report")?.condition).toBe('decision.output.verdict == "ACCEPT"') + + const runtime = new WorkflowRuntime( + nodes.map((node) => ({ + id: node.id, + dependsOn: node.depends_on, + required: node.required ?? false, + status: "pending" as const, + })), + 8, + ) + ;["evidence", "scope-gate--standards", "scope-gate--intent", "scope-gate"].forEach((id) => + runtime.markSatisfied(id), + ) + runtime.markSkipped("implementation") + expect(runtime.getReadyNodes()).toEqual([]) + expect(runtime.getCascadeSkipNodes()).toEqual(["verification"]) + runtime.markSkipped("verification") + expect(runtime.getCascadeSkipNodes()).toEqual(["decision--intent", "decision--standards"]) + ;["decision--intent", "decision--standards"].forEach((id) => runtime.markSkipped(id)) + expect(runtime.getCascadeSkipNodes()).toEqual(["decision"]) + runtime.markSkipped("decision") + expect(runtime.getCascadeSkipNodes()).toEqual(["report"]) + runtime.markSkipped("report") + expect(runtime.isComplete()).toBe(true) + }) + it("rejects an implementation review without one verification gate", () => { expect(() => DagBlocks.compileWorkflowBlocks({ diff --git a/packages/opencode/test/dag/dag-workflows.test.ts b/packages/opencode/test/dag/dag-workflows.test.ts index 858d057bcd..fc7985a522 100644 --- a/packages/opencode/test/dag/dag-workflows.test.ts +++ b/packages/opencode/test/dag/dag-workflows.test.ts @@ -1,9 +1,12 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test" -import { Effect } from "effect" +import { Effect, Layer } from "effect" import { DagWorkflows } from "@/dag/workflows" import * as os from "node:os" import * as path from "node:path" import * as fs from "node:fs/promises" +import { testEffect } from "../lib/effect" + +const itEffect = testEffect(Layer.empty) let dir: string let projectDir: string @@ -20,9 +23,9 @@ const spec = (name: string, nodes: number) => ].join("\n") const writeProject = (file: string, content: string) => - fs.mkdir(path.join(projectDir, ".opencode", "workflows"), { recursive: true }).then(() => - fs.writeFile(path.join(projectDir, ".opencode", "workflows", file), content), - ) + fs + .mkdir(path.join(projectDir, ".opencode", "workflows"), { recursive: true }) + .then(() => fs.writeFile(path.join(projectDir, ".opencode", "workflows", file), content)) const writeGlobal = (file: string, content: string) => fs @@ -78,6 +81,32 @@ describe("DagWorkflows.resolve", () => { expect(entry?.nodes).toBe(3) }) + itEffect.live("normalizes and bounds the objective used for route selection", () => + Effect.gen(function* () { + yield* Effect.promise(() => + writeProject( + "bounded-objective.yaml", + [ + "title: bounded objective", + "config:", + " name: bounded-objective", + " objective: >-", + ` ${"evidence ".repeat(40)}`, + " final outcome", + " nodes: []", + ].join("\n"), + ), + ) + + const entry = yield* DagWorkflows.resolve("bounded-objective", projectDir) + + expect(entry?.objective).not.toContain("\n") + expect(entry?.objective).not.toContain(" ") + expect(entry?.objective?.length).toBe(240) + expect(entry?.objective).toEndWith("…") + }), + ) + it("resolves a global workflow when the project has none", async () => { await writeGlobal("research.yaml", spec("research", 1)) const entry = await Effect.runPromise(DagWorkflows.resolve("research", projectDir)) @@ -160,4 +189,3 @@ describe("DagWorkflows.list", () => { expect(entries[0]?.nodes).toBe(0) }) }) - diff --git a/packages/opencode/test/dag/workflow-tool.test.ts b/packages/opencode/test/dag/workflow-tool.test.ts index 68f144bdbb..9f201264cc 100644 --- a/packages/opencode/test/dag/workflow-tool.test.ts +++ b/packages/opencode/test/dag/workflow-tool.test.ts @@ -2105,6 +2105,7 @@ describe("workflow tool saved workflows", () => { expect(result.output).toContain("shared [project] — project-shared title") expect(result.output).toContain("global-only [global] — global-only title") expect(result.output).toContain("block-flow [global] — block flow title (1 blocks)") + expect(result.output).toContain("objective: Review a bounded change") expect(result.output).not.toContain("global-shared") }), ), diff --git a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap index 3dffba21cf..51378467cf 100644 --- a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap +++ b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap @@ -458,7 +458,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) An exact saved workflow name returned by workflow(action="list"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, }, @@ -478,7 +478,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) An exact saved workflow name returned by workflow(action="list"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, "workflow_id": { @@ -511,7 +511,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) An exact saved workflow name returned by workflow(action="list"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, "workflow_id": { @@ -621,7 +621,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` { "properties": { "action": { - "description": "Show saved workflow specs in the library with their validation status", + "description": "Show saved workflow names, objectives, sizes, scopes, and validation status", "enum": [ "list", ], @@ -643,7 +643,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) An exact saved workflow name returned by workflow(action="list"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, }, @@ -696,7 +696,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) An exact saved workflow name returned by workflow(action="list"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, }, diff --git a/packages/opencode/test/tool/fixtures/workflow-parameters-post-change.json b/packages/opencode/test/tool/fixtures/workflow-parameters-post-change.json index eb7276f030..e16ebbcbf4 100644 --- a/packages/opencode/test/tool/fixtures/workflow-parameters-post-change.json +++ b/packages/opencode/test/tool/fixtures/workflow-parameters-post-change.json @@ -1,25 +1,25 @@ { "captured_from": "packages/opencode/src/tool/workflow.ts (file-backed discriminated-union Parameters)", - "schema_bytes": 4681, + "schema_bytes": 4712, "branch_count": 10, "session_id_exposed": false, "project_id_exposed": false, "inline_spec_exposed": false, "transformed": { "openai": { - "bytes": 4511, + "bytes": 4542, "branch_count": 10, "start_spec_path_present": true, "inline_spec_exposed": false }, "azure": { - "bytes": 4511, + "bytes": 4542, "branch_count": 10, "start_spec_path_present": true, "inline_spec_exposed": false }, "gemini": { - "bytes": 4681, + "bytes": 4712, "branch_count": 10, "start_spec_path_present": true, "inline_spec_exposed": false