diff --git a/.specgit.yaml b/.specgit.yaml index 39ba8f3bab..4d59cdf94e 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -1,10 +1,8 @@ version: 1 -delivery: end-structured-output +delivery: reduce-dag-auto context: kind: branch - branch: fix/386-end-structured-output + branch: refactor/392-reduce-dag-auto issues: - - 386 - - 387 - - 388 -pr: 390 + - 392 +pr: 393 diff --git a/AGENTS.md b/AGENTS.md index a8dd70ee60..1467e4c435 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,7 +215,7 @@ This repository owns the DAG schema, compiler, validator, runtime, and release i ## DAG command family -- Built-in commands ship compiled into the binary: `/dag-flow` (resident orchestration router), `/dag-init` (platform handshake → writes `.opencode/dag-init.json`), `/dag-auto` (six-block ultra-flow driver), `/dag-template-update` (template refresh without git). User command files shadow built-ins by name; register new built-ins through `packages/core/src/plugin/command.ts` + `packages/opencode/src/command/index.ts` (`Default` registry). +- Built-in commands ship compiled into the binary: `/dag-auto` (requirement → workflow routing: classify, match a saved DAG route, retarget, validate, start). Platform delivery (issues, PRs, CI, merge, release) is specgit's job — never part of `/dag-*`. User command files shadow built-ins by name; register new built-ins through `packages/core/src/plugin/command.ts` + `packages/opencode/src/command/index.ts` (`Default` registry). - Templates come from `opencode-dag-config`: 7 domains × `full`/`lite` plus cross-domain routes (`ultra-flow-route`, `release-route`). Precedence: project `.opencode/workflows/` > global config dir > builtin snapshot (the release pipeline compiles the config repo into the binary via `DAG_TEMPLATES_DIR`). - `dag.jsonc` supplies DAG node model tiers: `advanced` for `required: true` and review nodes, `standard` otherwise. Never pin `model` inside saved workflow specs. diff --git a/README.md b/README.md index 028106958e..9fb28054d4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Three terms worth knowing: - Composable blocks (`explore`, `plan`, `prototype`, `debug`, `coding`, `verify`, `review`, `synthesize`) compile into the node graph; low-level node fields remain available for anything blocks cannot express. - `workflow(action="draft")` renders a structured graph through the tool schema into a validated YAML spec — field-name mistakes are rejected by the provider, not discovered at validation time. -- Saved workflow libraries at three scopes (project / global / builtin), startable by name; the `/dag-flow` command picks a curated reference topology and retargets it to the task at hand. +- Saved workflow libraries at three scopes (project / global / builtin), startable by name; the `/dag-auto` command routes a requirement to a curated reference topology and retargets it to the task at hand. - Model tiers in `dag.jsonc` separate decisions from volume: critical nodes on the `advanced` model, fan-out work on `standard`. **Reliability** @@ -67,7 +67,7 @@ Three terms worth knowing: ## Using workflows Nothing has to be configured to try it: ask for work that has stages, parallel -parts, or a review gate in the middle (`/dag-flow `), and the agent +parts, or a review gate in the middle (`/dag-auto `), and the agent designs a graph and runs it. Three things turn that into a repeatable setup of your own. @@ -108,8 +108,8 @@ directory and it gains a **name**: Resolution takes the first match in that order, so a project file shadows a global one with the same name, and both shadow the builtin tier. The global scope is maintained by the [`opencode-dag-config`](https://github.com/LeXwDeX/opencode-dag-config) -repository; the `/dag-template-update` command syncs it (preview of -new/changed/unchanged files, backup before overwrite, QA decision gate). A minimal spec: +repository (sync it with a plain `git clone`/`git pull` into your config +dir). A minimal spec: ```yaml title: Dependency audit diff --git a/README.zh.md b/README.zh.md index babc3017b5..02e40fc1e4 100644 --- a/README.zh.md +++ b/README.zh.md @@ -37,7 +37,7 @@ GraphAgent 是本项目对外的产品名;仓库以 **OpenCode-GraphAgent** - 可组合块(`explore`、`plan`、`prototype`、`debug`、`coding`、`verify`、`review`、`synthesize`)编译成节点图;块表达不了的还有低级节点字段兜底。 - `workflow(action="draft")` 通过工具参数传结构化图,harness 渲染并校验出 YAML spec——字段写错在 provider 侧就被拒,不用等到校验才发现。 -- 三级作用域的工作流库(项目 / 全局 / 内嵌),按名字启动;`/dag-flow` 命令挑选合适的参考拓扑并注入当前任务。 +- 三级作用域的工作流库(项目 / 全局 / 内嵌),按名字启动;`/dag-auto` 命令把需求路由到合适的参考拓扑并注入当前任务。 - `dag.jsonc` 的模型分层把决策和跑量分开:关键节点用 `advanced` 模型,扇出跑量用 `standard`。 **可靠性** @@ -60,7 +60,7 @@ GraphAgent 是本项目对外的产品名;仓库以 **OpenCode-GraphAgent** ## 工作流怎么用 -不配置也能直接试:给它一件有阶段、有可并行部分、或者中间需要一道审查门禁的活(`/dag-flow <任务>`),智能体自己会建图并跑起来。想把它变成你自己的一套固定流程,有三件事: +不配置也能直接试:给它一件有阶段、有可并行部分、或者中间需要一道审查门禁的活(`/dag-auto <任务>`),智能体自己会建图并跑起来。想把它变成你自己的一套固定流程,有三件事: ### 1. 选定模型分层 —— `.opencode/dag.jsonc` @@ -90,7 +90,7 @@ GraphAgent 是本项目对外的产品名;仓库以 **OpenCode-GraphAgent** | 全局级 | `/workflows/.yaml` | 本机所有项目 | | 内嵌级 | 编译进正式版二进制 | 每个正式版安装——兜底解析层 | -解析按此顺序取第一个命中的名字:项目级遮蔽同名的全局级,二者都遮蔽内嵌级。全局作用域由 [`opencode-dag-config`](https://github.com/LeXwDeX/opencode-dag-config) 仓库维护,`/dag-template-update` 命令负责同步(预览新增/变更/不变清单,覆盖前备份,QA 决策门禁)。一个最小的 spec: +解析按此顺序取第一个命中的名字:项目级遮蔽同名的全局级,二者都遮蔽内嵌级。全局作用域由 [`opencode-dag-config`](https://github.com/LeXwDeX/opencode-dag-config) 仓库维护(直接 `git clone`/`git pull` 到配置目录即可同步)。一个最小的 spec: ```yaml title: Dependency audit diff --git a/packages/core/src/plugin/command.ts b/packages/core/src/plugin/command.ts index 653ed3e895..3d57799ab7 100644 --- a/packages/core/src/plugin/command.ts +++ b/packages/core/src/plugin/command.ts @@ -7,9 +7,6 @@ import { Effect } from "effect" import { Location } from "../location" import PROMPT_INITIALIZE from "./command/initialize.txt" import PROMPT_REVIEW from "./command/review.txt" -import DAG_FLOW_PROMPT from "./command/dag-flow.txt" -import DAG_TEMPLATE_UPDATE_PROMPT from "./command/dag-template-update.txt" -import DAG_INIT_PROMPT from "./command/dag-init.txt" import DAG_AUTO_PROMPT from "./command/dag-auto.txt" import workflowRouting from "./command/workflow-routing.md" with { type: "text" } import workflowBlocks from "./command/workflow-blocks.md" with { type: "text" } @@ -17,20 +14,13 @@ import workflowContent from "./command/workflow.md" with { type: "text" } import orchestrationPolicy from "./command/orchestration-policy.md" with { type: "text" } import orchestrationDomains from "./command/orchestration-domains.md" with { type: "text" } -export const DagFlowDescription = "Start a dependency-graph multi-agent workflow for the supplied task" -export const DagTemplateUpdateDescription = "Update the global DAG reference templates from opencode-dag-config" -export const DagInitDescription = - "Connect this repo to GitHub/GitLab, verify issue/PR permissions, and prepare everything /dag-auto needs" export const DagAutoDescription = - "Finish it: drive the composed ultra-flow (exploration → design → development → acceptance → release → summary) to completion" + "Route a requirement to a composed DAG workflow: classify, match a saved route, retarget, validate, start" export const WorkflowFactsContent = workflowContent export const WorkflowBlocksContent = workflowBlocks export const OrchestrationPolicyContent = orchestrationPolicy export const OrchestrationDomainsContent = orchestrationDomains export const WorkflowContent = workflowRouting -export const DagFlowContent = DAG_FLOW_PROMPT -export const DagTemplateUpdateContent = DAG_TEMPLATE_UPDATE_PROMPT -export const DagInitContent = DAG_INIT_PROMPT export const DagAutoContent = DAG_AUTO_PROMPT export const Plugin = define({ @@ -47,18 +37,6 @@ export const Plugin = define({ command.description = "review changes [commit|branch|pr], defaults to uncommitted" command.subtask = true }) - draft.update("dag-flow", (command) => { - command.template = DagFlowContent - command.description = DagFlowDescription - }) - draft.update("dag-template-update", (command) => { - command.template = DAG_TEMPLATE_UPDATE_PROMPT - command.description = DagTemplateUpdateDescription - }) - draft.update("dag-init", (command) => { - command.template = DagInitContent - command.description = DagInitDescription - }) draft.update("dag-auto", (command) => { command.template = DagAutoContent command.description = DagAutoDescription diff --git a/packages/core/src/plugin/command/dag-auto.txt b/packages/core/src/plugin/command/dag-auto.txt index 31c9ab540f..7ed1df3da4 100644 --- a/packages/core/src/plugin/command/dag-auto.txt +++ b/packages/core/src/plugin/command/dag-auto.txt @@ -1,26 +1,12 @@ -You are running `/dag-auto`. Its essence: the user says "finish this thing" -once, and you drive a composed ultra-flow to completion — all the way from -exploration through design, development, test and acceptance, build and -release, to the summary, without the user ever typing "continue". The flow -is audited and debugged BY DESIGN: direction checkpoints run between every -two blocks, repair goes through bounded replan, and delivery counts only -when CI is green and the ordered merge lands. - -**The stage classifier is a methodology embedded in this command, not a -skill or external router.** Three disciplines: (1) classify the disposition -of the current state before acting, (2) template-first — match a saved DAG -route before inventing a graph, (3) routing decisions stay in this parent -conversation; children receive concrete work. You apply these yourself at -every boundary wake. +You are running `/dag-auto`: the routing and workflow-composition driver of +the `/dag-*` command family. Its essence: classify the request, match a +saved DAG route, retarget it to the real task, validate, and start it. +Routing decisions stay in this parent conversation; children receive +concrete work. That is ALL this command does — it never touches platform +delivery: no issues, no PRs, no CI watching, no merge or release mechanics. Arguments: $ARGUMENTS -## Phase 0 — Gate - -Read `.opencode/dag-init.json`. Missing → STOP and tell the user to run -`/dag-init` first: auto cannot execute without a verified platform -connection, issue/PR permissions, and template availability. - ## Phase 1 — Entry classification Classify the request; never ask the user to pick a route: @@ -31,41 +17,32 @@ Classify the request; never ask the user to pick a route: `stage-release` (+ its checkpoint) when the project has no release mechanics, write the retargeted YAML to `.opencode/.dag-specs/.yaml`, then `workflow(action="validate")` and `workflow(action="start")`. -- **Narrow single-block request** ("review this PR", "grill this plan", +- **Narrow single-block request** ("review this diff", "grill this plan", "decide X", "只做设计阶段") → run ONLY the matching domain route (`product-planning`, `technical-design`, `project-development`, `code-review`, `debug-repair`, `security-audit`, `performance-audit` — full/lite by risk) or the single ultra-flow stage, and end on its report. Template-first for every route: `workflow(action="list")` → `read` → -retarget → start; create from scratch only when nothing fits. +retarget → validate → start; create from scratch only when nothing fits. -## Phase 2 — Driving the ultra-flow +## Phase 2 — Driving the graph **Auto contract.** Drive each stage/checkpoint wake to completion and advance automatically. Do not pause to ask "shall I continue". The ONLY -interruptions allowed are user-owned decisions: the spec confirmation gate, -a product decision checkpoint, the release human gate, a merge gate the -rulesets reserve for a human, or a cap exceeded (retries / replan loops). -Everything else advances on its own. +interruptions allowed are user-owned decisions (the product decision +checkpoint below) or a cap exceeded (retries / replan loops). Everything +else advances on its own. **Checkpoint wakes.** Every checkpoint node carries a verdict `{verdict: continue|replan, findings, target?}` reporting direction correctness: - `continue` → do nothing; the graph already advances by itself. - `replan` → use the findings + target to add correction nodes via - `workflow(action="control", operation="replan")` (never restart the - ultra-flow from scratch). The loop is bounded: at most **3 back-edges**; - on the third, stop and hand the user ONE decision point with the complete - state (findings, attempts, diffs). - -**Spec confirmation gate.** Before the design stage creates the platform -issue, present the spec draft (title + body) in chat and wait for ONE -confirmation; create the issue only after it. Spec content = issue body -(label `dag-spec`), the issue IS the atom — no X.Y task fragmentation. -Multiple specs in one plan → one tracking issue listing the spec issues IN -PLAN ORDER (that list is the merge order contract). Skip the gate only when -the user said `full-auto`. + `workflow(action="control", operation="replan")` (never restart the flow + from scratch). The loop is bounded: at most **3 back-edges**; on the + third, stop and hand the user ONE decision point with the complete state + (findings, attempts, diffs). **Product decision checkpoint.** Force a user decision when the flow hits: a new external dependency, a breaking change, a public API change, or an @@ -74,45 +51,18 @@ materially changes behavior, scope, or acceptance. Present the recommended answer and wait for one combined confirmation; write the result into the retargeted objective/instructions, never into child prompts. -## Phase 3 — Platform delivery rules +## Phase 3 — Completion report -1. **Auto CI/CD + TDD watching.** After push, run `gh pr checks --watch - --fail-fast` as a background task so the session wakes when checks - settle. TDD evidence is double: the development stage ran behavior checks - at public seams locally, remote CI re-runs them. CI failure → scoped - repair pass, re-push; cap 3 auto-fix retries, then hand back with failing - checks and logs. -2. **Ordered merge.** Strictly in plan order: PR-N merges only after PR-(N-1) - is confirmed merged. Before each merge gate, rebase the branch on the - advanced base, push, and re-watch CI. Merge acceptance = CI green + - review verdict, never "checkbox done". Respect rulesets from - `/dag-init`; when merge requires a human actor or approval, stop at the - gate and say exactly what to click. -3. **Release human gate.** The release stage defaults to HOLD: execute the - publish only after the user confirms the release brief (mechanism, - version derived from latest tag + commit types, changelog). Argument - `release-auto` overrides. -4. **Monitoring scope.** Nothing watches spec files or task boxes - mid-process. Watched milestones only: issue-closed, PR-merged, CI checks. -5. **Remote truth.** Single source of truth is the platform: every re-entry - reconciles from `gh issue view` / `gh pr list --state all --json` / - `gh pr checks`. Interrupted runs resume by re-querying — the ultra-flow - survives session restarts by finding its stage from remote state plus the - durable workflow graph. - -## Phase 4 — Completion report - -End with ONE consolidated summary: stages executed (incl. skipped with -reason), checkpoint verdicts and replan passes used, issue number(s), PR -URL(s), CI status, merge position in the plan, release outcome (or -documented skip), and any gate waiting on the human. +End with ONE consolidated summary: the route chosen (saved template name or +task-local graph), stages executed (incl. skipped with reason), checkpoint +verdicts and replan passes used, the exact Workflow ID, and any gate +waiting on the user. ## Rules -- Never create issues/PRs before the `/dag-init` config is verified. -- Never write workflow state to local files; local persistence is only the - retargeted spec YAML under `.opencode/.dag-specs/` and - `.opencode/dag-init.json`. - Routing decisions live in this conversation; child nodes get concrete work, not routing questions. -- Supported platforms: GitHub and GitLab (self-hosted included) only. +- Never write workflow state to local files; local persistence is only the + retargeted spec YAML under `.opencode/.dag-specs/`. +- Platform delivery (issues, PRs, CI, merge, release) is out of scope for + this command; if the user asks for it, name the boundary and stop. diff --git a/packages/core/src/plugin/command/dag-flow.txt b/packages/core/src/plugin/command/dag-flow.txt deleted file mode 100644 index 595f1f2029..0000000000 --- a/packages/core/src/plugin/command/dag-flow.txt +++ /dev/null @@ -1,32 +0,0 @@ -# Start a DAG Workflow - - -$ARGUMENTS - - -If the task is empty or contains only whitespace, ask for it; do not start a workflow. -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. - -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 -worker types or model IDs. If a configured capability or model is unavailable, -report the actual gap and leave the workflow uncreated. - -On success, report the exact Workflow ID and initial state, tell the user they -can run `/dag` for live inspection, and end the response. The workflow wakes -the parent when attention is needed. Do not poll, sleep, or loop to wait. On -failure, state that it did not start and report the real error; do not invent a -replacement run. - -A final synthesis block must contain the requested result rather than a plan or -placeholder. If its wake message says `truncated=true`, the parent reads every -page with `workflow(action="result")` before verification. The parent verifies -that complete artifact, disposes of any non-ACCEPT review verdict, and gives the -user one final report. diff --git a/packages/core/src/plugin/command/dag-init.txt b/packages/core/src/plugin/command/dag-init.txt deleted file mode 100644 index 46e58de173..0000000000 --- a/packages/core/src/plugin/command/dag-init.txt +++ /dev/null @@ -1,99 +0,0 @@ -You are running `/dag-init`: the platform handshake and readiness initializer -for the `/dag-*` command family. `/dag-auto` binds workflow specs to issues, -PRs, and CI — without a configured GitHub or GitLab remote it cannot work. -Your job is to verify that binding is possible AND that the auto pipeline has -everything it needs, then record the connection. - -Arguments (optional): $ARGUMENTS - -## Steps - -1. **Detect the platform from the git remote.** - - Run `git remote get-url origin`. Supported platforms are ONLY GitHub and - GitLab (including self-hosted/private GitLab). - - Not a git repo, or no `origin` → STOP: "`/dag-*` requires a git remote." - - Host is `github.com` → platform `github`, CLI `gh`. - - Host is `gitlab.com` → platform `gitlab`, CLI `glab`. - - Any other host → decide whether it is a self-hosted GitLab. GitLab's - `/api/v4/version` endpoint REQUIRES authentication by API design, so a - healthy instance answers a bare request with 401 — that 401 is POSITIVE - evidence (the endpoint exists and answers), not a failure. Probe order: - `curl -sSf https:///api/v4/version`; a version JSON response OR a - 401/GitLab-shaped error → classify as GitLab; connection refused or a - non-GitLab answer → STOP: "unsupported platform: only GitHub and GitLab - (self-hosted included) are supported." Bitbucket/Gitea/other remotes are - rejected here. Once classified, re-verify with `glab api version` after - step 2 auth passes. Platform `gitlab` (self-hosted), CLI `glab` pinned - to that host. - - When `origin` and a different `upstream` exist and point at different - repositories, ask the user ONCE which remote `/dag-auto` should bind to - and record the choice; otherwise bind `origin`. - -2. **Verify CLI and auth.** - - `gh auth status` (or `glab auth status`) must report the CLI installed and - authenticated against the detected host. If not, stop and tell the user - exactly what to install/login. - -3. **Probe permissions and capabilities.** For GitHub: - - - `gh api repos/{owner}/{repo}` → `default_branch` and `permissions.push`. - Push permission is required (gates branch push and PR creation; issue - creation rides with it). - - `gh issue list --limit 1 --json number` → issue read access. - - `gh api repos/{owner}/{repo}/actions/workflows` → CI presence. Zero - means WARN: the CI/CD + TDD watch step of `/dag-auto` has nothing to - monitor remotely. - - `gh api repos/{owner}/{repo}/rulesets` → record whether branch - protection/rulesets exist. They constrain the auto merge step (required - checks, merge actor); auto must respect them, so knowing is required. - - For GitLab use the `glab` equivalents (`glab repo view`, - `glab issue list --per-page 1`; CI = `.gitlab-ci.yml` present; protection - rules via `glab api projects/:id/protected_branches`). - -4. **Check template availability.** `/dag-auto` dispatches into saved DAG - route templates. Templates ship three ways, in precedence order: project - `.opencode/workflows/`, global `/workflows/`, and the builtin - templates compiled into the release. If `workflow(action="list")` returns - no templates at all, WARN with the install command: - `git clone git@github.com:LeXwDeX/opencode-dag-config.git ~/.config/opencode/workflows` - -5. **Write the connection config — only if every required check passed.** - - Write `.opencode/dag-init.json` in the project root: - - ```json - { - "platform": "github", - "repo": "owner/name", - "remote": "origin", - "base_branch": "main", - "cli": "gh", - "can_push": true, - "has_ci": true, - "has_rulesets": true, - "has_templates": true, - "merge_policy": "ordered", - "checked_at": "" - } - ``` - - If the file already exists, re-run the probes and refresh it (the command - is idempotent). This file is the ONLY local state the `/dag-*` family - keeps — connection config, never workflow state. It is safe to commit. - -6. **Report.** Print a checklist table: platform, auth, issue access, push - permission, CI presence, rulesets, template availability, config path - written. WARN rows do not block writing the config but must name which - `/dag-auto` capability they degrade. If any REQUIRED check failed, print - which one and STOP — do not write the config. - -## Rules - -- Never create throwaway issues/PRs as probes; permission checks are - read-only API calls. -- Never store tokens in the config file — auth lives in `gh`/`glab`. -- Do not proceed past a failed required check; partial handshake state is - worse than none. diff --git a/packages/core/src/plugin/command/dag-template-update.txt b/packages/core/src/plugin/command/dag-template-update.txt deleted file mode 100644 index 87d1fc985d..0000000000 --- a/packages/core/src/plugin/command/dag-template-update.txt +++ /dev/null @@ -1,127 +0,0 @@ -# Update Global DAG Reference Templates - -The user invoked `/dag-template-update` to update the global DAG reference -templates. These templates live in the opencode config directory (the same -trust level as `dag.jsonc`) and are curated by the `opencode-dag-config` -repository — shared across projects as the fallback scope, with project-level -`.opencode/workflows/` overrides taking precedence. - -The update downloads the repository archive (zip) — no git install, no SSH -key, and the target directory does not need to be a git repository. - - -$ARGUMENTS - - -## Determine the config directory - -The global workflow library directory is `/workflows`: - -1. If the `OPENCODE_CONFIG_DIR` environment variable is set, the config - directory is its value. -2. Otherwise resolve the platform config directory for opencode from the - environment: default `~/.config/opencode/` on macOS/Linux, but respect - `XDG_CONFIG_HOME` when set (runtime resolution follows the same order). - -## Download the templates - -Download the archive from the pinned repository URL (fixed — if the user -wants a different source they must say so explicitly): - -``` -https://codeload.github.com/LeXwDeX/opencode-dag-config/zip/refs/heads/main -``` - -Extract it into a temporary directory. The archive contains a top-level folder -(typically `opencode-dag-config-main/`) whose root holds the `*.yaml` and `*.yml` -templates. - -## Dry-run preview (always show before applying) - -Compare the extracted templates against the current -`/workflows/` and classify every template: - -- `NEW` — exists in the archive, not present locally -- `UNCHANGED` — same filename, identical content (skip) -- `UPDATE` — same filename, different content (needs overwrite) -- local-only files (present locally, absent from the archive) are kept as-is - -Show the user the three lists, or report that nothing needs updating. - -## Validate downloaded templates (fail closed, before any replacement) - -Before any copy or overwrite, discover and validate EVERY extracted `*.yaml` and `*.yml` template with -the same validation authority `start` and `list` use — the workflow tool's -`validate` action. For each extracted template call: - -``` -workflow(action: "validate", spec_path: "", profile: "portable") -``` - -- Every template must come back `valid: true`. -- If both `.yaml` and `.yml` exist, abort before applying anything; - one logical workflow name cannot have two source files. -- If ANY template is invalid: keep the current global library exactly as it - is — copy nothing, overwrite nothing. Report a per-file diagnostic list - (code, path, message, hint) for every failing template plus the names that - passed, and stop. Treat validation failure like a download failure: never - partially apply. -- Use the portable profile: the global library doubles as the distributable - builtin source, so a template that only works inside one specific project - does not belong here. - -## Merge - -- If there are no `UPDATE` entries: merge directly — copy `NEW` templates in, - skip `UNCHANGED`, leave local-only files untouched. -- If `UPDATE` entries exist, do not overwrite silently. Ask the user how to - proceed (QA): - - overwrite all updates (backup first) - - skip all updates, only add `NEW` templates - - decide per file -- If the user declines or cannot decide, only add `NEW` templates and report - the skipped updates. -- Before any overwrite, back up the local file next to the original with a - timestamped suffix (e.g. `.yaml.bak-`). If the backup - fails (read-only directory, disk full, permissions), abort the overwrite of - that file and report the error — never overwrite without a backup. - -## Concurrency lock - -Another session may be updating the same directory. Take an exclusive lock on -`/workflows` before downloading or merging: - -- Create the lock with `mkdir /workflows/.dag-update.lock` (mkdir - is atomic — if it fails because the directory exists, another update is in - progress). -- If the lock is held, wait briefly and retry a few times; if it is still held, - report that an update is already running and stop. -- Remove the lock (`rmdir /workflows/.dag-update.lock`) after the - merge finishes, including on failure. - -## Verify - -After applying, confirm the update actually landed by comparing file contents, -not just the workflow library listing: - -- Re-read each updated file from `/workflows/` and compare its - content to the extracted archive copy — they must match. -- Run `workflow(action: "list")` and report the resulting template count plus - the names that changed (added / updated / skipped). Note that a project-level - template with the same name shadows the global one in the listing. -- Report the backup locations when any file was overwritten. - -## Failure handling - -- Download failure (network, 404, rate limit): report the actual error - verbatim and stop — never invent success. -- Extraction failure (corrupt archive): report and stop. -- Validation failure (any template invalid): report per-file diagnostics and - stop; the existing library stays untouched. -- If `/workflows` does not exist, create it before applying. - -## Notes - -- Project-level templates (`.opencode/workflows/`) override global ones with - the same name — a user may see no change for a name the project already - shadows. Mention this when relevant. diff --git a/packages/core/src/plugin/command/workflow.md b/packages/core/src/plugin/command/workflow.md index 6afcc414b6..ddb6ef9555 100644 --- a/packages/core/src/plugin/command/workflow.md +++ b/packages/core/src/plugin/command/workflow.md @@ -1,5 +1,5 @@ @@ -575,4 +575,4 @@ omitted content from its preview. - No `node_complete` action — completion is automatic - No `history` action — inspect a known workflow with `status`; browsing running workflows remains TUI-only (`list` shows saved specs, not running workflows) -- No runtime-side magical topology selection — `/dag-flow` selects and adapts saved reference graphs in the parent agent; the workflow runtime executes the resulting validated spec +- No runtime-side magical topology selection — the routing command (`/dag-auto`) selects and adapts saved reference graphs in the parent agent; the workflow runtime executes the resulting validated spec diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index 2cd3f05bc6..93ef984fd8 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -43,28 +43,6 @@ describe("CommandPlugin.Plugin", () => { description: "review changes [commit|branch|pr], defaults to uncommitted", subtask: true, }) - expect(yield* command.get("dag-flow")).toMatchObject({ - name: "dag-flow", - description: CommandPlugin.DagFlowDescription, - template: CommandPlugin.DagFlowContent, - }) - expect(CommandPlugin.DagFlowContent).toContain("$ARGUMENTS") - expect(CommandPlugin.DagFlowContent).toContain("`action=start`") - expect(CommandPlugin.DagFlowContent).toContain("exact Workflow ID") - expect(CommandPlugin.DagFlowContent).toContain("run `/dag`") - expect(CommandPlugin.DagFlowContent).toContain("resident Orchestration Router") - expect(CommandPlugin.DagFlowContent).toContain("Decision Checkpoint") - expect(yield* command.get("dag-init")).toMatchObject({ - name: "dag-init", - description: CommandPlugin.DagInitDescription, - template: CommandPlugin.DagInitContent, - }) - expect(CommandPlugin.DagInitContent).toContain("$ARGUMENTS") - expect(CommandPlugin.DagInitContent).toContain("unsupported platform: only GitHub and GitLab") - expect(CommandPlugin.DagInitContent).toContain("401 is POSITIVE") - expect(CommandPlugin.DagInitContent).toContain("re-verify with `glab api version`") - expect(CommandPlugin.DagInitContent).toContain(".opencode/dag-init.json") - expect(CommandPlugin.DagInitContent).toContain("merge_policy") expect(yield* command.get("dag-auto")).toMatchObject({ name: "dag-auto", description: CommandPlugin.DagAutoDescription, @@ -73,9 +51,47 @@ describe("CommandPlugin.Plugin", () => { expect(CommandPlugin.DagAutoContent).toContain("$ARGUMENTS") expect(CommandPlugin.DagAutoContent).toContain("ultra-flow-route") expect(CommandPlugin.DagAutoContent).toContain("continue|replan") - expect(CommandPlugin.DagAutoContent).toContain("issue IS the atom") - expect(CommandPlugin.DagAutoContent).toContain("full-auto") - expect(CommandPlugin.DagAutoContent).toContain("Ordered merge") + expect(CommandPlugin.DagAutoContent).toContain("3 back-edges") + expect(CommandPlugin.DagAutoContent).toContain("Product decision checkpoint") + }), + ) + + it.effect("retires the platform-delivery commands", () => + Effect.gen(function* () { + const command = yield* CommandV2.Service + yield* CommandPlugin.Plugin.effect( + host({ + command: { transform: command.transform, reload: command.reload }, + }), + ).pipe( + Effect.provideService( + Location.Service, + Location.Service.of(location({ directory }, { projectDirectory: project })), + ), + ) + + expect(yield* command.get("dag-init")).toBeUndefined() + expect(yield* command.get("dag-flow")).toBeUndefined() + expect(yield* command.get("dag-template-update")).toBeUndefined() + }), + ) + + it.effect("keeps /dag-auto free of platform-delivery vocabulary", () => + Effect.sync(() => { + const content = CommandPlugin.DagAutoContent + expect(content).not.toContain("dag-init") + // The intro/Rules name the boundary in the negative ("no issues, no + // PRs"); actionable delivery mechanics must stay absent. + expect(content).not.toContain("issue number") + expect(content).not.toContain("gh pr") + expect(content).not.toContain("pr checks") + expect(content).not.toContain("Ordered merge") + expect(content).not.toContain("rebase") + expect(content).not.toContain("release brief") + expect(content).toContain("routing and workflow-composition") + expect(content).toContain("workflow(action=\"list\")") + expect(content).toContain("workflow(action=\"validate\")") + expect(content).toContain("workflow(action=\"start\")") }), ) @@ -91,7 +107,7 @@ describe("CommandPlugin.Plugin", () => { 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`") + expect(CommandPlugin.DagAutoContent).toContain('workflow(action="start")') }), ) @@ -157,7 +173,7 @@ describe("CommandPlugin.Plugin", () => { expect(CommandPlugin.OrchestrationPolicyContent).toContain("only the user's delivery standard") expect(CommandPlugin.WorkflowContent).toContain("One `task` child") expect(CommandPlugin.WorkflowContent).toContain("One `workflow` DAG") - expect(CommandPlugin.DagFlowContent).toMatch(/one consolidated\s+graph/) + expect(CommandPlugin.DagAutoContent).toContain("ONE consolidated summary") }), ) @@ -175,8 +191,8 @@ describe("CommandPlugin.Plugin", () => { 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`") + expect(CommandPlugin.DagAutoContent).toContain(".opencode/.dag-specs/") + expect(CommandPlugin.DagAutoContent).toContain("retarget") }), ) @@ -481,7 +497,6 @@ describe("CommandPlugin.Plugin", () => { // 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/script/validate-dag-templates.ts b/packages/opencode/script/validate-dag-templates.ts index 4eec643946..40278b09e4 100644 --- a/packages/opencode/script/validate-dag-templates.ts +++ b/packages/opencode/script/validate-dag-templates.ts @@ -2,8 +2,7 @@ * Directory-level template validator (change repair-workflow-authoring-validation, §4.3). * * Reuses the runtime source-to-graph authority (WorkflowAuthoring) so - * config-repo CI, release packaging, and /dag-template-update all enforce the - * same portable contract. Emits machine-readable diagnostics plus the runtime, + * config-repo CI and release packaging enforce the same portable contract. Emits machine-readable diagnostics plus the runtime, * template, and compatibility commit identifiers, and exits non-zero when any * template is invalid. * diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index fea6d00978..4e4b50e02a 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -51,9 +51,6 @@ export const Default = { GOAL: "goal", SUBGOAL: "subgoal", MEMORY: "memory", - DAG_FLOW: "dag-flow", - DAG_TEMPLATE_UPDATE: "dag-template-update", - DAG_INIT: "dag-init", DAG_AUTO: "dag-auto", IMPORT_HOOKS: "import-claude-hooks", CREATE_HOOK: "create-hook", @@ -118,27 +115,6 @@ export const layer = Layer.effect( template: "", hints: ["$ARGUMENTS"], } - commands[Default.DAG_FLOW] = { - name: Default.DAG_FLOW, - description: CommandPlugin.DagFlowDescription, - source: "command", - template: CommandPlugin.DagFlowContent, - hints: hints(CommandPlugin.DagFlowContent), - } - commands[Default.DAG_TEMPLATE_UPDATE] = { - name: Default.DAG_TEMPLATE_UPDATE, - description: CommandPlugin.DagTemplateUpdateDescription, - source: "command", - template: CommandPlugin.DagTemplateUpdateContent, - hints: hints(CommandPlugin.DagTemplateUpdateContent), - } - commands[Default.DAG_INIT] = { - name: Default.DAG_INIT, - description: CommandPlugin.DagInitDescription, - source: "command", - template: CommandPlugin.DagInitContent, - hints: hints(CommandPlugin.DagInitContent), - } commands[Default.DAG_AUTO] = { name: Default.DAG_AUTO, description: CommandPlugin.DagAutoDescription, diff --git a/packages/opencode/test/command/command.test.ts b/packages/opencode/test/command/command.test.ts index a1a7c182b4..8b6a0ae3b0 100644 --- a/packages/opencode/test/command/command.test.ts +++ b/packages/opencode/test/command/command.test.ts @@ -35,8 +35,8 @@ function commandLayer(commands: Record { }), ) - it.instance("registers the canonical dag-flow command without a built-in workflow fallback", () => + it.instance("registers the canonical dag-auto command", () => Effect.gen(function* () { const commands = yield* Command.Service - const command = yield* commands.get("dag-flow") + const command = yield* commands.get("dag-auto") expect(command).toMatchObject({ - name: "dag-flow", - description: CommandPlugin.DagFlowDescription, + name: "dag-auto", + description: CommandPlugin.DagAutoDescription, source: "command", - template: CommandPlugin.DagFlowContent, + template: CommandPlugin.DagAutoContent, hints: ["$ARGUMENTS"], }) expect(yield* commands.get("workflow")).toBeUndefined() }), ) - it.instance("registers the canonical dag-template-update command", () => + it.instance("retires the platform-delivery commands", () => Effect.gen(function* () { const commands = yield* Command.Service - expect(yield* commands.get("dag-template-update")).toMatchObject({ - name: "dag-template-update", - description: CommandPlugin.DagTemplateUpdateDescription, - source: "command", - template: CommandPlugin.DagTemplateUpdateContent, - hints: ["$ARGUMENTS"], - }) + expect(yield* commands.get("dag-init")).toBeUndefined() + expect(yield* commands.get("dag-flow")).toBeUndefined() + expect(yield* commands.get("dag-template-update")).toBeUndefined() }), ) - it.instance("registers the canonical dag-init and dag-auto commands", () => + overridden.instance("allows configured dag-auto commands to override the built-in", () => Effect.gen(function* () { const commands = yield* Command.Service - - expect(yield* commands.get("dag-init")).toMatchObject({ - name: "dag-init", - description: CommandPlugin.DagInitDescription, - source: "command", - template: CommandPlugin.DagInitContent, - hints: ["$ARGUMENTS"], - }) expect(yield* commands.get("dag-auto")).toMatchObject({ - name: "dag-auto", - description: CommandPlugin.DagAutoDescription, - source: "command", - template: CommandPlugin.DagAutoContent, - hints: ["$ARGUMENTS"], - }) - }), - ) - - overridden.instance("allows configured dag-flow commands to override the built-in", () => - Effect.gen(function* () { - const commands = yield* Command.Service - expect(yield* commands.get("dag-flow")).toMatchObject({ - description: "Custom DAG flow", + description: "Custom DAG auto", template: "Custom task:\n$ARGUMENTS", }) }), @@ -120,47 +95,35 @@ describe("legacy command registry", () => { it.effect("preserves complete multi-line command arguments", () => Effect.sync(() => { const input = "Investigate auth\nThen run the focused tests" - const expanded = SessionPrompt.expandCommandTemplate(CommandPlugin.DagFlowContent, input) + const expanded = SessionPrompt.expandCommandTemplate(CommandPlugin.DagAutoContent, input) - expect(expanded).toContain(`\n${input}\n`) + expect(expanded).toContain(`Arguments: ${input}`) expect(expanded).not.toContain("$ARGUMENTS") }), ) - it.effect("returns after starting a DAG instead of polling its status", () => - Effect.sync(() => { - const expanded = SessionPrompt.expandCommandTemplate(CommandPlugin.DagFlowContent, "Run two parallel workers") - - expect(expanded).toContain("Do not poll") - expect(expanded).toContain("and end the response") - }), - ) - - it.effect("requires router-driven compilation without dropping task constraints", () => + it.effect("routes template-first and never mentions platform delivery", () => Effect.sync(() => { const expanded = SessionPrompt.expandCommandTemplate( - CommandPlugin.DagFlowContent, + CommandPlugin.DagAutoContent, "Use @security-reviewer to review this project. Do not modify files.", ) - expect(expanded).toContain("resident Orchestration Router") - 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") - expect(expanded).toContain("final synthesis block must contain the requested result") + expect(expanded).toContain("workflow(action=\"list\")") + expect(expanded).toContain("never ask the user to pick a route") + expect(expanded).toContain("ultra-flow-route") + expect(expanded).not.toContain("dag-init") + expect(expanded).not.toContain("`gh ") + expect(expanded).not.toContain("Ordered merge") }), ) - it.effect("keeps the blank-task guard when dag-flow has no arguments", () => + it.effect("keeps the blank-arguments form valid", () => Effect.sync(() => { - const expanded = SessionPrompt.expandCommandTemplate(CommandPlugin.DagFlowContent, " ") + const expanded = SessionPrompt.expandCommandTemplate(CommandPlugin.DagAutoContent, "") - expect(expanded).toContain("\n \n") - expect(expanded).toContain("empty or contains only whitespace") - expect(expanded).toContain("do not start a workflow") + expect(expanded).toContain("Arguments:") + expect(expanded).not.toContain("$ARGUMENTS") }), ) }) diff --git a/packages/tui/src/feature-plugins/home/tips-view.tsx b/packages/tui/src/feature-plugins/home/tips-view.tsx index 27f28c0b2a..7e2f324418 100644 --- a/packages/tui/src/feature-plugins/home/tips-view.tsx +++ b/packages/tui/src/feature-plugins/home/tips-view.tsx @@ -186,7 +186,7 @@ const TIPS: Tip[] = [ (shortcuts) => press(shortcuts.messagesCopy(), "to copy the assistant's last message to clipboard"), (shortcuts) => press(shortcuts.commandList(), "to see all available actions and commands"), "Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers", - "Run {highlight}/dag-flow {/highlight} to start a DAG workflow, then {highlight}/dag{/highlight} to inspect it", + "Run {highlight}/dag-auto {/highlight} to start a DAG workflow, then {highlight}/dag{/highlight} to inspect it", (shortcuts) => `The leader key is ${shortcutText(shortcuts.leader())}; combine with other keys for quick actions`, (shortcuts) => press(shortcuts.modelCycleRecent(), "to quickly switch between recently used models"), (shortcuts) => press(shortcuts.sessionSidebarToggle(), "in a session to show or hide the sidebar panel"), diff --git a/packages/tui/src/feature-plugins/system/dag-inspector.tsx b/packages/tui/src/feature-plugins/system/dag-inspector.tsx index 6a672515ea..dcf2b98da1 100644 --- a/packages/tui/src/feature-plugins/system/dag-inspector.tsx +++ b/packages/tui/src/feature-plugins/system/dag-inspector.tsx @@ -519,7 +519,7 @@ function DagInspector(props: { api: TuiPluginApi }) { No workflows for this session - {"Run /dag-flow inside a session to start an orchestration"} + {"Run /dag-auto inside a session to start an orchestration"}