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
22 changes: 10 additions & 12 deletions packages/core/src/plugin/command/orchestration-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,16 @@ continue QA, reduce scope, use `standard`, or explicitly waive. A `WAIVED`
start is informed only when both `waiver_reason` and `acknowledged_risks` are
non-empty; preserve them for audit.

Do not supply `protocol_version`, `state`, or `fingerprint` in the admission
input. Those are durable audit fields owned by the workflow boundary:
it sets protocol version 1, initializes state from the verdict, normalizes the
Brief for fingerprint computation, and computes the lowercase hexadecimal
SHA-256 hash. A successful deep start alone transitions the durable record to
`CONSUMED`.
The author-written admission input accepts only `brief_revision`, `qa_mode`,
`verdict`, `brief`, and, for an informed waiver, `waiver_reason` and
`acknowledged_risks`. Do not copy any additional fields from a persisted
workflow or tool response. The workflow boundary creates and advances its
durable audit record.

Material changes to goal, scope, constraints, assumptions, or acceptance
criteria create a new brief revision, invalidate the prior fingerprint, and
return admission to questioning. The workflow boundary generates the
replacement fingerprint from the revised Brief. Do not replay QA from a
consumed record after recovery.
criteria create a new brief revision, invalidate the prior admission record,
and return admission to questioning. Do not replay QA from a consumed record
after recovery.

## Role Resolution

Expand All @@ -185,8 +183,8 @@ If a required capability has no eligible role, report the missing capability and

## Model Assignment

Never emit `node.model` or `config.node_defaults.model`. Model assignment belongs
to runtime configuration, not the workflow graph:
Workflow YAML has no model-selection field. Model assignment belongs to
runtime configuration, not the workflow graph:

`dag.jsonc` tier → configured agent model → parent session model

Expand Down
80 changes: 77 additions & 3 deletions packages/core/src/plugin/command/workflow-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ Blocks are the high-level interface for assembling a one-off workflow YAML
file. The tool compiles them into ordinary durable DAG nodes before validation
and persistence. Existing node-based YAML remains compatible.

## Shape
## Authoring contract

Use `objective` and `blocks` inside `config` for **start**, or alongside
`blocks` for **extend**. A replan uses the same fields inside `fragment`.
Never infer or invent a YAML field. Copy the envelope for the intended action
and change values only. Unknown fields are errors; a field from a tool call,
runtime result, low-level node, or another action does not belong here unless
it is explicitly listed below.

### Start file

The author-written top-level fields are optional `title`, optional `mode`,
optional `admission`, and required `config`. For the block route, `config`
contains required `name`, `objective`, and `blocks`; its only optional fields
are `node_defaults`, `max_concurrency`, `max_node_replan_attempts`, and
`max_total_nodes`.

```yaml
title: Implement session recovery
config:
name: implement-session-recovery
objective: Implement session recovery with focused tests and evidence-backed review.
Expand All @@ -33,6 +44,69 @@ config:
depends_on: [verify]
```

### Extend file

An extend file contains exactly `objective` and `blocks`. It has no `config`,
`name`, or `fragment` wrapper.

```yaml
objective: Add regression coverage for the newly confirmed recovery edge case.
blocks:
- id: recovery-fix
kind: coding
instruction: Implement only the confirmed edge-case fix and its regression test.
- id: recovery-verify
kind: verify
depends_on: [recovery-fix]
```

### Replan file

A replan file contains only `fragment`. For the block route, `fragment` has the
same fields as start's `config`: required `name`, `objective`, and `blocks`,
plus the four optional config fields listed above.

```yaml
fragment:
name: recover-session-recovery
objective: Replace the invalid route with a diagnosed, verified repair path.
blocks:
- id: diagnose
kind: debug
instruction: Reproduce the failure and identify the evidence-backed root cause.
- id: repair
kind: coding
depends_on: [diagnose]
instruction: Apply the smallest repair supported by the diagnosis.
- id: verify
kind: verify
depends_on: [repair]
```

Every block accepts only `id`, `kind`, `depends_on`, `instruction`,
`worker_type`, `required`, and `report_to_parent`. `id` and `kind` are required.
Allowed `kind` values are `explore`, `plan`, `prototype`, `debug`, `coding`,
`verify`, `review`, and `synthesize`. Omit `worker_type` unless the exact
configured agent name is already known; never invent one.

For optional `node_defaults`, the only fields are `required`,
`report_to_parent`, and `worker_config`; `worker_config` accepts only
`timeout_ms`.

`action`, `workflow_id`, `operation`, and `spec_path` are tool-call fields, not
YAML fields. `profile` is also a tool-call field used only by validate. The
listed YAML fields are exhaustive: do not copy extra fields from read/status
output or persisted runtime records. Deep admission is the only exception to
the minimal start envelope; load `guide(topic=policy)` and copy its
author-written admission shape instead of guessing fields.

Use these exact calls after writing the file:

- validate: `{ action: "validate", spec_path: "workflow.yaml", profile: "portable" }`
- start: `{ action: "start", spec_path: "workflow.yaml" }`
- extend: `{ action: "extend", workflow_id: "dag_...", spec_path: "extend.yaml" }`
- replan: `{ action: "control", operation: "replan", workflow_id: "dag_...", spec_path: "replan.yaml" }`

This guide owns the author-written block fields and semantics. The action
schema stays shallow and accepts only `spec_path`; the YAML validator rejects
unknown or missing graph fields by name and reports each error with its path.
Expand Down
22 changes: 11 additions & 11 deletions packages/core/src/plugin/command/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ file and retry with the same path.

Before a deep start, qualify the request interactively in the parent session.
The start spec places `mode: deep`, a versioned `READY` or informed `WAIVED`
admission input, and `config` at the same level. The admission input contains
`brief_revision`, `qa_mode`, `verdict`, `brief`, and waiver audit fields when
applicable; the workflow boundary owns `protocol_version`, `state`, and
`fingerprint`. Do not put admission QA inside the graph: its answers define the
admission input, and `config` at the same level. The admission input accepts
only `brief_revision`, `qa_mode`, `verdict`, `brief`, and waiver audit fields
when applicable. Do not copy additional fields from persisted records or tool
responses. Do not put admission QA inside the graph: its answers define the
graph. Use the orchestration policy below for QA modes, round budgets, verdict
recovery, revision invalidation, and waiver audit fields.

Expand Down Expand Up @@ -142,8 +142,9 @@ config:
timeout_ms: 600000
```

Never emit `node.model` or `config.node_defaults.model`. Model selection is
configuration-owned: critical nodes (`required: true` and review workers) use
The listed node and default fields are exhaustive; workflow YAML has no
model-selection field. Model selection is configuration-owned: critical nodes
(`required: true` and review workers) use
the `advanced` tier in `dag.jsonc`, other nodes use `standard`, then resolution
falls back to the selected agent model and the parent-session model. If no
source provides a model, the workflow tool starts parent-session QA and leaves
Expand Down Expand Up @@ -467,11 +468,10 @@ is believed to be running. Two disciplines close the gap:

## Model Assignment Strategy

Workflow definitions MUST NOT specify `node.model` or
`config.node_defaults.model`. Resolution follows the `dag.jsonc` tier, then the
configured agent model, then the parent-session model. If all three are absent,
the workflow tool asks the user to configure a model and does not create the
workflow.
Workflow YAML has no model-selection field. Resolution follows the `dag.jsonc`
tier, then the configured agent model, then the parent-session model. If all
three are absent, the workflow tool asks the user to configure a model and does
not create the workflow.

- Expensive models for planning, review, and arbitration — high-stakes decisions where reasoning quality matters.
- Fast models for mechanical implementation — well-specified edits where speed and cost matter.
Expand Down
49 changes: 37 additions & 12 deletions packages/core/test/plugin/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,32 @@ describe("CommandPlugin.Plugin", () => {
}),
)

it.effect("publishes an exact block YAML authoring contract", () =>
Effect.sync(() => {
expect(CommandPlugin.WorkflowBlocksContent).toContain("Never infer or invent a YAML field")
expect(CommandPlugin.WorkflowBlocksContent).toContain("### Start file")
expect(CommandPlugin.WorkflowBlocksContent).toContain("### Extend file")
expect(CommandPlugin.WorkflowBlocksContent).toContain("### Replan file")
expect(CommandPlugin.WorkflowBlocksContent).toMatch(
/`id`, `kind`, `depends_on`, `instruction`,\s+`worker_type`, `required`, and `report_to_parent`/,
)
expect(CommandPlugin.WorkflowBlocksContent).toMatch(
/`action`, `workflow_id`, `operation`, and `spec_path` are tool-call fields/,
)
const authoringContract = CommandPlugin.WorkflowBlocksContent.slice(
CommandPlugin.WorkflowBlocksContent.indexOf("## Authoring contract"),
CommandPlugin.WorkflowBlocksContent.indexOf("This guide owns"),
)
expect(authoringContract).toMatch(/[Tt]he\s+listed YAML fields are exhaustive/)
expect(authoringContract).not.toMatch(
/session_id|project_id|protocol_version|fingerprint|node_defaults\.model|node\.model/,
)
expect(CommandPlugin.WorkflowBlocksContent).toContain(
'{ action: "control", operation: "replan", workflow_id: "dag_...", spec_path: "replan.yaml" }',
)
}),
)

it.effect("preserves opt-outs read-only scope and explicit role assignments", () =>
Effect.sync(() => {
expect(CommandPlugin.OrchestrationPolicyContent).toContain("single agent")
Expand All @@ -137,9 +163,7 @@ describe("CommandPlugin.Plugin", () => {

it.effect("documents config-first model fallback without invented identifiers", () =>
Effect.sync(() => {
expect(CommandPlugin.OrchestrationPolicyContent).toContain(
"Never emit `node.model` or `config.node_defaults.model`",
)
expect(CommandPlugin.OrchestrationPolicyContent).toContain("Workflow YAML has no model-selection field")
expect(CommandPlugin.OrchestrationPolicyContent).toContain(
"`dag.jsonc` tier → configured agent model → parent session model",
)
Expand Down Expand Up @@ -338,14 +362,17 @@ describe("CommandPlugin.Plugin", () => {
expect(CommandPlugin.OrchestrationPolicyContent).toContain("waiver_reason")
expect(CommandPlugin.OrchestrationPolicyContent).toContain("acknowledged_risks")
expect(CommandPlugin.OrchestrationPolicyContent).toContain("Material changes")
expect(CommandPlugin.OrchestrationPolicyContent).toContain("invalidate the prior fingerprint")
expect(CommandPlugin.OrchestrationPolicyContent).toContain("SHA-256 hash")
expect(CommandPlugin.WorkflowFactsContent).toContain(
"The start spec places `mode: deep`, a versioned `READY` or informed `WAIVED`",
expect(CommandPlugin.OrchestrationPolicyContent).toContain("invalidate the prior admission record")
const admissionPolicy = CommandPlugin.OrchestrationPolicyContent.slice(
CommandPlugin.OrchestrationPolicyContent.indexOf("## Deep Admission QA"),
CommandPlugin.OrchestrationPolicyContent.indexOf("## Role Resolution"),
)
expect(admissionPolicy).not.toMatch(/protocol_version|fingerprint/)
expect(CommandPlugin.WorkflowFactsContent).toContain(
"the workflow boundary owns `protocol_version`, `state`, and\n`fingerprint`",
"The start spec places `mode: deep`, a versioned `READY` or informed `WAIVED`",
)
expect(CommandPlugin.WorkflowFactsContent).toContain("The admission input accepts\nonly `brief_revision`")
expect(CommandPlugin.WorkflowFactsContent).not.toMatch(/protocol_version|node_defaults\.model|node\.model/)
expect(CommandPlugin.WorkflowFactsContent).toContain("A one-off graph may use a")
expect(CommandPlugin.WorkflowFactsContent).toContain("task-local file")
expect(CommandPlugin.WorkflowFactsContent).not.toContain("`config.mode`")
Expand Down Expand Up @@ -388,11 +415,9 @@ describe("CommandPlugin.Plugin", () => {
expect(CommandPlugin.WorkflowFactsContent).not.toContain("Gate failure cancels the workflow automatically")
expect(CommandPlugin.WorkflowFactsContent).toContain("Static `prompt_template.input`")
expect(CommandPlugin.WorkflowFactsContent).toContain("it must\nnever appear as `[object Object]`")
expect(CommandPlugin.WorkflowFactsContent).toContain(
"Workflow definitions MUST NOT specify `node.model` or\n`config.node_defaults.model`",
)
expect(CommandPlugin.WorkflowFactsContent).toContain("Workflow YAML has no model-selection field")
expect(CommandPlugin.WorkflowFactsContent).toMatch(
/`dag\.jsonc` tier, then the\s+configured agent model, then the parent-session model/,
/`dag\.jsonc`\s+tier, then the\s+configured agent model, then the parent-session model/,
)
expect(CommandPlugin.WorkflowFactsContent).toContain("Propose-then-assemble")
const reviewExample = CommandPlugin.WorkflowFactsContent.slice(
Expand Down
30 changes: 30 additions & 0 deletions packages/opencode/test/dag/workflow-authoring.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { CommandPlugin } from "@opencode-ai/core/plugin/command"
import { WorkflowAuthoring } from "../../src/dag/authoring"
import { DagValidation } from "../../src/dag/validation"
import { testEffect } from "../lib/effect"
Expand All @@ -23,6 +24,35 @@ const start = {
}

describe("WorkflowAuthoring source-to-graph seam", () => {
it.effect("keeps every block-guide YAML envelope executable", () =>
Effect.gen(function* () {
const guide = CommandPlugin.WorkflowBlocksContent
const example = (heading: string) => {
const section = guide.slice(guide.indexOf(`### ${heading}`))
const match = section.match(/```yaml\n([\s\S]*?)```/)
expect(match?.[1]).toBeDefined()
return match?.[1] ?? ""
}
const authoring = WorkflowAuthoring.make()
const inputs = [
{ action: "start" as const, content: example("Start file") },
{ action: "extend" as const, content: example("Extend file") },
{ action: "replan" as const, content: example("Replan file") },
]

for (const input of inputs) {
const result = yield* authoring.prepare({
action: input.action,
source: { kind: "yaml", source: `${input.action}.yaml`, content: input.content },
profile: "portable",
})
expect(result.errors).toEqual([])
expect(result.valid).toBe(true)
expect(result.prepared?.nodes.length).toBeGreaterThan(0)
}
}),
)

it.effect("prepares start, extend, and replan through one action-aware interface", () =>
Effect.gen(function* () {
const authoring = WorkflowAuthoring.make()
Expand Down
Loading