diff --git a/packages/core/src/plugin/command.ts b/packages/core/src/plugin/command.ts index e67e2bce6d..2636a82076 100644 --- a/packages/core/src/plugin/command.ts +++ b/packages/core/src/plugin/command.ts @@ -9,6 +9,8 @@ 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" } import workflowContent from "./command/workflow.md" with { type: "text" } @@ -17,12 +19,18 @@ import orchestrationDomains from "./command/orchestration-domains.md" with { typ 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" 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 DagInitContent = DAG_INIT_PROMPT +export const DagAutoContent = DAG_AUTO_PROMPT export const Plugin = define({ id: "command", @@ -46,6 +54,14 @@ export const Plugin = define({ 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 new file mode 100644 index 0000000000..31c9ab540f --- /dev/null +++ b/packages/core/src/plugin/command/dag-auto.txt @@ -0,0 +1,118 @@ +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. + +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: + +- **Broad completion request** ("把这件事做完", an end-to-end feature/fix, + a goal stated without a stage) → start the **ultra-flow**: use the + `ultra-flow-route` template, retarget every stage to the real task, remove + `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", + "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. + +## Phase 2 — Driving the ultra-flow + +**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. + +**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`. + +**Product decision checkpoint.** Force a user decision when the flow hits: +a new external dependency, a breaking change, a public API change, or an +irreversible data migration — plus your own classification of anything that +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 + +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. + +## 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. diff --git a/packages/core/src/plugin/command/dag-init.txt b/packages/core/src/plugin/command/dag-init.txt new file mode 100644 index 0000000000..0055b49a6d --- /dev/null +++ b/packages/core/src/plugin/command/dag-init.txt @@ -0,0 +1,94 @@ +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 → probe whether it is a self-hosted GitLab: + `curl -sSf https:///api/v4/version` (or `glab api version` against + that host once authed). Responds as GitLab → platform `gitlab` + (self-hosted), CLI `glab` pinned to that host. Otherwise → STOP: + "unsupported platform: only GitHub and GitLab (self-hosted included) + are supported." Bitbucket/Gitea/other remotes are rejected here. + - 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/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index 253aac387e..eb33b78dfe 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -54,6 +54,26 @@ describe("CommandPlugin.Plugin", () => { 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(".opencode/dag-init.json") + expect(CommandPlugin.DagInitContent).toContain("merge_policy") + expect(yield* command.get("dag-auto")).toMatchObject({ + name: "dag-auto", + description: CommandPlugin.DagAutoDescription, + template: CommandPlugin.DagAutoContent, + }) + 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") }), ) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index ffdb528536..446e7a5ec3 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -52,6 +52,8 @@ export const Default = { SUBGOAL: "subgoal", MEMORY: "memory", DAG_FLOW: "dag-flow", + DAG_INIT: "dag-init", + DAG_AUTO: "dag-auto", IMPORT_HOOKS: "import-claude-hooks", CREATE_HOOK: "create-hook", } as const @@ -122,6 +124,20 @@ export const layer = Layer.effect( template: CommandPlugin.DagFlowContent, hints: hints(CommandPlugin.DagFlowContent), } + 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, + source: "command", + template: CommandPlugin.DagAutoContent, + hints: hints(CommandPlugin.DagAutoContent), + } commands[Default.IMPORT_HOOKS] = { name: Default.IMPORT_HOOKS, description: "Import hooks from Claude Code config to OpenCode hooks.json", diff --git a/packages/opencode/test/command/command.test.ts b/packages/opencode/test/command/command.test.ts index 0b3ef71855..1fdc536faf 100644 --- a/packages/opencode/test/command/command.test.ts +++ b/packages/opencode/test/command/command.test.ts @@ -72,6 +72,27 @@ describe("legacy command registry", () => { }), ) + it.instance("registers the canonical dag-init and dag-auto commands", () => + 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