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
5 changes: 5 additions & 0 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ on:
- main
- dev
pull_request:
# ready_for_review matters: the SpecGit Acceptance verdict requires
# required-check runs that started at/after the draft→ready transition
# (#315 anchor). Without this type a ready transition finds only stale
# pre-ready runs and the gate times out waiting for fresh ones.
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
- dev
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci-typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ on:
- main
- dev
pull_request:
# ready_for_review matters: the SpecGit Acceptance verdict requires
# required-check runs that started at/after the draft→ready transition
# (#315 anchor). Without this type a ready transition finds only stale
# pre-ready runs and the gate times out waiting for fresh ones.
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
- dev
Expand Down
282 changes: 246 additions & 36 deletions .github/workflows/specgit-accept.yml

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions .specgit.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
version: 1
delivery: correct-v1-0
delivery: accept-builtin-spec
context:
kind: branch
branch: docs/493-correct-v1-0
branch: fix/506-accept-builtin-spec
issues:
- 493
- 494
pr: 495
- 506
- 507
- 508
issueKinds:
- issue: 506
kind: kind::fix
- issue: 507
kind: kind::fix
- issue: 508
kind: kind::docs
pr: 509
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ feat/**, fix/** ──PR(Typecheck + Unit Tests 门禁)──▶ dev ──push
**CI 配置**:
- `ci-typecheck.yml`:push 到 `main`/`dev` + PR → `main`/`dev` 时触发;除 lint + typecheck 外还跑 `test:dag-core` DAG 核心行为/覆盖率门禁(10min 超时)
- `ci-test.yml`:push 到 `main`/`dev` + PR → `main`/`dev` 时触发全量测试(`cancel-in-progress: false` 保证跑完);Linux unit-tests job 额外校验生成物新鲜度(`packages/client` 与 `packages/sdk/js` 的 `check:generated`)并跑 HttpAPI 契约门禁(`test:httpapi:ci`)
- `specgit-accept.yml`:仅 PR → `main` 时触发;全局安装 `specgit@^0.5.0`(`npm install -g`;workspace 内安装会因 bun `catalog:` 协议失败),等 `spec_git/policy.yaml` `required_checks` 全部到终态后运行 `specgit finish --json` 产出 SpecGit Acceptance 裁决
- `specgit-accept.yml`:仅 PR → `main` 时触发;全局安装 `specgit@1.10.1`(`npm install -g`;workspace 内安装会因 bun `catalog:` 协议失败),等 `spec_git/policy.yaml` `required_checks` 全部到终态(等待脚本手工解析 YAML,不依赖 `yaml` 包)后运行 `specgit finish --json` 产出 SpecGit Acceptance 裁决
- `release-fork.yml`:手动 `workflow_dispatch` 是唯一真实构建路径(push 到 `main`/`dev` 仅注册不构建);从 `dev` 发布自动产出 `X.Y.Z-dev.N` prerelease,从 `main` 发布 `X.Y.Z` 并标 Latest

## Standard Delivery Workflow (标准交付流程)
Expand Down Expand Up @@ -267,7 +267,10 @@ This repository uses a multi-context domain-document layout rooted at `CONTEXT-M
Kept OUTSIDE the managed block so `specgit init`/`--force` never rewrites them; re-apply each deviation after every re-init:

- `specgit-accept.yml` drops the template's `workflow_dispatch` trigger. Dispatch is the privileged context that fires CodeQL's cache-poisoning taint rule on the `head_ref` checkout (false positive: no cache use, read-only token, `persist-credentials: false`), and on dispatch events `head_ref` is empty so the verdict would evaluate the default branch — the wrong tree. Delivery here always goes through a PR. The head-ref checkout itself must NOT be replaced with a SHA: `specgit finish` requires HEAD on the delivery branch (detached_head otherwise).
- `specgit-accept.yml` installs the CLI globally (`npm install -g specgit@^0.5.0`), NOT the template's `npm install --no-save specgit@X`: a workspace-local install reads this bun workspace's package.json and dies on the `catalog:` protocol (EUNSUPPORTEDPROTOCOL, #434, again on #459). Re-apply after every `specgit init --force`.
- `specgit-accept.yml` installs the CLI globally (`npm install -g specgit@1.10.1`), NOT the template's `npm install --no-save specgit@X`: a workspace-local install reads this bun workspace's package.json and dies on the `catalog:` protocol (EUNSUPPORTEDPROTOCOL, #434, again on #459). Re-apply after every `specgit init --force`.
- `specgit-accept.yml` runs the job with `timeout-minutes: 45` while the wait script's own poll deadline is 40 minutes (`Date.now() + 40 * 60 * 1000`): the deadline must stay BELOW the job timeout so a lost race against a slow sibling check exits with its own diagnosis instead of being killed mid-line by the job timeout.
- `specgit-accept.yml` pins `node-version: '22'` for the wait script and the CLI.
- The wait script hand-parses `spec_git/policy.yaml` (minimal line-based parse) instead of importing the `yaml` package: no root-reachable `yaml` exists under workspace catalog isolation, so `import { parse } from 'yaml'` would fail to resolve on the runner.
- `spec_git/policy.yaml` `required_checks` uses the template's canonical check IDs (`unit-tests`, `e2e-tests`), not display names.

<!-- specgit:block:start -->
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/plugin/command/orchestration-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,6 @@ A replan fragment takes real time to compose — template rendering, model reaso
1. On any user cancel/replan/model-change intent, IMMEDIATELY issue `control(pause)` in the same turn. Pause needs no fragment, applies in milliseconds, and stops new node spawns.
2. Pause does not interrupt nodes that are already running. Decide their disposition inside the fragment: `restart: true` re-spawns a running node with the new definition (its in-flight child session is hard-aborted at re-spawn), `cancel: true` terminates it, absence keeps it running to completion.
3. Compose the fragment, then issue `control(replan)` — replan is valid while paused.
4. Issue `control(resume)` to restore scheduling.
4. A successful replan auto-resumes the workflow. Issue `control(resume)` manually only when the replan output reports the automatic resume raced with another control op and the workflow is still paused; never resume a workflow the output says was already resumed.

If the workflow terminalized before you paused, do not force the replan: start a new workflow carrying the updated definitions, and state which prior results are superseded.
34 changes: 28 additions & 6 deletions packages/core/src/plugin/command/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ config:

## Saved workflows

A `spec_path` with no path separator and no `.yaml`/`.yml` extension is a
**name** resolved against the workflow library instead of the filesystem:
A `spec_path` with no path separator, no leading `.`, no control characters,
and no `.yaml`/`.yml` extension is a **name** resolved against the workflow
library instead of the filesystem (a leading dot or a recognized extension
means a filesystem path). The synthetic `builtin://<name>` marker the `list`
output shows for builtin templates also resolves by name:

1. `.opencode/workflows/<name>.yaml` — project scope, committed with the repo
2. `<opencode config dir>/workflows/<name>.yaml` — global scope, available in every project
Expand Down Expand Up @@ -340,7 +343,7 @@ Workflows are not static. After creating a workflow, use `extend` and `control(r

- **Scale up**: a node reports the work is larger than expected → `extend` with additional parallel nodes to split the load.
- **Cut short**: a node proves the remaining work is unnecessary → `control(complete)` to early-complete and skip pending nodes.
- **Redirect**: a gate or review reveals a wrong direction → `control(pause)` first to freeze scheduling, then `control(replan)` with `restart: true` on the affected nodes and `cancel: true` on their downstream dependents, then `control(resume)`.
- **Redirect**: a gate or review reveals a wrong direction → `control(pause)` first to freeze scheduling, then `control(replan)` with `restart: true` on the affected nodes and `cancel: true` on their downstream dependents. A successful replan auto-resumes a paused workflow; issue `control(resume)` manually only when the replan output reports the automatic resume raced with another control op.

Only nodes with `report_to_parent: true` produce intermediate parent
checkpoints, and those reports are delivered at the next actionable wake
Expand Down Expand Up @@ -527,6 +530,24 @@ validation status. This lists reusable specs, not running workflows; use
Pass `spec_path`, then retarget generic objectives and block instructions in
the parent, write the edited result to YAML, and start that file by path.

**draft** — Render a structured graph `config` (same fields as the start
file's `config`) into a validated YAML spec and return its `spec_path`; no
workflow is created. Preferred over hand-writing YAML: the parameter schema
rejects unknown fields, eliminating serialization drift. When validation fails
the file is still on disk — fix it by calling draft again with corrected
fields, then start the returned `spec_path`.

**guide** — Load on-demand authoring guidance. `topic` is one of `blocks`
(composable block schema), `interface` (low-level node fields), `policy`
(gates, admission, recovery), or `patterns` (cross-domain playbooks); omit it
for the compact index. Load only the topic needed for the current decision.

**validate** — Pre-flight one spec without creating a workflow. Pass
`spec_path` and an optional `profile` (`portable` for distributable-template
checks, `environment` to additionally resolve prompts, workers, and models in
this project; builtin specs default to portable, everything else to
environment). Returns diagnostics with per-error paths, never a workflow ID.

**extend** — Add nodes to a running workflow. Existing nodes are unaffected;
new nodes are immediately eligible for scheduling if their dependencies are
met. It also accepts a genuinely additive wave after a reporting leaf
Expand All @@ -547,9 +568,9 @@ omitted content from its preview.
**control** — Control a running workflow:

- `pause` — let running nodes finish, don't spawn new ones (pause does NOT stop nodes that are already running). On a cancel/replan intent, always pause FIRST: it needs no fragment and freezes scheduling while you compose the replan, so the graph cannot terminalize under you.
- `resume` — resume scheduling
- `resume` — resume scheduling. Unneeded after a successful replan or extend: both auto-resume a paused workflow; resume manually only when their output reports the automatic resume raced with another control op and the workflow is still paused.
- `cancel` — cancel the entire workflow
- `replan` — put `fragment: { ... }` with the graph fields and node definitions in YAML and pass its `spec_path`; running nodes can be `restart: true` or `cancel: true`; pending nodes absent from the fragment are cancelled. Valid while paused — the pause → write file → replan → resume sequence is the safe path.
- `replan` — put `fragment: { ... }` with the graph fields and node definitions in YAML and pass its `spec_path`; running nodes can be `restart: true` or `cancel: true`; pending nodes absent from the fragment are cancelled. Valid while paused — the pause → write file → replan sequence is the safe path: a successful replan auto-resumes the workflow, an explicit resume belongs only in the rare case the replan output reports the automatic resume raced with another control op and the workflow is still paused.
- `complete` — early-complete: remaining pending nodes are skipped (non-violation)
- `step` — advance exactly one ready node (the first by node ID lexicographic order), then wait. Use for controlled debugging or staged verification of a critical path. Unlike `pause`, which freezes all scheduling, `step` advances one node and re-waits. A second `step` while the stepped node is still running is rejected. Use `resume` to return to full-speed scheduling. Nodes are selected in lexicographic ID order for determinism.

Expand All @@ -566,10 +587,11 @@ omitted content from its preview.
| `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) |
| `worker_config` | no | `{ timeout_ms }` — bounds the node from admission to completion (defaults to 10 minutes if omitted); queue wait counts toward the budget, an expired queued node fails without spawning, and a running node that exceeds it escalates to the parent for adjudication (capped deadline extensions) before failing |
| `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 |
| `review` | no | (deep review workers) `{ phase: "design" \| "diff" }`; a `diff` review must also declare `implementation_node_id` / `verification_node_id` and wire them: transitive review→verification→implementation dependencies, `input_mapping` for the diff artifact + fingerprint + verification output, a PASS-gated `condition`, and a `verdict`+`implementation_fingerprint` `output_schema`. Authoring rejects violations in deep mode and warns in standard |

### What NOT to expect

Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/dag/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export class WorkflowBlock extends Schema.Class<WorkflowBlock>("WorkflowBlock")(
timeout_ms: Schema.Number,
}),
).annotate({
description: "{ timeout_ms } — bounds node execution; overrides config.node_defaults.worker_config",
description:
"{ timeout_ms } — bounds the node from admission to completion; queue wait counts toward the budget and an expired queued node fails without spawning. A running node that exceeds it escalates to the parent for adjudication (capped deadline extensions) before failing. Overrides config.node_defaults.worker_config",
}),
required: Schema.optional(Schema.Boolean).annotate({
description:
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/dag/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ export const NodeSchema = Schema.Struct({
Schema.Struct({
timeout_ms: Schema.optional(Schema.Number),
}),
).annotate({ description: "{ timeout_ms } — bounds node execution. Inherits config.node_defaults.worker_config" }),
).annotate({
description:
"{ timeout_ms } — bounds the node from admission to completion; queue wait counts toward the budget and an expired queued node fails without spawning. A running node that exceeds it escalates to the parent for adjudication (capped deadline extensions) before failing. Inherits config.node_defaults.worker_config",
}),
input_mapping: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({
description:
'Optional variable-to-source map, e.g. { resultA: "node-a", count: "node-b.output.count" }. Omit to expose each direct dependency under its node ID',
Expand Down Expand Up @@ -230,7 +233,7 @@ export const NodeSchema = Schema.Struct({
}),
).annotate({
description:
"(deep review workers) design reviews pre-implementation artifacts; diff reviews require implementation_node_id and verification_node_id",
"(deep review workers) design reviews pre-implementation artifacts; diff reviews require implementation_node_id, verification_node_id, and authoring-validated wiring: transitive review→verification→implementation dependencies, input_mapping for the diff artifact, fingerprint, and verification output, a PASS-gated condition, and a verdict+implementation_fingerprint output_schema; deep mode additionally requires the diff review to feed a required final gate conditioned on verdict ACCEPT. Violations are authoring errors in deep mode, warnings in standard mode.",
}),
})

Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ type AgentCatalogOptions = {
heading: string
includeHidden: boolean
includeModelState: boolean
/** Workflow worker_type defaults compile to the native build/plan primary
* agents, so the workflow catalog lists native primaries even though the
* task subagent catalog and user-defined primary modes stay filtered out. */
includePrimary?: boolean
}

export interface Interface {
Expand Down Expand Up @@ -278,7 +282,7 @@ export const layer = Layer.effect(
options: AgentCatalogOptions,
) {
const description = (yield* agents.list())
.filter((item) => item.mode !== "primary")
.filter((item) => item.mode !== "primary" || (options.includePrimary && item.native))
.filter((item) => options.includeHidden || !item.hidden)
.filter((item) => Permission.evaluate("task", item.name, caller.permission).action !== "deny")
.toSorted((a, b) => a.name.localeCompare(b.name))
Expand Down Expand Up @@ -333,6 +337,7 @@ export const layer = Layer.effect(
heading: "Available workflow worker_type values:",
includeHidden: false,
includeModelState: true,
includePrimary: true,
})
: undefined,
]
Expand Down
Loading
Loading