Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/core/src/plugin/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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",
Expand All @@ -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
})
})
}),
})
118 changes: 118 additions & 0 deletions packages/core/src/plugin/command/dag-auto.txt
Original file line number Diff line number Diff line change
@@ -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/<task>.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 <n> --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.
94 changes: 94 additions & 0 deletions packages/core/src/plugin/command/dag-init.txt
Original file line number Diff line number Diff line change
@@ -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://<host>/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 `<config dir>/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": "<ISO 8601 date>"
}
```

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.
20 changes: 20 additions & 0 deletions packages/core/test/plugin/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}),
)

Expand Down
16 changes: 16 additions & 0 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions packages/opencode/test/command/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading