From d5a572be031aba55f14caf68fb1bca024227044f Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:14:04 +0800 Subject: [PATCH 01/11] fix(dag): restore file-backed workflow input --- packages/core/src/plugin/command/dag-flow.txt | 8 +- .../src/plugin/command/workflow-blocks.md | 13 +- .../src/plugin/command/workflow-routing.md | 16 +- packages/core/src/plugin/command/workflow.md | 41 +- packages/core/test/plugin/command.test.ts | 15 +- packages/opencode/src/dag/CONTEXT.md | 5 +- .../docs/adr/0001-workflow-authoring-check.md | 11 +- packages/opencode/src/tool/workflow.ts | 89 +- .../opencode/test/dag/workflow-tool.test.ts | 237 +-- .../__snapshots__/parameters.test.ts.snap | 1472 +---------------- .../workflow-parameters-post-change.json | 130 +- .../test/tool/workflow-authoring.test.ts | 61 +- .../tool/workflow-provider-schema.test.ts | 58 +- 13 files changed, 285 insertions(+), 1871 deletions(-) diff --git a/packages/core/src/plugin/command/dag-flow.txt b/packages/core/src/plugin/command/dag-flow.txt index 7de158df1f..32c57329ad 100644 --- a/packages/core/src/plugin/command/dag-flow.txt +++ b/packages/core/src/plugin/command/dag-flow.txt @@ -11,13 +11,13 @@ execution; the router still owns any material Decision Checkpoint. Prefer composable blocks for a fresh flow. Load `workflow(action="guide", topic="blocks")` only if the block contract is not -already in context. Use inline `spec` for one-off work; use `spec_path` only -when a saved workflow already matches or persistence was requested. Preserve +already in context. Write one-off work to a task-local YAML file and pass its +`spec_path`; a matching saved workflow name is also a valid `spec_path`. Preserve the task, user constraints, named roles, read-only limits, acceptance checks, and confirmed decisions in the objective and block instructions. -Call the workflow tool with `action=start` in the first response after the -route is ready. Printing a plan, JSON, or YAML does not start it. Never invent +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. diff --git a/packages/core/src/plugin/command/workflow-blocks.md b/packages/core/src/plugin/command/workflow-blocks.md index 95feef7e21..0e06745d42 100644 --- a/packages/core/src/plugin/command/workflow-blocks.md +++ b/packages/core/src/plugin/command/workflow-blocks.md @@ -1,8 +1,8 @@ # Composable Workflow Blocks -Blocks are the high-level interface for assembling a one-off workflow. The -tool compiles them into ordinary durable DAG nodes before validation and -persistence. Existing node-based YAML remains compatible. +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 @@ -33,10 +33,9 @@ config: depends_on: [verify] ``` -The parameter schema owns the exact block field shapes; the tool rejects -unknown or missing fields by name, and `workflow(action="validate")` reports -each field error with its path. This guide covers semantics and constraints -only — compose blocks against the schema, not against prose. +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. `objective` is required and is injected into every generated node. Use blocks or nodes, never both. Block IDs use letters, numbers, underscores, and hyphens. diff --git a/packages/core/src/plugin/command/workflow-routing.md b/packages/core/src/plugin/command/workflow-routing.md index 92394309a4..e2112b7c6d 100644 --- a/packages/core/src/plugin/command/workflow-routing.md +++ b/packages/core/src/plugin/command/workflow-routing.md @@ -55,12 +55,13 @@ instructions specialize the task and never name external Skills. When a saved route matches the topology, read it, retarget its objective and block instructions, and prune or add justified blocks before starting the -edited inline spec. Start `spec_path` directly only when its target already -matches exactly. Use low-level nodes only for bindings, conditions, output -schemas, or lifecycle metadata blocks cannot express. +edited YAML file. Start the saved `spec_path` directly only when its target +already matches exactly. Use low-level nodes only for bindings, conditions, +output schemas, or lifecycle metadata blocks cannot express. -Validate the composed or edited spec before start. Fix every diagnostic and -validate again; validation creates no workflow. A successful start returns the +Write the composed or edited graph to YAML and validate that `spec_path` before +start. Fix every diagnostic in the same file and validate again; validation +creates no workflow. A successful start returns the exact workflow ID. The parent owns the brief, graph, user interaction, checkpoints, controls, and final report; children own bounded executable work. End after start and let the workflow wake the parent. Do not poll merely to @@ -74,5 +75,6 @@ wait, and never claim an unstarted graph is running. - `guide(topic="policy")`: gates, recovery, and bounded repair. - `guide(topic="patterns")`: larger domain playbooks. -The tool parameter schema owns required fields and exclusivity; author calls -from that schema rather than reconstructed prose. +The tool parameter schema owns action fields and requires `spec_path`; the +on-demand block/interface guides own author-written YAML fields, and validation +is the final authority for the file. diff --git a/packages/core/src/plugin/command/workflow.md b/packages/core/src/plugin/command/workflow.md index bc1e296b4e..c802c0a880 100644 --- a/packages/core/src/plugin/command/workflow.md +++ b/packages/core/src/plugin/command/workflow.md @@ -36,11 +36,11 @@ as independent workstreams, cross-domain uncertainty, high blast radius, conflicting constraints, evidence gathering, or multiple verification perspectives. -For a one-off graph, pass `spec` inline on `start`, `extend`, or -`control(replan)`. This is the default: do not create a transient YAML file. -Use `spec_path` only for a saved workflow name, a reusable workflow file, or an -explicitly requested file-backed spec. Exactly one of `spec` and `spec_path` is -valid. After a validation failure, correct the same source and retry the call. +Before `start`, `extend`, `control(replan)`, or `validate`, write the graph to a +`.yaml` or `.yml` file and pass only `spec_path`. A one-off graph may use a +task-local file such as `.opencode/.dag-specs/.yaml`; it does not need to +become a saved library workflow. After a validation failure, edit that same +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` @@ -94,10 +94,11 @@ Prefer a saved workflow when the user names a recurring procedure ("run the code review workflow") and the saved target/inputs already match: starting it is one call, and its graph has already been reviewed. When only its topology matches, call `{ action: "read", spec_path: "code-review" }`, retarget its objective and block instructions to the current task, prune or add lanes, then -start that edited value as an inline spec. `read` never starts a workflow. -Compose a fresh inline `spec` when the task is one-off or no reference fits. -To turn a working one-off spec into a saved workflow, persist it as YAML in one -of the two workflow-library directories under a descriptive name. +write the edited value to a task-local YAML file and start its `spec_path`. +`read` never starts a workflow. Compose a fresh task-local YAML file when the +task is one-off or no reference fits. To turn a working one-off spec into a +saved workflow, move it into one of the two workflow-library directories under +a descriptive name. ## Orchestration Lifecycle @@ -151,9 +152,9 @@ the workflow uncreated so the user can configure a model and retry. ## Collaboration Patterns Four structural patterns cover the common cases. Real workflows often combine -them. Every block below shows the object shape for inline `spec`; pass the -selected shape with `{ action: "start", spec: { ... } }`. Persist it as YAML -and use `spec_path` only when the workflow itself should be saved. +them. Every block below is YAML file content. Save the selected shape, validate +it with `{ action: "validate", spec_path: ".yaml" }`, then start it with +`{ action: "start", spec_path: ".yaml" }`. ### 1. Staged Pipeline with Gate @@ -540,10 +541,9 @@ All nodes share the same workspace. Write conflicts are an orchestration concern ### Actions **start** — Create a workflow from `config` and optional `title`, `mode`, and -admission input. For a one-off graph call -`{ action: "start", spec: { config: { ... } } }`. Use `spec_path` for a saved -workflow name (`{ action: "start", spec_path: "code-review" }`) or an explicit -YAML path. +admission input stored in YAML. Pass a task-local YAML path for one-off work or +a saved workflow name such as +`{ action: "start", spec_path: "code-review" }`. Returns the workflow ID. Nodes declare `depends_on` (node IDs); layers and execution order are computed automatically. @@ -553,14 +553,15 @@ not running workflows; use `status` for a workflow's live state. **read** — Return one saved workflow as structured JSON without starting it. Pass `spec_path`, then retarget generic objectives and block instructions in -the parent before using the edited result as an inline `start` spec. +the parent, write the edited result to YAML, and start that file by path. **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 checkpoint naturally completed the current graph; an early -`control(complete)` workflow remains terminal. Put the new nodes under `spec.nodes`, -then call `{ action: "extend", workflow_id: "dag_...", spec: { nodes: [...] } }`. +`control(complete)` workflow remains terminal. Put the new nodes under `nodes` +in a YAML file, then call +`{ action: "extend", workflow_id: "dag_...", spec_path: "extend.yaml" }`. **status** — Read the durable state of one workflow and all of its nodes. Pass `workflow_id`. Use it when the user explicitly asks for current state or once before a decision that requires fresh state, such as replan/control. Do not poll a running workflow merely to wait: node reports and terminal outcomes wake the parent session automatically. @@ -576,7 +577,7 @@ omitted content from its preview. - `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 - `cancel` — cancel the entire workflow -- `replan` — pass `spec: { fragment: { ... } }` with the graph fields and node definitions; running nodes can be `restart: true` or `cancel: true`; pending nodes absent from the fragment are cancelled. Valid while paused — the pause → compose spec → replan → resume sequence is the safe path. Use `spec_path` only for a saved or explicitly file-backed fragment. +- `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. - `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. diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index 264fe05098..328c5424c4 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -107,10 +107,10 @@ describe("CommandPlugin.Plugin", () => { }), ) - it.effect("uses inline specs for one-off graphs without removing saved workflows", () => + it.effect("uses file-backed specs for one-off and saved workflows", () => Effect.sync(() => { - expect(CommandPlugin.WorkflowFactsContent).toContain("For a one-off graph, pass `spec` inline") - expect(CommandPlugin.WorkflowFactsContent).toContain("Use `spec_path` only") + expect(CommandPlugin.WorkflowFactsContent).toContain("write the graph to a") + expect(CommandPlugin.WorkflowFactsContent).toContain("task-local file") // The resident description keeps tool selection and the progressive // guide index only; per-action field semantics live in the parameter // schema (change repair-workflow-authoring-validation). @@ -118,9 +118,9 @@ describe("CommandPlugin.Plugin", () => { expect(CommandPlugin.WorkflowContent).toContain("parameter schema") expect(CommandPlugin.WorkflowFactsContent).toContain('{ action: "read", spec_path: "code-review" }') expect(CommandPlugin.WorkflowFactsContent).toContain("retarget its objective and block instructions") - expect(CommandPlugin.WorkflowFactsContent).not.toContain("Never inline graph nodes") - expect(CommandPlugin.WorkflowFactsContent).not.toContain("Before any graph-carrying action") - expect(CommandPlugin.DagFlowContent).toContain("inline `spec`") + expect(CommandPlugin.WorkflowFactsContent).not.toContain("pass `spec` inline") + expect(CommandPlugin.DagFlowContent).toContain("task-local YAML file") + expect(CommandPlugin.DagFlowContent).toContain("`spec_path`") }), ) @@ -346,7 +346,8 @@ describe("CommandPlugin.Plugin", () => { expect(CommandPlugin.WorkflowFactsContent).toContain( "the workflow boundary owns `protocol_version`, `state`, and\n`fingerprint`", ) - expect(CommandPlugin.WorkflowFactsContent).toContain("For a one-off graph, pass `spec` inline") + 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`") }), ) diff --git a/packages/opencode/src/dag/CONTEXT.md b/packages/opencode/src/dag/CONTEXT.md index 063b7ade16..3cd98f308e 100644 --- a/packages/opencode/src/dag/CONTEXT.md +++ b/packages/opencode/src/dag/CONTEXT.md @@ -1,12 +1,12 @@ # Workflow Orchestration Context -Workflow Orchestration turns one user objective into one durable DAG. It supports saved or inline custom workflows and recommends heuristic composition from reusable Blocks. Low-level Nodes remain available when a Block route cannot express the objective. +Workflow Orchestration turns one user objective into one durable DAG. Its model-facing tool accepts saved or file-backed custom workflows and recommends heuristic composition from reusable Blocks. Low-level Nodes remain available when a Block route cannot express the objective. ## Glossary | Term | Meaning | | --- | --- | -| Workflow Source | An inline object or YAML document supplied to start, extend, replan, read, validate, or release tooling. | +| Workflow Source | An in-memory object used by trusted internal callers or a YAML document supplied to runtime and release tooling. Model-authored graph actions use YAML through `spec_path`. | | Workflow Authoring Check | The side-effect-free source-to-graph boundary that parses, normalizes file compatibility, decodes the action shape, compiles Blocks, applies the selected validation profile, and returns diagnostics or a Prepared Workflow Graph. | | Prepared Workflow Graph | A strictly decoded and compiled graph that passed the requested authoring checks and is ready for a runtime mutation. | | Workflow Route | A complete Block or Node composition selected for one objective. It may be custom, saved, or assembled heuristically. | @@ -28,6 +28,7 @@ Workflow Orchestration turns one user objective into one durable DAG. It support - `portable` validation does not load user environment catalogs. `environment` validation reads current catalogs and verifies actual model availability. - No workflow event or durable mutation occurs before a valid Prepared Workflow Graph exists. - The model-facing schema contains fields the model owns. Session/Project identity, admission audit state, model assignment, and other runtime-derived fields remain hidden. +- Model-facing graph actions expose only `spec_path`; graph fields live in YAML so provider tool-call serialization cannot turn a nested graph into a string. - Legacy YAML may be adapted at the file boundary without making legacy fields valid inline input. - Runtime Admission and Workflow Authoring Check have separate names, state, and responsibilities. diff --git a/packages/opencode/src/dag/docs/adr/0001-workflow-authoring-check.md b/packages/opencode/src/dag/docs/adr/0001-workflow-authoring-check.md index 5c1779590b..2dc2f5da23 100644 --- a/packages/opencode/src/dag/docs/adr/0001-workflow-authoring-check.md +++ b/packages/opencode/src/dag/docs/adr/0001-workflow-authoring-check.md @@ -5,7 +5,7 @@ ## Context -Workflow input was interpreted independently by the provider-facing tool schema, start, validate, list/read, replan, CLI, generation, and packaging. Hidden YAML authoring removed the model's accidental examples while leaving it unable to infer required fields. Later patches added validators at individual callers, so accepted shapes and diagnostics drifted and some paths reached durable DAG operations before equivalent checks had run. +Workflow input was interpreted independently by the provider-facing tool schema, start, validate, list/read, replan, CLI, generation, and packaging. Earlier hidden YAML authoring left the model unable to infer required fields. Exposing the complete graph as an inline tool argument fixed discoverability but introduced another failure mode: providers or models could double-serialize the nested `spec` object into a JSON string before validation. Field guidance now belongs to the on-demand workflow guides, while the model-facing action remains shallow and file-backed. The product supports a single custom workflow, saved workflows, and heuristic Block composition. Those are source choices for one orchestration product, not separate validation systems. @@ -15,13 +15,18 @@ The product supports a single custom workflow, saved workflows, and heuristic Bl All tool graph actions and offline config/release consumers call this boundary. Callers may authorize and read files or perform durable DAG mutations, but they do not reinterpret source shape or decide graph validity. +Model-facing `start`, `extend`, `control(replan)`, and `validate` accept only +`spec_path`. One-off graphs use task-local YAML files; saved workflow names use +the same field. Trusted internal consumers may still pass an in-memory source +directly to `WorkflowAuthoring` without creating a second validation path. + The provider schema exposes only author-owned fields. Runtime identity, model assignment, and persisted admission audit fields are derived or adapted behind the boundary. Portable checks are environment-free; environment checks resolve live catalogs and are not cached as content-only facts. ## Consequences - A valid source has one compiled meaning across validate, start, extend, replan, read/list diagnostics, CI, generation, and packaging. -- Provider schema is sufficient for model authoring without exposing runtime-owned fields. -- Legacy YAML remains readable while new inline input stays strict. +- Provider schema stays shallow; on-demand guides describe author-owned YAML fields without exposing runtime-owned fields. +- File-backed custom and saved workflows share one YAML validation path; internal in-memory input stays strict. - Environment changes are observed on the next environment check. - Durable DAG methods retain lifecycle validation as defense in depth, but do not become a second raw-source validator. diff --git a/packages/opencode/src/tool/workflow.ts b/packages/opencode/src/tool/workflow.ts index f1a87358b0..b242722b80 100644 --- a/packages/opencode/src/tool/workflow.ts +++ b/packages/opencode/src/tool/workflow.ts @@ -47,34 +47,18 @@ export { Parameters as WorkflowParameters } // derives ownership from the calling session. // ============================================================================ -const specDescription = "Inline structured spec for a one-off graph. Use this or spec_path, never both" const specPathDescription = - '(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Relative paths resolve from the session directory' + '(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory' -const StartInline = Schema.Struct({ - action: Schema.Literal("start").annotate({ description: "Create a workflow" }), - spec: DagValidation.StartSpec.annotate({ description: specDescription }), -}) const StartPath = Schema.Struct({ action: Schema.Literal("start").annotate({ description: "Create a workflow" }), spec_path: Schema.String.annotate({ description: specPathDescription }), }) -const ExtendInline = Schema.Struct({ - action: Schema.Literal("extend").annotate({ description: "Add nodes or blocks to a live workflow" }), - workflow_id: Dag.ID.annotate({ description: "Target workflow ID" }), - spec: DagValidation.ExtendSpec.annotate({ description: specDescription }), -}) const ExtendPath = Schema.Struct({ action: Schema.Literal("extend").annotate({ description: "Add nodes or blocks to a live workflow" }), workflow_id: Dag.ID.annotate({ description: "Target workflow ID" }), spec_path: Schema.String.annotate({ description: specPathDescription }), }) -const ControlReplanInline = Schema.Struct({ - action: Schema.Literal("control").annotate({ description: "Control a live workflow" }), - operation: Schema.Literal("replan").annotate({ description: "Apply a node fragment (add/cancel/restart/replace)" }), - workflow_id: Dag.ID.annotate({ description: "Target workflow ID" }), - spec: DagValidation.ReplanSpec.annotate({ description: specDescription }), -}) const ControlReplanPath = Schema.Struct({ action: Schema.Literal("control").annotate({ description: "Control a live workflow" }), operation: Schema.Literal("replan").annotate({ description: "Apply a node fragment (add/cancel/restart/replace)" }), @@ -121,14 +105,7 @@ const Guide = Schema.Struct({ }) const ValidationProfile = Schema.optional(Schema.Literals(["portable", "environment"])).annotate({ description: - "portable: distributable-template checks; environment: additionally resolves prompts, workers, and models in this project. Defaults: builtin specs portable, inline and project/global specs environment", -}) -const ValidateInline = Schema.Struct({ - action: Schema.Literal("validate").annotate({ - description: "Pre-flight a custom spec without creating a workflow; returns diagnostics, never a workflow ID", - }), - spec: DagValidation.StartSpec.annotate({ description: specDescription }), - profile: ValidationProfile, + "portable: distributable-template checks; environment: additionally resolves prompts, workers, and models in this project. Defaults: builtin specs portable, project/global/path specs environment", }) const ValidatePath = Schema.Struct({ action: Schema.Literal("validate").annotate({ @@ -139,11 +116,8 @@ const ValidatePath = Schema.Struct({ }) export const Parameters = Schema.Union([ - StartInline, StartPath, - ExtendInline, ExtendPath, - ControlReplanInline, ControlReplanPath, ControlOther, Status, @@ -151,7 +125,6 @@ export const Parameters = Schema.Union([ List, Read, Guide, - ValidateInline, ValidatePath, ]) @@ -267,7 +240,7 @@ export const WorkflowTool = Tool.define< formatValidationError: (error) => [ `Workflow call rejected by the action schema: ${error instanceof Error ? error.message : String(error)}`, - "Each action owns only its own fields: start {spec | spec_path}; extend {workflow_id, spec | spec_path}; control {workflow_id, operation} plus spec/spec_path for replan; status {workflow_id}; result {workflow_id, node_id, cursor?, limit?}; list {}; read {spec_path}; guide {topic?}; validate {spec | spec_path, profile?}. Graph-carrying actions take exactly one source (spec or spec_path), and session/project identity is never a parameter.", + "Each action owns only its own fields: start {spec_path}; extend {workflow_id, spec_path}; control(replan) {workflow_id, operation, spec_path}; other control operations {workflow_id, operation}; status {workflow_id}; result {workflow_id, node_id, cursor?, limit?}; list {}; read {spec_path}; guide {topic?}; validate {spec_path, profile?}. Put graph content in a .yaml/.yml file; session/project identity is never a parameter.", ].join("\n"), execute: (params: Schema.Schema.Type, ctx: Tool.Context) => Effect.gen(function* () { @@ -372,21 +345,18 @@ export const WorkflowTool = Tool.define< } } case "validate": { - const loaded = - "spec" in params - ? { path: "", source: { kind: "inline" as const, value: params.spec } } - : yield* loadSpecFile(params.spec_path, callingSession.directory, ctx).pipe( - Effect.map((file) => ({ - path: file.path, - source: { kind: "yaml" as const, source: file.path, content: file.content }, - })), - Effect.catch((error: unknown) => - Effect.succeed({ - path: params.spec_path, - loadError: error instanceof Error ? error.message : String(error), - }), - ), - ) + const loaded = yield* loadSpecFile(params.spec_path, callingSession.directory, ctx).pipe( + Effect.map((file) => ({ + path: file.path, + source: { kind: "yaml" as const, source: file.path, content: file.content }, + })), + Effect.catch((error: unknown) => + Effect.succeed({ + path: params.spec_path, + loadError: error instanceof Error ? error.message : String(error), + }), + ), + ) const profile = params.profile ?? (DagWorkflows.isBuiltinPath(loaded.path) ? "portable" : "environment") const result = "loadError" in loaded @@ -537,11 +507,9 @@ export const WorkflowTool = Tool.define< } case "start": { const sessionID = SessionID.make(ctx.sessionID) - const source = yield* loadAuthoringSource( - "spec" in params ? { inline: params.spec } : { specPath: params.spec_path }, - callingSession.directory, - ctx, - ).pipe(Effect.orDie) + const source = yield* loadAuthoringSource(params.spec_path, callingSession.directory, ctx).pipe( + Effect.orDie, + ) const result = yield* authoring.prepare({ action: "start", source, @@ -612,11 +580,9 @@ export const WorkflowTool = Tool.define< const knownDependencies = (yield* dag.store.getNodes(params.workflow_id).pipe(Effect.orDie)).map( (node) => node.id, ) - const source = yield* loadAuthoringSource( - "spec" in params ? { inline: params.spec } : { specPath: params.spec_path }, - callingSession.directory, - ctx, - ).pipe(Effect.orDie) + const source = yield* loadAuthoringSource(params.spec_path, callingSession.directory, ctx).pipe( + Effect.orDie, + ) const result = yield* authoring.prepare({ action: "extend", source, @@ -642,11 +608,9 @@ export const WorkflowTool = Tool.define< if (params.operation === "replan") { const workflowDefaults = Dag.parseWorkflowConfig(workflow.config)?.node_defaults const knownDependencies = (yield* dag.store.getNodes(wfId).pipe(Effect.orDie)).map((node) => node.id) - const source = yield* loadAuthoringSource( - "spec" in params ? { inline: params.spec } : { specPath: params.spec_path }, - callingSession.directory, - ctx, - ).pipe(Effect.orDie) + const source = yield* loadAuthoringSource(params.spec_path, callingSession.directory, ctx).pipe( + Effect.orDie, + ) const result = yield* authoring.prepare({ action: "replan", source, @@ -783,12 +747,11 @@ function loadSpecFile(specPath: string, directory: string, ctx: Tool.Context) { } function loadAuthoringSource( - input: { inline: unknown; specPath?: never } | { inline?: never; specPath: string }, + specPath: string, directory: string, ctx: Tool.Context, ): Effect.Effect { - if ("inline" in input) return Effect.succeed({ kind: "inline" as const, value: input.inline }) - return loadSpecFile(input.specPath, directory, ctx).pipe( + return loadSpecFile(specPath, directory, ctx).pipe( Effect.map((file) => ({ kind: "yaml" as const, source: file.path, content: file.content })), ) } diff --git a/packages/opencode/test/dag/workflow-tool.test.ts b/packages/opencode/test/dag/workflow-tool.test.ts index c00efb1d2e..68f144bdbb 100644 --- a/packages/opencode/test/dag/workflow-tool.test.ts +++ b/packages/opencode/test/dag/workflow-tool.test.ts @@ -542,7 +542,7 @@ describe("workflow tool schema (negative tests)", () => { expect(() => decode({ action: "guide", topic: "blocks" })).not.toThrow() }) - it("retains an inline structured spec", () => { + it("rejects inline structured specs and JSON-stringified specs", () => { const decode = Schema.decodeUnknownSync(Parameters) const spec = { config: { @@ -551,7 +551,8 @@ describe("workflow tool schema (negative tests)", () => { }, } - expect(decode({ action: "start", spec })).toEqual({ action: "start", spec }) + expect(() => decode({ action: "start", spec })).toThrow() + expect(() => decode({ action: "start", spec: JSON.stringify(spec) })).toThrow() }) it("action field rejects unknown actions", () => { @@ -585,7 +586,7 @@ describe("workflow tool schema (negative tests)", () => { } }) - it("control replan requires exactly one graph source", () => { + it("control replan requires a YAML graph source", () => { const decode = Schema.decodeUnknownSync(Parameters, { onExcessProperty: "error" }) expect(() => decode({ action: "control", workflow_id: "dag_wf_1", operation: "replan", spec_path: "fragment.yaml" }), @@ -597,7 +598,7 @@ describe("workflow tool schema (negative tests)", () => { operation: "replan", spec: { fragment: { name: "fragment", nodes: [] } }, }), - ).not.toThrow() + ).toThrow() expect(() => decode({ action: "control", workflow_id: "dag_wf_1", operation: "replan" })).toThrow() }) @@ -607,9 +608,9 @@ describe("workflow tool schema (negative tests)", () => { expect(() => decode({ action: "control", workflow_id: "dag_wf_1", operation: "start" })).toThrow() }) - it("keeps workflow graph and admission fields inside spec", () => { - const decode = Schema.decodeUnknownSync(Parameters) - expect( + it("keeps workflow graph and admission fields inside the YAML file", () => { + const decode = Schema.decodeUnknownSync(Parameters, { onExcessProperty: "error" }) + expect(() => decode({ action: "start", spec_path: ".opencode/workflows/deep.yaml", @@ -620,7 +621,8 @@ describe("workflow tool schema (negative tests)", () => { nodes: [], }, }), - ).toEqual({ + ).toThrow() + expect(decode({ action: "start", spec_path: ".opencode/workflows/deep.yaml" })).toEqual({ action: "start", spec_path: ".opencode/workflows/deep.yaml", }) @@ -841,7 +843,7 @@ describe("workflow tool execution", () => { ) const extendExit = yield* Effect.exit( workflow.execute( - { action: "extend", workflow_id: Dag.ID.make("dag_defaults"), spec: { nodes: [] } }, + { action: "extend", workflow_id: Dag.ID.make("dag_defaults"), spec_path: "foreign.yaml" }, foreignContext, ), ) @@ -908,25 +910,26 @@ describe("workflow tool execution", () => { }), ) - runtime.effect("starts from an inline structured spec without a file", () => + runtime.effect("starts from a YAML workflow file", () => Effect.gen(function* () { published.length = 0 const info = yield* WorkflowTool const workflow = yield* info.init() + const spec_path = yield* writeWorkflowSpec("file-start", { + config: { + name: "file-start", + nodes: [], + }, + }) const result = yield* workflow.execute( Schema.decodeUnknownSync(Parameters)({ action: "start", - spec: { - config: { - name: "inline-start", - nodes: [], - }, - }, + spec_path, }), toolContext(), ) - expect(result.title).toBe("Workflow started: inline-start") + expect(result.title).toBe("Workflow started: file-start") expect(result.metadata.workflowId).toBeDefined() expect(published.some((event) => event.type === DagEvent.WorkflowCreated.type)).toBe(true) }), @@ -937,20 +940,21 @@ describe("workflow tool execution", () => { published.length = 0 const info = yield* WorkflowTool const workflow = yield* info.init() + const spec_path = yield* writeWorkflowSpec("block-start", { + config: { + name: "block-start", + objective: "Implement and review session recovery", + blocks: [ + { id: "build", kind: "coding" }, + { id: "verify", kind: "verify", depends_on: ["build"] }, + { id: "review", kind: "review", depends_on: ["verify"] }, + ], + }, + }) const result = yield* workflow.execute( Schema.decodeUnknownSync(Parameters)({ action: "start", - spec: { - config: { - name: "block-start", - objective: "Implement and review session recovery", - blocks: [ - { id: "build", kind: "coding" }, - { id: "verify", kind: "verify", depends_on: ["build"] }, - { id: "review", kind: "review", depends_on: ["verify"] }, - ], - }, - }, + spec_path, }), toolContext(), ) @@ -973,33 +977,34 @@ describe("workflow tool execution", () => { }), ) - runtime.effect("extends from an inline structured spec without a file", () => + runtime.effect("extends from a YAML workflow file", () => Effect.gen(function* () { published.length = 0 const info = yield* WorkflowTool const workflow = yield* info.init() + const spec_path = yield* writeWorkflowSpec("file-extend", { + nodes: [ + { + id: "file-added", + name: "File added", + worker_type: "general", + depends_on: [], + prompt_template: { inline: "work" }, + }, + ], + }) const result = yield* workflow.execute( Schema.decodeUnknownSync(Parameters)({ action: "extend", workflow_id: "dag_defaults", - spec: { - nodes: [ - { - id: "inline-added", - name: "Inline added", - worker_type: "general", - depends_on: [], - prompt_template: { inline: "work" }, - }, - ], - }, + spec_path, }), toolContext(), ) expect(result.title).toBe("Workflow extended: 1 nodes added") expect(published.find((event) => event.type === DagEvent.NodeRegistered.type)?.data).toEqual( - expect.objectContaining({ nodeID: "inline-added" }), + expect.objectContaining({ nodeID: "file-added" }), ) }), ) @@ -1009,14 +1014,15 @@ describe("workflow tool execution", () => { published.length = 0 const info = yield* WorkflowTool const workflow = yield* info.init() + const spec_path = yield* writeWorkflowSpec("block-extend", { + objective: "Repair from the current diagnostic evidence", + blocks: [{ id: "repair", kind: "coding", depends_on: ["node_running"] }], + }) const result = yield* workflow.execute( Schema.decodeUnknownSync(Parameters)({ action: "extend", workflow_id: "dag_status", - spec: { - objective: "Repair from the current diagnostic evidence", - blocks: [{ id: "repair", kind: "coding", depends_on: ["node_running"] }], - }, + spec_path, }), toolContext(), ) @@ -1028,42 +1034,43 @@ describe("workflow tool execution", () => { }), ) - runtime.effect("replans from an inline structured spec without a file", () => + runtime.effect("replans from a YAML workflow file", () => Effect.gen(function* () { published.length = 0 const info = yield* WorkflowTool const workflow = yield* info.init() + const spec_path = yield* writeWorkflowSpec("file-replan", { + fragment: { + name: "file-replan", + nodes: [ + { + id: "file-replanned", + name: "File replanned", + worker_type: "general", + depends_on: [], + prompt_template: { inline: "work" }, + }, + ], + }, + }) const result = yield* workflow.execute( Schema.decodeUnknownSync(Parameters)({ action: "control", workflow_id: "dag_defaults", operation: "replan", - spec: { - fragment: { - name: "inline-replan", - nodes: [ - { - id: "inline-replanned", - name: "Inline replanned", - worker_type: "general", - depends_on: [], - prompt_template: { inline: "work" }, - }, - ], - }, - }, + spec_path, }), toolContext(), ) expect(result.title).toContain("Workflow replanned: +1") expect(published.find((event) => event.type === DagEvent.NodeRegistered.type)?.data).toEqual( - expect.objectContaining({ nodeID: "inline-replanned" }), + expect.objectContaining({ nodeID: "file-replanned" }), ) }), ) - runtime.effect("rejects ambiguous or missing spec sources before side effects", () => + runtime.effect("rejects inline or missing spec sources before side effects", () => Effect.gen(function* () { const info = yield* WorkflowTool const workflow = yield* info.init() @@ -1093,10 +1100,10 @@ describe("workflow tool execution", () => { expect(published).toHaveLength(0) } - // The recovery guidance names both valid source variants. + // Recovery guidance tells the model to move graph content into YAML. const guidance = workflow.formatValidationError?.(new Error("no branch matched")) ?? "" - expect(guidance).toContain("exactly one source") - expect(guidance).toContain("spec or spec_path") + expect(guidance).toContain("start {spec_path}") + expect(guidance).toContain("Put graph content in a .yaml/.yml file") }), ) @@ -1419,9 +1426,15 @@ config: depends_on: [], prompt_template: { inline: "work" }, } + const extendPath = path.join(missingModelDirectory, "unresolved-extend.yaml") + const replanPath = path.join(missingModelDirectory, "unresolved-replan.yaml") + yield* Effect.promise(() => Bun.write(extendPath, JSON.stringify({ nodes: [node] }))) + yield* Effect.promise(() => + Bun.write(replanPath, JSON.stringify({ fragment: { name: "unresolved-replan", nodes: [node] } })), + ) const extendExit = yield* workflow - .execute({ action: "extend", workflow_id: Dag.ID.make("dag_paused"), spec: { nodes: [node] } }, toolContext()) + .execute({ action: "extend", workflow_id: Dag.ID.make("dag_paused"), spec_path: extendPath }, toolContext()) .pipe(Effect.exit) const replanExit = yield* workflow .execute( @@ -1429,7 +1442,7 @@ config: action: "control", operation: "replan", workflow_id: Dag.ID.make("dag_paused"), - spec: { fragment: { name: "unresolved-replan", nodes: [node] } }, + spec_path: replanPath, }, toolContext(), ) @@ -1456,9 +1469,11 @@ config: depends_on: [], prompt_template: { inline: "work" }, } + const extendPath = path.join(missingModelDirectory, "modeled-extend.yaml") + yield* Effect.promise(() => Bun.write(extendPath, JSON.stringify({ nodes: [node] }))) const extended = yield* workflow.execute( - { action: "extend", workflow_id: Dag.ID.make("dag_defaults"), spec: { nodes: [node] } }, + { action: "extend", workflow_id: Dag.ID.make("dag_defaults"), spec_path: extendPath }, toolContext(), ) const replanned = yield* workflow.execute( @@ -1915,20 +1930,7 @@ config: decode({ action: "start", session_id: "ses_other_parent", - spec: { - config: { - name: "foreign-parent", - nodes: [ - { - id: "work", - name: "work", - worker_type: "build", - depends_on: [], - prompt_template: { inline: "work" }, - }, - ], - }, - }, + spec_path: "foreign-parent.yaml", }), ).toThrow() }), @@ -2133,12 +2135,15 @@ describe("workflow tool saved workflows", () => { environmentSkillListCalls = 0 const info = yield* WorkflowTool const workflow = yield* info.init() + const spec_path = yield* writeWorkflowSpec("portable-file", { + config: { name: "portable-file", nodes: [] }, + }) const result = yield* workflow.execute( { action: "validate", profile: "portable", - spec: { config: { name: "portable-inline", nodes: [] } }, + spec_path, }, toolContext(), ) @@ -2157,32 +2162,33 @@ describe("workflow tool saved workflows", () => { environmentProviderGetModelCalls = 0 const info = yield* WorkflowTool const workflow = yield* info.init() + const spec_path = yield* writeWorkflowSpec("catalog-snapshot", { + config: { + name: "catalog-snapshot", + nodes: [ + { + id: "first", + name: "First", + worker_type: "build", + depends_on: [], + prompt_template: { inline: "First" }, + }, + { + id: "second", + name: "Second", + worker_type: "build", + depends_on: ["first"], + prompt_template: { inline: "Second" }, + }, + ], + }, + }) const result = yield* workflow.execute( { action: "validate", profile: "environment", - spec: { - config: { - name: "catalog-snapshot", - nodes: [ - { - id: "first", - name: "First", - worker_type: "build", - depends_on: [], - prompt_template: { inline: "First" }, - }, - { - id: "second", - name: "Second", - worker_type: "build", - depends_on: ["first"], - prompt_template: { inline: "Second" }, - }, - ], - }, - }, + spec_path, }, toolContext(), ) @@ -2200,7 +2206,7 @@ describe("workflow tool saved workflows", () => { Effect.gen(function* () { published.length = 0 // project scope shadows global for the same name; builtin fills the - // gap a file scope does not own; inline stays session-local. + // gap a file scope does not own; an explicit path stays session-local. const routeSpec = (name: string) => `title: ${name} title\nconfig:\n name: ${name}\n objective: Route objective\n blocks:\n - id: plan\n kind: plan\n` yield* Effect.promise(() => @@ -2211,6 +2217,7 @@ describe("workflow tool saved workflows", () => { path.join(workflowSpecDirectory, ".opencode", "workflows", "shared-route.yaml"), routeSpec("project-route"), ), + Bun.write(path.join(workflowSpecDirectory, "path-route.yaml"), routeSpec("path-route")), ]), ) const previousBuiltin = (globalThis as Record).OPENCODE_DAG_TEMPLATES @@ -2240,24 +2247,18 @@ describe("workflow tool saved workflows", () => { expect(builtinResult.profile).toBe("portable") expect(builtinResult.valid).toBe(true) - // inline source validates under the environment profile by default - const inlineValidate = yield* workflow.execute( + // explicit path source validates under the environment profile by default + const pathValidate = yield* workflow.execute( { action: "validate", - spec: { - config: { - name: "inline-route", - objective: "Inline objective", - blocks: [{ id: "plan", kind: "plan" }], - }, - }, + spec_path: "path-route.yaml", }, contextWith([]), ) - const inlineResult = JSON.parse(inlineValidate.output) - expect(inlineResult.source).toBe("") - expect(inlineResult.profile).toBe("environment") - expect(inlineResult.valid).toBe(true) + const pathResult = JSON.parse(pathValidate.output) + expect(pathResult.source).toBe(path.join(workflowSpecDirectory, "path-route.yaml")) + expect(pathResult.profile).toBe("environment") + expect(pathResult.valid).toBe(true) // validate and start see the same resolved content: validate passes, // start succeeds from the same name, and mutating the file changes diff --git a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap index 772564d18e..3dffba21cf 100644 --- a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap +++ b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap @@ -448,454 +448,6 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` { "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ - { - "properties": { - "action": { - "description": "Create a workflow", - "enum": [ - "start", - ], - "type": "string", - }, - "spec": { - "description": "Inline structured spec for a one-off graph. Use this or spec_path, never both", - "properties": { - "admission": { - "properties": { - "acknowledged_risks": { - "items": { - "type": "string", - }, - "type": "array", - }, - "brief": { - "properties": { - "acceptance_criteria": { - "items": { - "type": "string", - }, - "type": "array", - }, - "assumptions": { - "items": { - "type": "string", - }, - "type": "array", - }, - "blocking_questions": { - "items": { - "type": "string", - }, - "type": "array", - }, - "constraints": { - "items": { - "type": "string", - }, - "type": "array", - }, - "evidence_required": { - "items": { - "type": "string", - }, - "type": "array", - }, - "goal": { - "type": "string", - }, - "open_questions": { - "items": { - "type": "string", - }, - "type": "array", - }, - "review_plan": { - "items": { - "type": "string", - }, - "type": "array", - }, - "risks": { - "items": { - "type": "string", - }, - "type": "array", - }, - "scope": { - "properties": { - "in": { - "items": { - "type": "string", - }, - "type": "array", - }, - "out": { - "items": { - "type": "string", - }, - "type": "array", - }, - }, - "required": [ - "in", - "out", - ], - "type": "object", - }, - }, - "required": [ - "goal", - "scope", - "constraints", - "assumptions", - "acceptance_criteria", - "evidence_required", - "risks", - "review_plan", - "open_questions", - "blocking_questions", - ], - "type": "object", - }, - "brief_revision": { - "type": "number", - }, - "qa_mode": { - "enum": [ - "LIGHT", - "STANDARD", - "GRILL", - ], - "type": "string", - }, - "verdict": { - "enum": [ - "READY", - "NOT_READY", - "WAIVED", - ], - "type": "string", - }, - "waiver_reason": { - "type": "string", - }, - }, - "required": [ - "brief_revision", - "qa_mode", - "verdict", - "brief", - ], - "type": "object", - }, - "config": { - "anyOf": [ - { - "properties": { - "blocks": { - "description": "Composable blocks compiled into nodes by the runtime", - "items": { - "properties": { - "depends_on": { - "description": "Block IDs this block waits for. Defaults to []", - "items": { - "type": "string", - }, - "type": "array", - }, - "id": { - "description": "Unique block identifier; dependencies target block IDs", - "type": "string", - }, - "instruction": { - "description": "Task-specific instruction added to the block's built-in execution contract", - "type": "string", - }, - "kind": { - "description": "Composable workflow block; debug and review expand into evidence-gathering subgraphs", - "enum": [ - "explore", - "plan", - "prototype", - "debug", - "coding", - "verify", - "review", - "synthesize", - ], - "type": "string", - }, - "report_to_parent": { - "description": "Override wake behavior. Review decisions and synthesis report by default", - "type": "boolean", - }, - "required": { - "description": "Whether failure is terminal. Decision and verification blocks default to true; volume blocks to false", - "type": "boolean", - }, - "worker_type": { - "description": "Optional configured agent override; defaults from the block kind", - "type": "string", - }, - }, - "required": [ - "id", - "kind", - ], - "type": "object", - }, - "type": "array", - }, - "max_concurrency": { - "description": "Max parallel nodes. Default: 5", - "type": "number", - }, - "max_node_replan_attempts": { - "description": "Max replan restarts per node ID. Default: 5", - "type": "number", - }, - "max_total_nodes": { - "description": "Cumulative node cap across the workflow lifetime. Default: 100", - "type": "number", - }, - "name": { - "description": "Workflow name", - "type": "string", - }, - "node_defaults": { - "description": "Defaults inherited by nodes that omit required, worker_config, or report_to_parent", - "properties": { - "report_to_parent": { - "type": "boolean", - }, - "required": { - "type": "boolean", - }, - "worker_config": { - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - }, - "type": "object", - }, - "objective": { - "description": "Injected into every generated child prompt; required for blocks", - "type": "string", - }, - }, - "required": [ - "name", - "objective", - "blocks", - ], - "type": "object", - }, - { - "properties": { - "max_concurrency": { - "description": "Max parallel nodes. Default: 5", - "type": "number", - }, - "max_node_replan_attempts": { - "description": "Max replan restarts per node ID. Default: 5", - "type": "number", - }, - "max_total_nodes": { - "description": "Cumulative node cap across the workflow lifetime. Default: 100", - "type": "number", - }, - "name": { - "description": "Workflow name", - "type": "string", - }, - "node_defaults": { - "description": "Defaults inherited by nodes that omit required, worker_config, or report_to_parent", - "properties": { - "report_to_parent": { - "type": "boolean", - }, - "required": { - "type": "boolean", - }, - "worker_config": { - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - }, - "type": "object", - }, - "nodes": { - "description": "Low-level node declarations", - "items": { - "properties": { - "cancel": { - "description": "(replan only) Cancel this node", - "type": "boolean", - }, - "condition": { - "description": "Expression evaluated before spawn; node is skipped if false", - "type": "string", - }, - "depends_on": { - "description": "Node IDs this node waits for ([] for root)", - "items": { - "type": "string", - }, - "type": "array", - }, - "id": { - "description": "Unique node identifier, used in depends_on", - "type": "string", - }, - "input_mapping": { - "additionalProperties": { - "type": "string", - }, - "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", - "type": "object", - }, - "name": { - "description": "Human-readable node name", - "type": "string", - }, - "output_schema": { - "description": "JSON Schema; child agent must call submit_result to submit structured output", - "type": "object", - }, - "prompt_template": { - "anyOf": [ - { - "properties": { - "inline": { - "description": "Inline prompt text; bind {{placeholders}} via input or input_mapping", - "type": "string", - }, - "input": { - "type": "object", - }, - }, - "required": [ - "inline", - ], - "type": "object", - }, - { - "properties": { - "id": { - "description": "Prompt asset id resolved from .opencode/dag-prompts (project, then global)", - "type": "string", - }, - "input": { - "type": "object", - }, - }, - "required": [ - "id", - ], - "type": "object", - }, - ], - "description": "Template: exactly one of { id: "..." } or { inline: "...", input: {...} }. Direct dependency outputs are available as {{node-id}} by default", - }, - "report_to_parent": { - "description": "If true, the parent agent is woken when this node completes or fails. Inherits config.node_defaults.report_to_parent", - "type": "boolean", - }, - "required": { - "description": "If true and this node fails, the workflow terminalizes as failed. Inherits config.node_defaults.required", - "type": "boolean", - }, - "restart": { - "description": "(replan only) Re-spawn this running node with new prompt. Running nodes only — terminal (completed/failed/skipped) nodes are immutable; to retry a failed node, add a replacement node under a new id", - "type": "boolean", - }, - "review": { - "description": "(deep review workers) design reviews pre-implementation artifacts; diff reviews require implementation_node_id and verification_node_id", - "properties": { - "implementation_node_id": { - "type": "string", - }, - "phase": { - "enum": [ - "design", - "diff", - ], - "type": "string", - }, - "verification_node_id": { - "type": "string", - }, - }, - "required": [ - "phase", - ], - "type": "object", - }, - "worker_config": { - "description": "{ timeout_ms } — bounds node execution. Inherits config.node_defaults.worker_config", - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - "worker_type": { - "description": "Agent type (explore, build, general, plan, or custom)", - "type": "string", - }, - }, - "required": [ - "id", - "name", - "worker_type", - "depends_on", - "prompt_template", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": [ - "name", - "nodes", - ], - "type": "object", - }, - ], - }, - "mode": { - "enum": [ - "standard", - "deep", - ], - "type": "string", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "config", - ], - "type": "object", - }, - }, - "required": [ - "action", - "spec", - ], - "type": "object", - }, { "properties": { "action": { @@ -906,7 +458,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, }, @@ -916,238 +468,6 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` ], "type": "object", }, - { - "properties": { - "action": { - "description": "Add nodes or blocks to a live workflow", - "enum": [ - "extend", - ], - "type": "string", - }, - "spec": { - "anyOf": [ - { - "properties": { - "blocks": { - "items": { - "properties": { - "depends_on": { - "description": "Block IDs this block waits for. Defaults to []", - "items": { - "type": "string", - }, - "type": "array", - }, - "id": { - "description": "Unique block identifier; dependencies target block IDs", - "type": "string", - }, - "instruction": { - "description": "Task-specific instruction added to the block's built-in execution contract", - "type": "string", - }, - "kind": { - "description": "Composable workflow block; debug and review expand into evidence-gathering subgraphs", - "enum": [ - "explore", - "plan", - "prototype", - "debug", - "coding", - "verify", - "review", - "synthesize", - ], - "type": "string", - }, - "report_to_parent": { - "description": "Override wake behavior. Review decisions and synthesis report by default", - "type": "boolean", - }, - "required": { - "description": "Whether failure is terminal. Decision and verification blocks default to true; volume blocks to false", - "type": "boolean", - }, - "worker_type": { - "description": "Optional configured agent override; defaults from the block kind", - "type": "string", - }, - }, - "required": [ - "id", - "kind", - ], - "type": "object", - }, - "type": "array", - }, - "objective": { - "description": "Injected into every generated child prompt", - "type": "string", - }, - }, - "required": [ - "objective", - "blocks", - ], - "type": "object", - }, - { - "properties": { - "nodes": { - "items": { - "properties": { - "cancel": { - "description": "(replan only) Cancel this node", - "type": "boolean", - }, - "condition": { - "description": "Expression evaluated before spawn; node is skipped if false", - "type": "string", - }, - "depends_on": { - "description": "Node IDs this node waits for ([] for root)", - "items": { - "type": "string", - }, - "type": "array", - }, - "id": { - "description": "Unique node identifier, used in depends_on", - "type": "string", - }, - "input_mapping": { - "additionalProperties": { - "type": "string", - }, - "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", - "type": "object", - }, - "name": { - "description": "Human-readable node name", - "type": "string", - }, - "output_schema": { - "description": "JSON Schema; child agent must call submit_result to submit structured output", - "type": "object", - }, - "prompt_template": { - "anyOf": [ - { - "properties": { - "inline": { - "description": "Inline prompt text; bind {{placeholders}} via input or input_mapping", - "type": "string", - }, - "input": { - "type": "object", - }, - }, - "required": [ - "inline", - ], - "type": "object", - }, - { - "properties": { - "id": { - "description": "Prompt asset id resolved from .opencode/dag-prompts (project, then global)", - "type": "string", - }, - "input": { - "type": "object", - }, - }, - "required": [ - "id", - ], - "type": "object", - }, - ], - "description": "Template: exactly one of { id: "..." } or { inline: "...", input: {...} }. Direct dependency outputs are available as {{node-id}} by default", - }, - "report_to_parent": { - "description": "If true, the parent agent is woken when this node completes or fails. Inherits config.node_defaults.report_to_parent", - "type": "boolean", - }, - "required": { - "description": "If true and this node fails, the workflow terminalizes as failed. Inherits config.node_defaults.required", - "type": "boolean", - }, - "restart": { - "description": "(replan only) Re-spawn this running node with new prompt. Running nodes only — terminal (completed/failed/skipped) nodes are immutable; to retry a failed node, add a replacement node under a new id", - "type": "boolean", - }, - "review": { - "description": "(deep review workers) design reviews pre-implementation artifacts; diff reviews require implementation_node_id and verification_node_id", - "properties": { - "implementation_node_id": { - "type": "string", - }, - "phase": { - "enum": [ - "design", - "diff", - ], - "type": "string", - }, - "verification_node_id": { - "type": "string", - }, - }, - "required": [ - "phase", - ], - "type": "object", - }, - "worker_config": { - "description": "{ timeout_ms } — bounds node execution. Inherits config.node_defaults.worker_config", - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - "worker_type": { - "description": "Agent type (explore, build, general, plan, or custom)", - "type": "string", - }, - }, - "required": [ - "id", - "name", - "worker_type", - "depends_on", - "prompt_template", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": [ - "nodes", - ], - "type": "object", - }, - ], - "description": "Inline structured spec for a one-off graph. Use this or spec_path, never both", - }, - "workflow_id": { - "description": "Target workflow ID", - "pattern": "^dag", - "type": "string", - }, - }, - "required": [ - "action", - "workflow_id", - "spec", - ], - "type": "object", - }, { "properties": { "action": { @@ -1158,7 +478,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, "workflow_id": { @@ -1174,330 +494,6 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` ], "type": "object", }, - { - "properties": { - "action": { - "description": "Control a live workflow", - "enum": [ - "control", - ], - "type": "string", - }, - "operation": { - "description": "Apply a node fragment (add/cancel/restart/replace)", - "enum": [ - "replan", - ], - "type": "string", - }, - "spec": { - "description": "Inline structured spec for a one-off graph. Use this or spec_path, never both", - "properties": { - "fragment": { - "anyOf": [ - { - "properties": { - "blocks": { - "description": "Composable blocks compiled into nodes by the runtime", - "items": { - "properties": { - "depends_on": { - "description": "Block IDs this block waits for. Defaults to []", - "items": { - "type": "string", - }, - "type": "array", - }, - "id": { - "description": "Unique block identifier; dependencies target block IDs", - "type": "string", - }, - "instruction": { - "description": "Task-specific instruction added to the block's built-in execution contract", - "type": "string", - }, - "kind": { - "description": "Composable workflow block; debug and review expand into evidence-gathering subgraphs", - "enum": [ - "explore", - "plan", - "prototype", - "debug", - "coding", - "verify", - "review", - "synthesize", - ], - "type": "string", - }, - "report_to_parent": { - "description": "Override wake behavior. Review decisions and synthesis report by default", - "type": "boolean", - }, - "required": { - "description": "Whether failure is terminal. Decision and verification blocks default to true; volume blocks to false", - "type": "boolean", - }, - "worker_type": { - "description": "Optional configured agent override; defaults from the block kind", - "type": "string", - }, - }, - "required": [ - "id", - "kind", - ], - "type": "object", - }, - "type": "array", - }, - "max_concurrency": { - "description": "Max parallel nodes. Default: 5", - "type": "number", - }, - "max_node_replan_attempts": { - "description": "Max replan restarts per node ID. Default: 5", - "type": "number", - }, - "max_total_nodes": { - "description": "Cumulative node cap across the workflow lifetime. Default: 100", - "type": "number", - }, - "name": { - "description": "Workflow name", - "type": "string", - }, - "node_defaults": { - "description": "Defaults inherited by nodes that omit required, worker_config, or report_to_parent", - "properties": { - "report_to_parent": { - "type": "boolean", - }, - "required": { - "type": "boolean", - }, - "worker_config": { - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - }, - "type": "object", - }, - "objective": { - "description": "Injected into every generated child prompt; required for blocks", - "type": "string", - }, - }, - "required": [ - "name", - "objective", - "blocks", - ], - "type": "object", - }, - { - "properties": { - "max_concurrency": { - "description": "Max parallel nodes. Default: 5", - "type": "number", - }, - "max_node_replan_attempts": { - "description": "Max replan restarts per node ID. Default: 5", - "type": "number", - }, - "max_total_nodes": { - "description": "Cumulative node cap across the workflow lifetime. Default: 100", - "type": "number", - }, - "name": { - "description": "Workflow name", - "type": "string", - }, - "node_defaults": { - "description": "Defaults inherited by nodes that omit required, worker_config, or report_to_parent", - "properties": { - "report_to_parent": { - "type": "boolean", - }, - "required": { - "type": "boolean", - }, - "worker_config": { - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - }, - "type": "object", - }, - "nodes": { - "description": "Low-level node declarations", - "items": { - "properties": { - "cancel": { - "description": "(replan only) Cancel this node", - "type": "boolean", - }, - "condition": { - "description": "Expression evaluated before spawn; node is skipped if false", - "type": "string", - }, - "depends_on": { - "description": "Node IDs this node waits for ([] for root)", - "items": { - "type": "string", - }, - "type": "array", - }, - "id": { - "description": "Unique node identifier, used in depends_on", - "type": "string", - }, - "input_mapping": { - "additionalProperties": { - "type": "string", - }, - "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", - "type": "object", - }, - "name": { - "description": "Human-readable node name", - "type": "string", - }, - "output_schema": { - "description": "JSON Schema; child agent must call submit_result to submit structured output", - "type": "object", - }, - "prompt_template": { - "anyOf": [ - { - "properties": { - "inline": { - "description": "Inline prompt text; bind {{placeholders}} via input or input_mapping", - "type": "string", - }, - "input": { - "type": "object", - }, - }, - "required": [ - "inline", - ], - "type": "object", - }, - { - "properties": { - "id": { - "description": "Prompt asset id resolved from .opencode/dag-prompts (project, then global)", - "type": "string", - }, - "input": { - "type": "object", - }, - }, - "required": [ - "id", - ], - "type": "object", - }, - ], - "description": "Template: exactly one of { id: "..." } or { inline: "...", input: {...} }. Direct dependency outputs are available as {{node-id}} by default", - }, - "report_to_parent": { - "description": "If true, the parent agent is woken when this node completes or fails. Inherits config.node_defaults.report_to_parent", - "type": "boolean", - }, - "required": { - "description": "If true and this node fails, the workflow terminalizes as failed. Inherits config.node_defaults.required", - "type": "boolean", - }, - "restart": { - "description": "(replan only) Re-spawn this running node with new prompt. Running nodes only — terminal (completed/failed/skipped) nodes are immutable; to retry a failed node, add a replacement node under a new id", - "type": "boolean", - }, - "review": { - "description": "(deep review workers) design reviews pre-implementation artifacts; diff reviews require implementation_node_id and verification_node_id", - "properties": { - "implementation_node_id": { - "type": "string", - }, - "phase": { - "enum": [ - "design", - "diff", - ], - "type": "string", - }, - "verification_node_id": { - "type": "string", - }, - }, - "required": [ - "phase", - ], - "type": "object", - }, - "worker_config": { - "description": "{ timeout_ms } — bounds node execution. Inherits config.node_defaults.worker_config", - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - "worker_type": { - "description": "Agent type (explore, build, general, plan, or custom)", - "type": "string", - }, - }, - "required": [ - "id", - "name", - "worker_type", - "depends_on", - "prompt_template", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": [ - "name", - "nodes", - ], - "type": "object", - }, - ], - }, - }, - "required": [ - "fragment", - ], - "type": "object", - }, - "workflow_id": { - "description": "Target workflow ID", - "pattern": "^dag", - "type": "string", - }, - }, - "required": [ - "action", - "operation", - "workflow_id", - "spec", - ], - "type": "object", - }, { "properties": { "action": { @@ -1515,7 +511,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, "workflow_id": { @@ -1647,7 +643,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, }, @@ -1692,463 +688,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "profile": { - "description": "portable: distributable-template checks; environment: additionally resolves prompts, workers, and models in this project. Defaults: builtin specs portable, inline and project/global specs environment", - "enum": [ - "portable", - "environment", - ], - "type": "string", - }, - "spec": { - "description": "Inline structured spec for a one-off graph. Use this or spec_path, never both", - "properties": { - "admission": { - "properties": { - "acknowledged_risks": { - "items": { - "type": "string", - }, - "type": "array", - }, - "brief": { - "properties": { - "acceptance_criteria": { - "items": { - "type": "string", - }, - "type": "array", - }, - "assumptions": { - "items": { - "type": "string", - }, - "type": "array", - }, - "blocking_questions": { - "items": { - "type": "string", - }, - "type": "array", - }, - "constraints": { - "items": { - "type": "string", - }, - "type": "array", - }, - "evidence_required": { - "items": { - "type": "string", - }, - "type": "array", - }, - "goal": { - "type": "string", - }, - "open_questions": { - "items": { - "type": "string", - }, - "type": "array", - }, - "review_plan": { - "items": { - "type": "string", - }, - "type": "array", - }, - "risks": { - "items": { - "type": "string", - }, - "type": "array", - }, - "scope": { - "properties": { - "in": { - "items": { - "type": "string", - }, - "type": "array", - }, - "out": { - "items": { - "type": "string", - }, - "type": "array", - }, - }, - "required": [ - "in", - "out", - ], - "type": "object", - }, - }, - "required": [ - "goal", - "scope", - "constraints", - "assumptions", - "acceptance_criteria", - "evidence_required", - "risks", - "review_plan", - "open_questions", - "blocking_questions", - ], - "type": "object", - }, - "brief_revision": { - "type": "number", - }, - "qa_mode": { - "enum": [ - "LIGHT", - "STANDARD", - "GRILL", - ], - "type": "string", - }, - "verdict": { - "enum": [ - "READY", - "NOT_READY", - "WAIVED", - ], - "type": "string", - }, - "waiver_reason": { - "type": "string", - }, - }, - "required": [ - "brief_revision", - "qa_mode", - "verdict", - "brief", - ], - "type": "object", - }, - "config": { - "anyOf": [ - { - "properties": { - "blocks": { - "description": "Composable blocks compiled into nodes by the runtime", - "items": { - "properties": { - "depends_on": { - "description": "Block IDs this block waits for. Defaults to []", - "items": { - "type": "string", - }, - "type": "array", - }, - "id": { - "description": "Unique block identifier; dependencies target block IDs", - "type": "string", - }, - "instruction": { - "description": "Task-specific instruction added to the block's built-in execution contract", - "type": "string", - }, - "kind": { - "description": "Composable workflow block; debug and review expand into evidence-gathering subgraphs", - "enum": [ - "explore", - "plan", - "prototype", - "debug", - "coding", - "verify", - "review", - "synthesize", - ], - "type": "string", - }, - "report_to_parent": { - "description": "Override wake behavior. Review decisions and synthesis report by default", - "type": "boolean", - }, - "required": { - "description": "Whether failure is terminal. Decision and verification blocks default to true; volume blocks to false", - "type": "boolean", - }, - "worker_type": { - "description": "Optional configured agent override; defaults from the block kind", - "type": "string", - }, - }, - "required": [ - "id", - "kind", - ], - "type": "object", - }, - "type": "array", - }, - "max_concurrency": { - "description": "Max parallel nodes. Default: 5", - "type": "number", - }, - "max_node_replan_attempts": { - "description": "Max replan restarts per node ID. Default: 5", - "type": "number", - }, - "max_total_nodes": { - "description": "Cumulative node cap across the workflow lifetime. Default: 100", - "type": "number", - }, - "name": { - "description": "Workflow name", - "type": "string", - }, - "node_defaults": { - "description": "Defaults inherited by nodes that omit required, worker_config, or report_to_parent", - "properties": { - "report_to_parent": { - "type": "boolean", - }, - "required": { - "type": "boolean", - }, - "worker_config": { - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - }, - "type": "object", - }, - "objective": { - "description": "Injected into every generated child prompt; required for blocks", - "type": "string", - }, - }, - "required": [ - "name", - "objective", - "blocks", - ], - "type": "object", - }, - { - "properties": { - "max_concurrency": { - "description": "Max parallel nodes. Default: 5", - "type": "number", - }, - "max_node_replan_attempts": { - "description": "Max replan restarts per node ID. Default: 5", - "type": "number", - }, - "max_total_nodes": { - "description": "Cumulative node cap across the workflow lifetime. Default: 100", - "type": "number", - }, - "name": { - "description": "Workflow name", - "type": "string", - }, - "node_defaults": { - "description": "Defaults inherited by nodes that omit required, worker_config, or report_to_parent", - "properties": { - "report_to_parent": { - "type": "boolean", - }, - "required": { - "type": "boolean", - }, - "worker_config": { - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - }, - "type": "object", - }, - "nodes": { - "description": "Low-level node declarations", - "items": { - "properties": { - "cancel": { - "description": "(replan only) Cancel this node", - "type": "boolean", - }, - "condition": { - "description": "Expression evaluated before spawn; node is skipped if false", - "type": "string", - }, - "depends_on": { - "description": "Node IDs this node waits for ([] for root)", - "items": { - "type": "string", - }, - "type": "array", - }, - "id": { - "description": "Unique node identifier, used in depends_on", - "type": "string", - }, - "input_mapping": { - "additionalProperties": { - "type": "string", - }, - "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", - "type": "object", - }, - "name": { - "description": "Human-readable node name", - "type": "string", - }, - "output_schema": { - "description": "JSON Schema; child agent must call submit_result to submit structured output", - "type": "object", - }, - "prompt_template": { - "anyOf": [ - { - "properties": { - "inline": { - "description": "Inline prompt text; bind {{placeholders}} via input or input_mapping", - "type": "string", - }, - "input": { - "type": "object", - }, - }, - "required": [ - "inline", - ], - "type": "object", - }, - { - "properties": { - "id": { - "description": "Prompt asset id resolved from .opencode/dag-prompts (project, then global)", - "type": "string", - }, - "input": { - "type": "object", - }, - }, - "required": [ - "id", - ], - "type": "object", - }, - ], - "description": "Template: exactly one of { id: "..." } or { inline: "...", input: {...} }. Direct dependency outputs are available as {{node-id}} by default", - }, - "report_to_parent": { - "description": "If true, the parent agent is woken when this node completes or fails. Inherits config.node_defaults.report_to_parent", - "type": "boolean", - }, - "required": { - "description": "If true and this node fails, the workflow terminalizes as failed. Inherits config.node_defaults.required", - "type": "boolean", - }, - "restart": { - "description": "(replan only) Re-spawn this running node with new prompt. Running nodes only — terminal (completed/failed/skipped) nodes are immutable; to retry a failed node, add a replacement node under a new id", - "type": "boolean", - }, - "review": { - "description": "(deep review workers) design reviews pre-implementation artifacts; diff reviews require implementation_node_id and verification_node_id", - "properties": { - "implementation_node_id": { - "type": "string", - }, - "phase": { - "enum": [ - "design", - "diff", - ], - "type": "string", - }, - "verification_node_id": { - "type": "string", - }, - }, - "required": [ - "phase", - ], - "type": "object", - }, - "worker_config": { - "description": "{ timeout_ms } — bounds node execution. Inherits config.node_defaults.worker_config", - "properties": { - "timeout_ms": { - "type": "number", - }, - }, - "type": "object", - }, - "worker_type": { - "description": "Agent type (explore, build, general, plan, or custom)", - "type": "string", - }, - }, - "required": [ - "id", - "name", - "worker_type", - "depends_on", - "prompt_template", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": [ - "name", - "nodes", - ], - "type": "object", - }, - ], - }, - "mode": { - "enum": [ - "standard", - "deep", - ], - "type": "string", - }, - "title": { - "type": "string", - }, - }, - "required": [ - "config", - ], - "type": "object", - }, - }, - "required": [ - "action", - "spec", - ], - "type": "object", - }, - { - "properties": { - "action": { - "description": "Pre-flight a custom spec without creating a workflow; returns diagnostics, never a workflow ID", - "enum": [ - "validate", - ], - "type": "string", - }, - "profile": { - "description": "portable: distributable-template checks; environment: additionally resolves prompts, workers, and models in this project. Defaults: builtin specs portable, inline and project/global specs environment", + "description": "portable: distributable-template checks; environment: additionally resolves prompts, workers, and models in this project. Defaults: builtin specs portable, project/global/path specs environment", "enum": [ "portable", "environment", @@ -2156,7 +696,7 @@ exports[`tool parameters JSON Schema (wire shape) workflow 1`] = ` "type": "string", }, "spec_path": { - "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Relative paths resolve from the session directory", + "description": "(start/extend/control replan/read/validate) A saved workflow name from the library (e.g. "code-review"), or a path to a YAML workflow spec. Graph content belongs in that file; relative paths resolve from the session directory", "type": "string", }, }, diff --git a/packages/opencode/test/tool/fixtures/workflow-parameters-post-change.json b/packages/opencode/test/tool/fixtures/workflow-parameters-post-change.json index 7a73cf124e..eb7276f030 100644 --- a/packages/opencode/test/tool/fixtures/workflow-parameters-post-change.json +++ b/packages/opencode/test/tool/fixtures/workflow-parameters-post-change.json @@ -1,126 +1,28 @@ { - "captured_from": "packages/opencode/src/tool/workflow.ts (discriminated-union Parameters)", - "schema_bytes": 29254, - "branch_count": 14, + "captured_from": "packages/opencode/src/tool/workflow.ts (file-backed discriminated-union Parameters)", + "schema_bytes": 4681, + "branch_count": 10, "session_id_exposed": false, "project_id_exposed": false, + "inline_spec_exposed": false, "transformed": { "openai": { - "bytes": 29306, - "branch_count": 14, - "start_inline_spec_config_present": true, - "blocks_branch_fields": [ - "name", - "objective", - "blocks", - "node_defaults", - "max_concurrency", - "max_node_replan_attempts", - "max_total_nodes" - ], - "block_item_fields": [ - "id", - "kind", - "depends_on", - "instruction", - "worker_type", - "required", - "report_to_parent" - ], - "node_item_fields": [ - "id", - "name", - "worker_type", - "depends_on", - "required", - "prompt_template", - "worker_config", - "input_mapping", - "report_to_parent", - "condition", - "restart", - "cancel", - "output_schema", - "review" - ] + "bytes": 4511, + "branch_count": 10, + "start_spec_path_present": true, + "inline_spec_exposed": false }, "azure": { - "bytes": 29306, - "branch_count": 14, - "start_inline_spec_config_present": true, - "blocks_branch_fields": [ - "name", - "objective", - "blocks", - "node_defaults", - "max_concurrency", - "max_node_replan_attempts", - "max_total_nodes" - ], - "block_item_fields": [ - "id", - "kind", - "depends_on", - "instruction", - "worker_type", - "required", - "report_to_parent" - ], - "node_item_fields": [ - "id", - "name", - "worker_type", - "depends_on", - "required", - "prompt_template", - "worker_config", - "input_mapping", - "report_to_parent", - "condition", - "restart", - "cancel", - "output_schema", - "review" - ] + "bytes": 4511, + "branch_count": 10, + "start_spec_path_present": true, + "inline_spec_exposed": false }, "gemini": { - "bytes": 29254, - "branch_count": 14, - "start_inline_spec_config_present": true, - "blocks_branch_fields": [ - "name", - "objective", - "blocks", - "node_defaults", - "max_concurrency", - "max_node_replan_attempts", - "max_total_nodes" - ], - "block_item_fields": [ - "id", - "kind", - "depends_on", - "instruction", - "worker_type", - "required", - "report_to_parent" - ], - "node_item_fields": [ - "id", - "name", - "worker_type", - "depends_on", - "required", - "prompt_template", - "worker_config", - "input_mapping", - "report_to_parent", - "condition", - "restart", - "cancel", - "output_schema", - "review" - ] + "bytes": 4681, + "branch_count": 10, + "start_spec_path_present": true, + "inline_spec_exposed": false } } } diff --git a/packages/opencode/test/tool/workflow-authoring.test.ts b/packages/opencode/test/tool/workflow-authoring.test.ts index 8330690420..97d44a9b3c 100644 --- a/packages/opencode/test/tool/workflow-authoring.test.ts +++ b/packages/opencode/test/tool/workflow-authoring.test.ts @@ -23,7 +23,9 @@ const WORKTREE_LIFECYCLE_BRIEF = { objective: "Repair worktree lifecycle handling: fix bootstrap cleanup races and cover both tiers with regression tests", route: ["plan", "coding(worktree-core)", "coding(callers-and-fixture)", "verify", "review"], - skips: ["explore — the confirmed brief already supplies file references, failure mechanism, package split, risks, and acceptance checks"], + skips: [ + "explore — the confirmed brief already supplies file references, failure mechanism, package split, risks, and acceptance checks", + ], packages: { "worktree-core": "worktree bootstrap/cleanup ownership in the core lifecycle", "callers-and-fixture": "call-site updates plus the isolated memory fixture in cli tests", @@ -79,6 +81,11 @@ const worktreeLifecycleStartInput = { }, } as const +const worktreeLifecyclePathInput = { + action: "start", + spec_path: ".opencode/workflows/worktree-lifecycle-repair.yaml", +} as const + // The previously observed polluted calls: a start that carries another // action's identifiers and control fields. The empty-workflow_id shape // already fails the Dag.ID brand today; the plausible-id shape passes the @@ -104,6 +111,21 @@ const decode = (input: unknown) => Result.isSuccess(Schema.decodeUnknownResult(Parameters, { onExcessProperty: "error" })(input)) describe("worktree-lifecycle regression fixtures", () => { + test("model-facing graph actions require a YAML source path", () => { + const spec = worktreeLifecycleStartInput.spec + + expect(decode({ action: "start", spec_path: "workflow.yaml" })).toBe(true) + expect(decode({ action: "start", spec })).toBe(false) + expect(decode({ action: "extend", workflow_id: "dag_2x9k4m", spec_path: "extend.yaml" })).toBe(true) + expect(decode({ action: "extend", workflow_id: "dag_2x9k4m", spec })).toBe(false) + expect( + decode({ action: "control", operation: "replan", workflow_id: "dag_2x9k4m", spec_path: "replan.yaml" }), + ).toBe(true) + expect(decode({ action: "control", operation: "replan", workflow_id: "dag_2x9k4m", spec })).toBe(false) + expect(decode({ action: "validate", spec_path: "workflow.yaml", profile: "environment" })).toBe(true) + expect(decode({ action: "validate", spec, profile: "environment" })).toBe(false) + }) + test("decision brief route compiles under the block compiler", () => { const nodes = DagBlocks.compileWorkflowBlocks({ objective: WORKTREE_LIFECYCLE_BRIEF.objective, @@ -131,22 +153,23 @@ describe("worktree-lifecycle regression fixtures", () => { ) }) - test("accepted start fixture carries only start-owned fields and a complete config.blocks", () => { - expect(Object.keys(worktreeLifecycleStartInput)).toEqual(["action", "spec"]) + test("accepted start carries only its YAML path while the authored fixture keeps complete blocks", () => { + expect(Object.keys(worktreeLifecyclePathInput)).toEqual(["action", "spec_path"]) expect(worktreeLifecycleStartInput.spec.config.blocks.length).toBe(5) - expect(decode(worktreeLifecycleStartInput)).toBe(true) + expect(decode(worktreeLifecyclePathInput)).toBe(true) }) - test("replay: the complete audit adds no explore block and strict decode keeps a clean start", () => { + test("replay: the complete audit adds no explore block and strict decode keeps a file-backed start", () => { // The confirmed brief already supplies repository evidence, so the route // starts at plan — no explore lane is added back. const blockIDs = worktreeLifecycleStartInput.spec.config.blocks.map((block) => block.id) expect(blockIDs.some((id) => id.includes("explore"))).toBe(false) expect(blockIDs).toEqual(["plan", "coding-worktree-core", "coding-callers-and-fixture", "verify", "review"]) // Strict decoding admits exactly the start-owned fields. - const decoded = Schema.decodeUnknownSync(Parameters, { onExcessProperty: "error" })(worktreeLifecycleStartInput) + const decoded = Schema.decodeUnknownSync(Parameters, { onExcessProperty: "error" })(worktreeLifecyclePathInput) expect(decoded.action).toBe("start") - expect("spec" in decoded).toBe(true) + expect("spec_path" in decoded).toBe(true) + expect("spec" in decoded).toBe(false) expect("workflow_id" in decoded).toBe(false) expect("operation" in decoded).toBe(false) expect("node_id" in decoded).toBe(false) @@ -165,17 +188,17 @@ describe("worktree-lifecycle regression fixtures", () => { }) test("validate rejects control and result fields it does not own", () => { - const spec = worktreeLifecycleStartInput.spec - expect(decode({ action: "validate", spec, workflow_id: "dag_2x9k4m" })).toBe(false) - expect(decode({ action: "validate", spec, node_id: "verify" })).toBe(false) - expect(decode({ action: "validate", spec, operation: "cancel" })).toBe(false) - expect(decode({ action: "validate", spec, cursor: "", limit: 500 })).toBe(false) - // The validate action itself stays clean with exactly one source. - expect(decode({ action: "validate", spec, profile: "portable" })).toBe(true) + const spec_path = "workflow.yaml" + expect(decode({ action: "validate", spec_path, workflow_id: "dag_2x9k4m" })).toBe(false) + expect(decode({ action: "validate", spec_path, node_id: "verify" })).toBe(false) + expect(decode({ action: "validate", spec_path, operation: "cancel" })).toBe(false) + expect(decode({ action: "validate", spec_path, cursor: "", limit: 500 })).toBe(false) + // The validate action itself stays clean with its file source. + expect(decode({ action: "validate", spec_path, profile: "portable" })).toBe(true) expect(decode({ action: "validate", spec_path: "saved-route", profile: "environment" })).toBe(true) }) - test("inline admission rejects boundary-owned audit fields; file reads strip them instead", () => { + test("model-facing start rejects inline admission objects entirely", () => { const brief = { goal: "Ship the change", scope: { in: ["dag"], out: [] }, @@ -197,13 +220,9 @@ describe("worktree-lifecycle regression fixtures", () => { acknowledged_risks: ["unresolved rollout"], } const spec = { ...worktreeLifecycleStartInput.spec, mode: "deep", admission: cleanAdmission } - expect(decode({ action: "start", spec })).toBe(true) - // System-generated fields never belong in the model-facing schema — the - // file-read boundary strips them for legacy YAML compatibility instead. + expect(decode({ action: "start", spec })).toBe(false) for (const field of ["protocol_version", "state", "fingerprint"]) { - expect(decode({ action: "start", spec: { ...spec, admission: { ...cleanAdmission, [field]: "x" } } })).toBe( - false, - ) + expect(decode({ action: "start", spec: { ...spec, admission: { ...cleanAdmission, [field]: "x" } } })).toBe(false) } }) }) diff --git a/packages/opencode/test/tool/workflow-provider-schema.test.ts b/packages/opencode/test/tool/workflow-provider-schema.test.ts index 0aa59f8448..b7be8f53dc 100644 --- a/packages/opencode/test/tool/workflow-provider-schema.test.ts +++ b/packages/opencode/test/tool/workflow-provider-schema.test.ts @@ -4,11 +4,9 @@ import { Parameters } from "../../src/tool/workflow" import { ToolJsonSchema } from "../../src/tool/json-schema" import { ProviderTransform } from "../../src/provider/transform" -// Wire-shape regression for change repair-workflow-authoring-validation: -// the discriminated union must survive provider transformation — every action -// keeps its discriminator and required fields, and nested block/node fields -// stay visible to the model (the pre-change Record spec collapsed to -// `properties: {}` on OpenAI — see fixtures/workflow-parameters-pre-change.json). +// Wire-shape regression for the file-backed workflow entry: every action keeps +// its discriminator and owned fields, while graph content stays out of the +// provider call and is supplied through spec_path. const openaiModel = { providerID: "openai", api: { id: "gpt-4.1", npm: "@ai-sdk/openai" } } as never const azureModel = { providerID: "azure", api: { id: "gpt-4.1", npm: "@ai-sdk/azure" } } as never @@ -43,12 +41,12 @@ function record(node: JsonSchemaNode | undefined): Record { - test("base wire shape is the 14-branch discriminated union", async () => { + test("base wire shape is the 10-branch file-backed discriminated union", async () => { const schema = ToolJsonSchema.fromSchema(Parameters as never) as JsonSchemaNode const evidence = (await Bun.file( new URL("./fixtures/workflow-parameters-post-change.json", import.meta.url), ).json()) as JsonSchemaNode - expect(schema.anyOf?.length).toBe(14) + expect(schema.anyOf?.length).toBe(10) const flat = JSON.stringify(schema) expect(flat).not.toContain('"session_id"') expect(flat).not.toContain('"project_id"') @@ -71,36 +69,18 @@ describe("workflow provider-facing schema", () => { expect(Object.keys(status.properties ?? {})).toEqual(["action", "workflow_id"]) }) - test("OpenAI transformation exposes the nested blocks spec instead of properties: {}", () => { + test("OpenAI transformation exposes paths without inline graph objects", () => { const transformed = ProviderTransform.schema( openaiModel, ToolJsonSchema.fromSchema(Parameters as never), ) as JsonSchemaNode - const startInline = branchByAction(transformed, "start", "spec")[0] - const config = record(startInline)["spec"] - expect(record(config)["config"]).toBeDefined() - const configUnion = record(config)["config"].anyOf ?? [] - const blocksBranch = configUnion.find((branch) => branch.properties?.blocks !== undefined) - const nodesBranch = configUnion.find((branch) => branch.properties?.nodes !== undefined) - expect(blocksBranch).toBeDefined() - expect(nodesBranch).toBeDefined() - expect(record(blocksBranch)["objective"]).toBeDefined() - expect(blocksBranch?.required).toEqual(expect.arrayContaining(["name", "objective", "blocks"])) - expect(nodesBranch?.required).toEqual(expect.arrayContaining(["name", "nodes"])) - const blockItem = record(blocksBranch)["blocks"]?.items - expect(Object.keys(record(blockItem))).toEqual(expect.arrayContaining(["id", "kind", "depends_on", "instruction"])) - expect(Object.keys(record(blockItem))).not.toContain("skills") - const nodeItem = record(nodesBranch)["nodes"]?.items - expect(Object.keys(record(nodeItem))).toEqual( - expect.arrayContaining(["id", "name", "worker_type", "depends_on", "prompt_template"]), - ) - expect(Object.keys(record(nodeItem))).not.toContain("model") - expect(Object.keys(record(record(nodesBranch)["node_defaults"]))).not.toContain("model") - // Exactly-one-source prompt_template: both variants declared. - const promptTemplate = record(nodeItem)["prompt_template"] - const promptVariants = promptTemplate?.anyOf ?? [] - expect(promptVariants.some((variant) => variant.properties?.inline !== undefined)).toBe(true) - expect(promptVariants.some((variant) => variant.properties?.id !== undefined)).toBe(true) + for (const action of ["start", "extend", "validate"]) { + expect(branchByAction(transformed, action, "spec_path").length).toBe(1) + expect(branchByAction(transformed, action, "spec")).toEqual([]) + } + expect(branchByAction(transformed, "control", "spec_path").length).toBe(1) + expect(branchByAction(transformed, "control", "spec")).toEqual([]) + expect(Object.keys(record(branchByAction(transformed, "start", "spec_path")[0]))).toEqual(["action", "spec_path"]) }) test("pins the removed Skill-dependent block surface as red evidence", async () => { @@ -119,19 +99,19 @@ describe("workflow provider-facing schema", () => { azureModel, ToolJsonSchema.fromSchema(Parameters as never), ) as JsonSchemaNode - expect(transformed.anyOf?.length).toBe(14) - expect(branchByAction(transformed, "start", "spec").length).toBeGreaterThan(0) + expect(transformed.anyOf?.length).toBe(10) + expect(branchByAction(transformed, "start", "spec_path").length).toBe(1) expect(branchByAction(transformed, "validate", "spec_path").length).toBeGreaterThan(0) }) - test("Gemini transformation keeps every branch and nested fields", () => { + test("Gemini transformation keeps every file-backed branch", () => { const transformed = ProviderTransform.schema( geminiModel, ToolJsonSchema.fromSchema(Parameters as never), ) as JsonSchemaNode - expect(transformed.anyOf?.length).toBe(14) - const startInline = branchByAction(transformed, "start", "spec")[0] - expect(record(record(startInline)["spec"])["config"]).toBeDefined() + expect(transformed.anyOf?.length).toBe(10) + expect(branchByAction(transformed, "start", "spec_path").length).toBe(1) + expect(branchByAction(transformed, "start", "spec")).toEqual([]) const resultBranch = branchByAction(transformed, "result")[0] expect(resultBranch.required).toEqual(expect.arrayContaining(["workflow_id", "node_id"])) expect(Object.keys(record(resultBranch))).toEqual(expect.arrayContaining(["cursor", "limit"])) From 4d2ad6ce695c32b63b5f43031895877516552e7a Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:18:37 +0800 Subject: [PATCH 02/11] chore(repo): remove local workflow artifacts --- .../.dag-specs/closing-audit-cygpath-fix.yaml | 141 ----- ...deep-review-diff-error-class-continue.yaml | 229 -------- .../deep-review-diff-error-class-loop1.yaml | 176 ------ .../deep-review-diff-error-class.yaml | 502 ----------------- .../deep-review-pr167-continue.yaml | 217 -------- .opencode/.dag-specs/deep-review-pr167.yaml | 424 --------------- .opencode/.dag-specs/deep-review-round2.yaml | 513 ------------------ .../deep-review-round3-continue.yaml | 177 ------ .../.dag-specs/deep-review-round3-final.yaml | 223 -------- .../final-confirmation-continue.yaml | 203 ------- .../final-confirmation-three-pr-stack.yaml | 291 ---------- .../review-parts-diff/review-contract.md | 1 - .../review-parts-diff/review-dataflow.md | 1 - .../review-parts-diff/review-prompts.md | 1 - .../review-parts-diff/review-runtime.md | 1 - .../review-parts-diff/review-style.md | 1 - .../review-parts-diff/review-tests.md | 1 - .../review-parts-diff/scope-diff.md | 1 - .../review-parts-diff/verify-suite.md | 1 - .../review-parts-final/review-config-repo.md | 1 - .../review-parts-final/review-stack.md | 1 - .../review-parts-final/verify-suite.md | 1 - .../review-parts-round3/final-audit-report.md | 73 --- .../review-parts-round3/verify-suite.md | 1 - .../.dag-specs/review-parts/explore-build.md | 110 ---- .../.dag-specs/review-parts/explore-ci.md | 117 ---- .../review-parts/explore-runtime.md | 125 ----- .../review-parts/review-architecture.md | 75 --- .../.dag-specs/review-parts/review-logic.md | 79 --- .../review-parts/review-robustness.md | 50 -- .../.dag-specs/review-parts/review-style.md | 41 -- .../review-parts/review-testability.md | 42 -- .opencode/batch-a-implement-manifest.md | 57 -- .opencode/dag-prompts/arch-gate.md | 46 -- .opencode/dag-prompts/code-explore.md | 43 -- .opencode/dag-prompts/config-explore.md | 42 -- .opencode/dag-prompts/implement.md | 49 -- .opencode/dag-prompts/integration-test.md | 38 -- .opencode/dag-prompts/patcher-assemble.md | 42 -- .opencode/dag-prompts/plan.md | 43 -- .opencode/dag-prompts/review-arch.md | 44 -- .opencode/dag-prompts/review-logic.md | 44 -- .opencode/dag-prompts/review-style.md | 44 -- .opencode/dag-prompts/test-explore.md | 42 -- .opencode/dag-prompts/verify.md | 41 -- .opencode/grill-batch-a/CONTEXT.md | 50 -- .../ADR-0001-escalation-pending-semantics.md | 25 - .../adr/ADR-0002-delivery-gated-retime.md | 69 --- .../ADR-0003-node-deadline-extended-event.md | 107 ---- .../adr/ADR-0004-lock-timeout-occams.md | 35 -- .../node-lifecycle-transitions.md | 60 -- .opencode/handoff-batch-b.md | 49 -- .opencode/opencode.jsonc | 2 +- .opencode/workflows/GRAPH-ENGINEERING.md | 41 -- .../workflows/algo-complexity-review.yaml | 246 --------- .opencode/workflows/dag-module-review-v2.yaml | 255 --------- .opencode/workflows/dag-module-review.yaml | 235 -------- .opencode/workflows/dag-review.yaml | 166 ------ .opencode/workflows/deep-perf-review.yaml | 346 ------------ .../full-codebase-critical-review.yaml | 251 --------- .opencode/workflows/perf-deep-review.yaml | 376 ------------- .opencode/workflows/review-dag-subsystem.yaml | 206 ------- .../01-q1-escalation-pending-lifecycle.md | 17 - .../issues/02-q2-delivery-gated-retime.md | 17 - .../03-q3-node-deadline-extended-event.md | 18 - .../issues/04-q3-sdk-regen-consumers.md | 15 - .../issues/05-s5-workflow-lock-timeout.md | 17 - .../issues/06-flaky-stdout-pollution.md | 16 - .../issues/07-flaky-sharenext-timing.md | 15 - .../issues/08-flaky-workspace-timing.md | 15 - .../batch-a/issues/09-promote-dev-to-main.md | 14 - .../10-backlog-phantom-cancelled-state.md | 28 - .../issues/11-backlog-spurious-t8-budget.md | 27 - .scratch/batch-b/README.md | 23 - .scratch/batch-b/abort-path-contracts.md | 53 -- .scratch/batch-b/config-lkg-spec.md | 269 --------- .scratch/batch-b/evidence.md | 61 --- .../batch-b/issues/01-u1-fork-rollback.md | 23 - .../issues/02-u2-transport-timeout-abort.md | 24 - .../issues/03-transport-midstream-stall.md | 24 - .../issues/04-f3-subscription-readiness.md | 22 - .../05-f4-type-safe-dag-store-fixtures.md | 21 - .scratch/batch-b/issues/06-o1-lkg-spec.md | 22 - .../batch-b/issues/07-o1-lkg-implement.md | 45 -- .../08-s7-recovery-invented-diagnosis.md | 23 - .../issues/09-promote-dev-main-release.md | 12 - .scratch/batch-b/s7-diagnosis.md | 53 -- .../issues/01-p8-spawn-ready-observation.md | 30 - AGENTS.md | 6 + CLAUDE.md | 178 ++++++ 90 files changed, 185 insertions(+), 7817 deletions(-) delete mode 100644 .opencode/.dag-specs/closing-audit-cygpath-fix.yaml delete mode 100644 .opencode/.dag-specs/deep-review-diff-error-class-continue.yaml delete mode 100644 .opencode/.dag-specs/deep-review-diff-error-class-loop1.yaml delete mode 100644 .opencode/.dag-specs/deep-review-diff-error-class.yaml delete mode 100644 .opencode/.dag-specs/deep-review-pr167-continue.yaml delete mode 100644 .opencode/.dag-specs/deep-review-pr167.yaml delete mode 100644 .opencode/.dag-specs/deep-review-round2.yaml delete mode 100644 .opencode/.dag-specs/deep-review-round3-continue.yaml delete mode 100644 .opencode/.dag-specs/deep-review-round3-final.yaml delete mode 100644 .opencode/.dag-specs/final-confirmation-continue.yaml delete mode 100644 .opencode/.dag-specs/final-confirmation-three-pr-stack.yaml delete mode 100644 .opencode/.dag-specs/review-parts-diff/review-contract.md delete mode 100644 .opencode/.dag-specs/review-parts-diff/review-dataflow.md delete mode 100644 .opencode/.dag-specs/review-parts-diff/review-prompts.md delete mode 100644 .opencode/.dag-specs/review-parts-diff/review-runtime.md delete mode 100644 .opencode/.dag-specs/review-parts-diff/review-style.md delete mode 100644 .opencode/.dag-specs/review-parts-diff/review-tests.md delete mode 100644 .opencode/.dag-specs/review-parts-diff/scope-diff.md delete mode 100644 .opencode/.dag-specs/review-parts-diff/verify-suite.md delete mode 100644 .opencode/.dag-specs/review-parts-final/review-config-repo.md delete mode 100644 .opencode/.dag-specs/review-parts-final/review-stack.md delete mode 100644 .opencode/.dag-specs/review-parts-final/verify-suite.md delete mode 100644 .opencode/.dag-specs/review-parts-round3/final-audit-report.md delete mode 100644 .opencode/.dag-specs/review-parts-round3/verify-suite.md delete mode 100644 .opencode/.dag-specs/review-parts/explore-build.md delete mode 100644 .opencode/.dag-specs/review-parts/explore-ci.md delete mode 100644 .opencode/.dag-specs/review-parts/explore-runtime.md delete mode 100644 .opencode/.dag-specs/review-parts/review-architecture.md delete mode 100644 .opencode/.dag-specs/review-parts/review-logic.md delete mode 100644 .opencode/.dag-specs/review-parts/review-robustness.md delete mode 100644 .opencode/.dag-specs/review-parts/review-style.md delete mode 100644 .opencode/.dag-specs/review-parts/review-testability.md delete mode 100644 .opencode/batch-a-implement-manifest.md delete mode 100644 .opencode/dag-prompts/arch-gate.md delete mode 100644 .opencode/dag-prompts/code-explore.md delete mode 100644 .opencode/dag-prompts/config-explore.md delete mode 100644 .opencode/dag-prompts/implement.md delete mode 100644 .opencode/dag-prompts/integration-test.md delete mode 100644 .opencode/dag-prompts/patcher-assemble.md delete mode 100644 .opencode/dag-prompts/plan.md delete mode 100644 .opencode/dag-prompts/review-arch.md delete mode 100644 .opencode/dag-prompts/review-logic.md delete mode 100644 .opencode/dag-prompts/review-style.md delete mode 100644 .opencode/dag-prompts/test-explore.md delete mode 100644 .opencode/dag-prompts/verify.md delete mode 100644 .opencode/grill-batch-a/CONTEXT.md delete mode 100644 .opencode/grill-batch-a/adr/ADR-0001-escalation-pending-semantics.md delete mode 100644 .opencode/grill-batch-a/adr/ADR-0002-delivery-gated-retime.md delete mode 100644 .opencode/grill-batch-a/adr/ADR-0003-node-deadline-extended-event.md delete mode 100644 .opencode/grill-batch-a/adr/ADR-0004-lock-timeout-occams.md delete mode 100644 .opencode/grill-batch-a/node-lifecycle-transitions.md delete mode 100644 .opencode/handoff-batch-b.md delete mode 100644 .opencode/workflows/GRAPH-ENGINEERING.md delete mode 100644 .opencode/workflows/algo-complexity-review.yaml delete mode 100644 .opencode/workflows/dag-module-review-v2.yaml delete mode 100644 .opencode/workflows/dag-module-review.yaml delete mode 100644 .opencode/workflows/dag-review.yaml delete mode 100644 .opencode/workflows/deep-perf-review.yaml delete mode 100644 .opencode/workflows/full-codebase-critical-review.yaml delete mode 100644 .opencode/workflows/perf-deep-review.yaml delete mode 100644 .opencode/workflows/review-dag-subsystem.yaml delete mode 100644 .scratch/batch-a/issues/01-q1-escalation-pending-lifecycle.md delete mode 100644 .scratch/batch-a/issues/02-q2-delivery-gated-retime.md delete mode 100644 .scratch/batch-a/issues/03-q3-node-deadline-extended-event.md delete mode 100644 .scratch/batch-a/issues/04-q3-sdk-regen-consumers.md delete mode 100644 .scratch/batch-a/issues/05-s5-workflow-lock-timeout.md delete mode 100644 .scratch/batch-a/issues/06-flaky-stdout-pollution.md delete mode 100644 .scratch/batch-a/issues/07-flaky-sharenext-timing.md delete mode 100644 .scratch/batch-a/issues/08-flaky-workspace-timing.md delete mode 100644 .scratch/batch-a/issues/09-promote-dev-to-main.md delete mode 100644 .scratch/batch-a/issues/10-backlog-phantom-cancelled-state.md delete mode 100644 .scratch/batch-a/issues/11-backlog-spurious-t8-budget.md delete mode 100644 .scratch/batch-b/README.md delete mode 100644 .scratch/batch-b/abort-path-contracts.md delete mode 100644 .scratch/batch-b/config-lkg-spec.md delete mode 100644 .scratch/batch-b/evidence.md delete mode 100644 .scratch/batch-b/issues/01-u1-fork-rollback.md delete mode 100644 .scratch/batch-b/issues/02-u2-transport-timeout-abort.md delete mode 100644 .scratch/batch-b/issues/03-transport-midstream-stall.md delete mode 100644 .scratch/batch-b/issues/04-f3-subscription-readiness.md delete mode 100644 .scratch/batch-b/issues/05-f4-type-safe-dag-store-fixtures.md delete mode 100644 .scratch/batch-b/issues/06-o1-lkg-spec.md delete mode 100644 .scratch/batch-b/issues/07-o1-lkg-implement.md delete mode 100644 .scratch/batch-b/issues/08-s7-recovery-invented-diagnosis.md delete mode 100644 .scratch/batch-b/issues/09-promote-dev-main-release.md delete mode 100644 .scratch/batch-b/s7-diagnosis.md delete mode 100644 .scratch/batch-c/issues/01-p8-spawn-ready-observation.md create mode 100644 CLAUDE.md diff --git a/.opencode/.dag-specs/closing-audit-cygpath-fix.yaml b/.opencode/.dag-specs/closing-audit-cygpath-fix.yaml deleted file mode 100644 index 72799b1b31..0000000000 --- a/.opencode/.dag-specs/closing-audit-cygpath-fix.yaml +++ /dev/null @@ -1,141 +0,0 @@ -title: "Closing audit: cygpath HIGH fix verification" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "Close the final-confirmation audit loop: verify the sole HIGH (missing cygpath conversion in release-fork.yml Extract Templates) is fixed by commit 434a4cbca on feat/dag-config-repo, per the arbiter's bounded LOOP scope" - scope: - in: - - "release-fork.yml Extract Templates step: cygpath -m guard now mirrors the models.dev step" - - "packages/opencode/script/generate.ts: contextual error for set-but-missing DAG_TEMPLATES_DIR" - - "affected gates only: typecheck opencode, SDK regen determinism (zero diff), generate.ts error/happy paths" - - "fix is committed and pushed to feat/dag-config-repo (PR #171)" - out: - - "the 3 MEDIUM + 10 LOW deferred follow-ups (arbiter excluded them from the loop)" - - "re-running the 9/9 gate suite or re-reviewing lanes (arbiter forbade both)" - constraints: - - "read-only verification" - assumptions: - - "arbiter verdict and loop_scope from dag_035a2534affe21Pdb2mCgFs6a6 are the binding contract" - acceptance_criteria: - - "Extract Templates step contains the cygpath guard pattern identical in shape to the models.dev step" - - "generate.ts throws a contextual error for missing dir and loads a real dir correctly" - - "typecheck PASS; SDK regen produces zero diff; fix commit present on feat/dag-config-repo" - evidence_required: - - "file:line citations + executed checks" - risks: - - "none material; mechanical one-pattern mirror" - review_plan: - - "one verifier node (fix existence + gates), one arbiter (final PASS/LOOP/BLOCKED)" - open_questions: [] - blocking_questions: [] -config: - name: closing-audit-cygpath-fix - max_concurrency: 2 - max_node_replan_attempts: 1 - max_total_nodes: 4 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 600000 - nodes: - - id: verify-fix - name: "Verify: cygpath fix + affected gates" - worker_type: general - depends_on: [] - required: true - output_schema: - type: object - required: [verdict, results, anomalies] - properties: - verdict: - type: string - enum: [PASS, FAIL, BLOCKED] - results: - type: array - items: - type: object - required: [gate, command, outcome, detail] - properties: - gate: { type: string } - command: { type: string } - outcome: { type: string, enum: [PASS, FAIL, SKIPPED] } - detail: { type: string } - anomalies: { type: array, items: { type: string } } - prompt_template: - inline: | - You are an OBJECTIVE VERIFIER. Read-only (no file modifications). Current - branch: feat/dag-config-repo. Verify the HIGH-fix closure: - 1. Fix presence: .github/workflows/release-fork.yml "Extract Templates" step - contains the `command -v cygpath` guard and writes the converted - templates_dir to GITHUB_ENV; compare its shape against the models.dev - step in the same file (quote both hunks, file:line). - 2. Fix committed: `git log --oneline -3` shows the cygpath fix commit on - feat/dag-config-repo; `git diff HEAD~1 HEAD --stat` covers exactly - release-fork.yml + script/generate.ts. - 3. generate.ts contextual error: run - DAG_TEMPLATES_DIR=/nonexistent-probe bun -e "await import('./packages/opencode/script/generate.ts').then(()=>console.log('NO-THROW'),(e)=>console.log('THREW:',e.message.slice(0,120)))" - — expect THREW with the contextual message. - 4. generate.ts happy path: run the same import with - DAG_TEMPLATES_DIR pointing at ~/.config/opencode/workflows — expect the - "Loaded dag templates snapshot" log with a positive template count. - 5. typecheck: bun run typecheck (packages/opencode) - 6. SDK regen determinism: ./packages/sdk/js/script/build.ts then - `git status --short packages/sdk/js/src/v2/gen` — expect zero diff. - Verdict PASS only if all pass. Report decisive fragments. - - - id: arbitrate - name: "Arbiter: Closure Verdict" - worker_type: general - depends_on: [verify-fix] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, reason, evidence, findings, stop_reason, next_action] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: { type: string, enum: [CRITICAL, HIGH, MEDIUM, LOW] } - title: { type: string } - description: { type: string } - evidence: { type: string } - status: { type: string, enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] } - recommendation: { type: string } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prompt_template: - inline: | - You are the ARBITER closing the final-confirmation audit loop for the - three-PR stack (#171/#169/#170). The parent loop verdict - (dag_035a2534affe21Pdb2mCgFs6a6) demanded exactly one bounded fix: the - cygpath HIGH. Judge ONLY whether verify-fix proves that closure: - - PASS: fix present + committed + contextual error works + gates green; - declare the full stack merge-ready, listing the deferred follow-ups as - accepted residuals (3 MEDIUM + 10 LOW from the parent arbiter: version - pinning, template retry, builtin third-scope docs, cascade wording, - goal route optional schema, dagFailNode union, hydration race, stack base - drift, artifact commit intent note, builtin scope tests, embed - determinism, template-update lock semantics, chore message wording) - - LOOP: only if the fix itself is wrong/incomplete (name it) - - BLOCKED: evidence missing - State reason, evidence, stop_reason, next_action explicitly. Submit via - submit_result. diff --git a/.opencode/.dag-specs/deep-review-diff-error-class-continue.yaml b/.opencode/.dag-specs/deep-review-diff-error-class-continue.yaml deleted file mode 100644 index f9cb5e4d1e..0000000000 --- a/.opencode/.dag-specs/deep-review-diff-error-class-continue.yaml +++ /dev/null @@ -1,229 +0,0 @@ -title: "Deep diff review (continuation): reuse 8 completed waves, re-run timed-out verify-claims" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "Continue the failed deep diff review workflow dag_039602a9fffesslSA479Re5hEt: verify-claims timed out (environmental 'timeout' class, not a task error). Reuse all 8 completed node outputs on disk; re-run only verify-claims with a larger budget, then arbitrate and finalize." - scope: - in: - - "verify-claims over the 8 persisted upstream reports in .opencode/.dag-specs/review-parts-diff/ (fresh-context claim verification against actual code)" - - "arbitrate: PASS/LOOP/BLOCKED on verified evidence incl. verify-suite gate results" - - "continuation: deep-dive (LOOP) or finalize-review (PASS)" - out: - - "re-running scope-diff or any reviewer wave (outputs reused, never re-executed)" - constraints: - - "read-only: reviewers/verifiers must not modify any file" - - "upstream report files are read-only inputs" - assumptions: - - "the 8 persisted reports are complete and trustworthy (extracted from completed nodes of the failed workflow)" - - "reused_nodes: scope-diff, review-dataflow, review-runtime, review-contract, review-prompts, review-tests, review-style, verify-suite (verdict PASS)" - - "verify-claims timeout at 600000ms was environmental (reviewer-volume saturation); budget raised to 1800000ms" - acceptance_criteria: - - "every material claim verified with CONFIRMED/REFUTED/PARTIALLY_CONFIRMED/UNRESOLVABLE against file:line" - - "no unresolved CRITICAL/HIGH" - - "arbiter emits structured PASS/LOOP/BLOCKED verdict" - - "reference prune manifest audited" - evidence_required: - - "file:line citations verified against source" - - "persisted verify-suite gate results (all PASS) as objective evidence" - risks: - - "upstream reports may contain reviewer misreadings — verify wave must check independently" - - "verify-claims may time out again despite larger budget" - review_plan: - - "verify-claims reads the 8 persisted reports + checks claims against code" - - "arbitrate rules on verified evidence (PASS/LOOP/BLOCKED, report_to_parent)" - - "LOOP -> deep-dive replan proposal; PASS -> finalize-review publishes the report" - open_questions: [] - blocking_questions: [] -config: - name: deep-review-diff-error-class-continue - max_concurrency: 2 - max_node_replan_attempts: 2 - max_total_nodes: 10 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - # Continuation graph: scope-diff + 6 reviewers + verify-suite COMPLETED in - # workflow dag_039602a9fffesslSA479Re5hEt; outputs persisted at - # .opencode/.dag-specs/review-parts-diff/. Only verify-claims (which timed - # out) re-runs, with a 30-minute budget. - - id: verify-claims - name: "Verify Disputed Claims (from persisted reports)" - worker_type: general - depends_on: [] - required: true - worker_config: - timeout_ms: 1800000 - output_schema: - type: object - required: [verdict, verified_claims, disputed_findings_resolution, critical_findings_status, coverage_gaps, evidence_quality, prune_audit] - properties: - verdict: - type: string - enum: [VERIFIED, GAPS, BLOCKED] - verified_claims: { type: array, items: { type: object } } - disputed_findings_resolution: { type: array, items: { type: object } } - critical_findings_status: { type: array, items: { type: object } } - coverage_gaps: { type: array, items: { type: object } } - evidence_quality: { type: string } - prune_audit: { type: array, items: { type: object } } - prompt_template: - inline: | - You are a CLAIM VERIFIER (fresh context). Read-only — do not modify any file. - - The scope wave (1 node), review wave (6 nodes), and objective gate runner - (verify-suite) already COMPLETED in the prior workflow; their reports are - persisted. Read them ALL first: - - .opencode/.dag-specs/review-parts-diff/scope-diff.md (change map + REFERENCE MANIFEST with prune decisions) - - .opencode/.dag-specs/review-parts-diff/review-dataflow.md - - .opencode/.dag-specs/review-parts-diff/review-runtime.md - - .opencode/.dag-specs/review-parts-diff/review-contract.md - - .opencode/.dag-specs/review-parts-diff/review-prompts.md - - .opencode/.dag-specs/review-parts-diff/review-tests.md - - .opencode/.dag-specs/review-parts-diff/review-style.md - - .opencode/.dag-specs/review-parts-diff/verify-suite.md (objective gates: verdict PASS) - - The review target is the uncommitted working-tree diff (`git diff HEAD`). - Your job: - 1. Extract every unverified_claims item, every reviewer disagreement, and - ALL CRITICAL/HIGH findings from the six reviewer reports. - 2. Verify each against the actual source at the cited file:line: - CONFIRMED / REFUTED / PARTIALLY_CONFIRMED / UNRESOLVABLE (cite exact line). - 3. Every factual claim in review-prompts about runtime semantics MUST be - checked against code — those prompts steer parent-agent repair decisions. - 4. Sample MEDIUM/LOW claims instead of trusting self-report. - 5. Audit the reference manifest from scope-diff.md: each prune must carry - prune_reason + replacement_coverage; record results in prune_audit. - 6. Audit scope coverage: any diff area with no evidence-bearing report is a gap. - - Verdict: - - VERIFIED: every material scope/criterion covered, no material claim unresolved - - GAPS: a bounded fresh review can close named gaps - - BLOCKED: required evidence cannot be obtained - - Submit the structured result via submit_result. coverage_gaps must name the - missing scope, evidence, and the smallest reviewer lane to add in a LOOP. - - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action, prune_decisions] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: { type: string } - description: { type: string } - evidence: { type: string } - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: { type: string } - loop_scope: - type: array - items: { type: string } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prune_decisions: - type: array - items: - type: object - required: [node, prune_reason, replacement_coverage] - properties: - node: { type: string } - prune_reason: { type: string } - replacement_coverage: { type: string } - prompt_template: - inline: | - You are the ARBITER for this deep review of the uncommitted diff. You rule on - VERIFIED evidence only. - - Evidence base: - - The verify-claims structured output (primary — it verified reviewer claims - against code in fresh context). - - Persisted upstream reports in .opencode/.dag-specs/review-parts-diff/ - (read verify-suite.md for the objective gate results — verdict PASS — and - any reviewer report needed for context). - - Your job: - 1. For each CONFIRMED finding, assess true severity (reviewers may over/under-rate) - 2. Discard REFUTED claims; correct PARTIALLY_CONFIRMED descriptions - 3. Deduplicate findings sharing a root cause; rank by impact - 4. A broken prompt-guidance factual claim is at least HIGH (it mis-steers - future parent-agent repair decisions); any real objective-gate failure is - at least HIGH (verify-suite reported PASS — treat contradictions with it - as disputes to resolve from evidence) - 5. Audit prune_decisions from the scope manifest (via verify-claims prune_audit); - missing prune_reason/replacement_coverage forbids PASS - 6. Verdict: - - PASS: no unresolved material finding; scope and evidence coverage complete - - LOOP: a bounded targeted review can resolve specific omissions/disputes - - BLOCKED: evidence insufficient, critical contradiction unresolved, or ceiling reached - 7. LOOP names the minimal new scope in loop_scope; never rerun completed waves - 8. State reason, evidence, stop_reason, next_action explicitly - - Parent disposal contract: PASS -> finalize; LOOP -> pause/replan/resume fresh - targeted nodes; BLOCKED -> stop. Submit via submit_result. - - - id: deep-dive - name: "Plan the bounded fresh review loop" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "LOOP"' - required: true - report_to_parent: true - prompt_template: - inline: | - The arbiter required LOOP. Produce a minimal replan fragment proposal for a - NEW local review wave. Include only the missing or disputed scope from the - arbiter's loop_scope, assign NEW node IDs, preserve real artifact - dependencies, add a fresh verifier and a new arbiter, and stay within the - workflow caps. Read-only: do not fix code. Reused completed outputs live in - .opencode/.dag-specs/review-parts-diff/ — never propose re-running them. - - Return the loop reason, new nodes, dependencies, evidence each node must - collect, acceptance condition, and stop reason. - - - id: finalize-review - name: "Publish the accepted deep-review report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - Publish the final evidence-backed review report for the uncommitted diff. - Include: scope coverage, confirmed findings (severity-ranked with file:line - evidence), discarded/refuted claims, objective gate results (verify-suite), - residual low-risk issues, and the final PASS reason. Source material: - arbiter verdict, verify-claims output, and the persisted reports in - .opencode/.dag-specs/review-parts-diff/. Do not introduce new findings or - claims that were not verified upstream. diff --git a/.opencode/.dag-specs/deep-review-diff-error-class-loop1.yaml b/.opencode/.dag-specs/deep-review-diff-error-class-loop1.yaml deleted file mode 100644 index 246cb581b4..0000000000 --- a/.opencode/.dag-specs/deep-review-diff-error-class-loop1.yaml +++ /dev/null @@ -1,176 +0,0 @@ -# Loop-1 extension fragment for workflow deep-review-diff-error-class-continue. -# Scope is EXACTLY arbitrate.loop_scope: one bounded repair-spec author for the -# five adjudicated guidance-doc defects, one fresh verifier, one new arbiter. -# Read-only everywhere. The 8 persisted reports in .opencode/.dag-specs/review-parts-diff/ -# are read-only inputs and are NEVER re-executed. Caps check: 4 existing + 3 = 7 nodes -# <= max_total_nodes 10; linear chain fits max_concurrency 2; single bounded wave fits -# max_node_replan_attempts 2. -nodes: - - id: loop1-repair-spec - name: "Deep-dive: corrected repair spec for the 5 adjudicated guidance-doc defects" - worker_type: general - depends_on: [arbitrate] - required: true - worker_config: - timeout_ms: 900000 - output_schema: - type: object - required: [defects, corrections_incorporated, optional_test_plan] - properties: - defects: - type: array - items: - type: object - required: [finding_id, target, current_text, replacement_text, justification] - properties: - finding_id: { type: string } - target: { type: string } - current_text: { type: string } - replacement_text: { type: string } - justification: { type: string } - corrections_incorporated: - type: array - items: { type: string } - optional_test_plan: - type: array - items: { type: string } - prompt_template: - inline: | - You are a READ-ONLY repair-spec author. Do not modify any file. The upstream - deep review's arbiter ruled LOOP; its adjudicated findings ({{arbitrate}}) are - your sole mandate. Persisted upstream reports are read-only context in - .opencode/.dag-specs/review-parts-diff/. Never propose re-running any wave. - - For each defect below produce EXACT replacement wording (verbatim markdown) for - the diff author, anchored to the cited file:line, and justify each line against - the adjudicated evidence: - - 1. F1 HIGH — packages/core/src/plugin/command/workflow.md:412 dependency-cascade - triage row: rewrite detection to the two REAL cascade signals — - (a) required-root failure leaves transitive dependents durably pending while - the workflow is failed (scheduling.ts:108-120,162); (b) optional-root failure - places 'Dependency "X" failed/skipped' placeholder text inside dependents' - PROMPT CONTEXT only (loop.ts:132-136; dag-wake-integration.test.ts:438). - Never claim that prefix appears in error_reason (19-site producer inventory + - dag.ts:488-494 refute it). Fix the response: repair root X; for (a) continue - with root replaced — pending dependents resume naturally and must NOT be - replaced; for (b) re-run dependents that consumed placeholder input. - 2. F2 MEDIUM — workflow.md:409 exec_failed row: add ownership-loss - (recovery.ts:141) and workflow-collateral reasons (dag.ts:487-495; - loop.ts:272,830: orchestrator_unresponsive / required node(s) failed / - workflow_failed) plus replan-ceiling (dag.ts:599), condition-eval (loop.ts:114), - template-resolution (loop.ts:173); gate the response on the reason (config - fixes only for model/auth/provider/template/condition; replace+rerun for - ownership loss; sibling root-cause for collateral kills). - 3. F3 MEDIUM — workflow.md:410 verdict_fail row: split ran-but-broke-contract - (submit_result/schema/fingerprint) vs never-ran (unresolved placeholders - loop.ts:186 / review input contract loop.ts:153 → fix template or input - mapping, then rerun). - 4. F4 MEDIUM — workflow.md:404 + dag-flow.txt:37 'every failed node carries an - error_class': qualify — cancelled-via-replan rows project status=failed, - error_reason 'cancelled via replan', NO error_class (projector.ts:320-323; - excluded by digest filter loop.ts:856); same for pre-migration rows; add the - one-line 'no repair needed' note. - 5. F7 LOW — value-set enumeration: mark workflow.md:409-412 + dag-flow.txt:37 - lists non-exhaustive ('e.g.') or add push_exhausted with a 'reserved, never - emitted' note; align dag-event.ts:253 / sql.ts:65 / groups/dag.ts:42-44 - comments. Bundle with F1 wording. - - Mandatory corrections to incorporate (from arbitration; list each in - corrections_incorporated): - - 'required node(s) failed: X' is ephemeral WorkflowFailed-event data only — - never claim it is visible in wake digest or status output. - - Cancelled-via-replan rows carry no error_class (projector.ts:320-323). - - The loop.ts:857 Effect.catch arm does not catch defects (they are orDie'd); - actual DB-error behavior is abort + guarded() log + retry. - - Digest filter is loop.ts:856; 'every failed node' sentence is workflow.md:404; - exec_failed tally 10/5/4. - - Read the live files verbatim before writing replacements: - packages/core/src/plugin/command/workflow.md (esp. :401-427) and - packages/core/src/plugin/command/dag-flow.txt (:34-46), plus the runtime anchors - above. Optional: append F5 test-invariant assertions (cancelled row keeps - errorClass===null; cancelled node absent from wake 'Failed nodes:') as - optional_test_plan. Submit via submit_result. - - - id: loop1-verify-repair - name: "Fresh verification of the repair spec against source" - worker_type: general - depends_on: [loop1-repair-spec] - required: true - worker_config: - timeout_ms: 900000 - output_schema: - type: object - required: [verdict, claim_checks] - properties: - verdict: - type: string - enum: [VERIFIED, GAPS, BLOCKED] - claim_checks: - type: array - items: - type: object - required: [finding_id, status, evidence] - properties: - finding_id: { type: string } - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED, UNRESOLVABLE] - evidence: { type: string } - prompt_template: - inline: | - You are a fresh CLAIM VERIFIER. Read-only — do not modify any file. - Input: the repair spec ({{loop1-repair-spec}}) plus the persisted upstream - reports in .opencode/.dag-specs/review-parts-diff/ (read-only; do not re-run - any wave). - - 1. For each of the 5 replacement blocks: re-read the target file:line verbatim - (workflow.md, dag-flow.txt) and every runtime anchor the spec cites - (loop.ts:132-136,855-862; projector.ts:320-323; dag.ts:487-495,599; - scheduling.ts:108-120,162; recovery.ts:79-141; sql.ts:65; dag-event.ts:253; - groups/dag.ts:42-44). - 2. Assert each replacement makes NO claim already refuted upstream: the - 'Dependency "X" failed/skipped' prefix must never be described as an - error_reason value; workflow-collateral reasons must not be claimed visible - in wake/status; error_class claims must exclude cancelled-via-replan rows. - 3. Confirm the three mandatory arbitration corrections are present and accurate. - 4. Verdict: VERIFIED only if every block is CONFIRMED; otherwise GAPS naming the - exact block and defect. Submit via submit_result. - - - id: loop1-arbitrate - name: "Loop-1 arbiter: accept repair spec or stop" - worker_type: general - depends_on: [loop1-verify-repair] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, reason, loop_scope, stop_reason, next_action] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - loop_scope: { type: array, items: { type: string } } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prompt_template: - inline: | - You are the LOOP-1 ARBITER. Read-only. Rule on the repair spec - ({{loop1-repair-spec}}) as checked by the fresh verifier - ({{loop1-verify-repair}}); upstream adjudication context is in the persisted - reports under .opencode/.dag-specs/review-parts-diff/ (verify-suite gates all - PASS — objective evidence stands). - - PASS: all 5 defects covered by exact replacement wording, all verifier - claim_checks CONFIRMED, mandatory corrections incorporated, no new CRITICAL/HIGH. - Then stop_reason=goal_met, next_action=finalize; the repair spec is the final - deliverable for the diff author. - LOOP: allowed at most once more and only for a named defect in the spec itself; - this is the bounded final wave (max_node_replan_attempts=2) — on any further - non-PASS report BLOCKED/round_cap/stop with residual findings instead of looping. diff --git a/.opencode/.dag-specs/deep-review-diff-error-class.yaml b/.opencode/.dag-specs/deep-review-diff-error-class.yaml deleted file mode 100644 index b75202b05d..0000000000 --- a/.opencode/.dag-specs/deep-review-diff-error-class.yaml +++ /dev/null @@ -1,502 +0,0 @@ -title: "Deep diff review: error_class exposure + wake attribution + failure triage guidance" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "Jointly deep-review the uncommitted working-tree diff: failure-class exposure (error_class persistence, status/wake/httpapi/SDK surfaces), wake attribution semantics, and the failure-triage guidance prompts — plus the earlier Resume-first optimization in dag-flow.txt" - scope: - in: - - "git diff HEAD across packages/core (migration, sql, store, projector, workflow.md, dag-flow.txt, schema.json, migration.gen.ts, schema.gen.ts)" - - "packages/opencode (runtime/loop.ts wake summary, tool/workflow.ts status, httpapi groups+handlers, test/dag edits)" - - "packages/sdk/js/src/v2/gen/types.gen.ts regenerated field" - - "cross-cutting consistency: docs vs runtime semantics, schema vs SDK vs httpapi, test coverage of new behavior" - out: - - "unchanged DAG module behavior outside the diff" - - "untracked .opencode/workflows/*.yaml experiment files" - - "TUI rendering changes (no TUI files were modified)" - constraints: - - "reviewers are read-only; do not modify any file" - - "every material finding must cite file:line evidence" - assumptions: - - "the working tree diff against HEAD is the complete review target (uncommitted; includes the pre-existing dag-flow.txt Resume-first section)" - - "migration/schema.gen/registry files were generated by bun script/migration.ts" - acceptance_criteria: - - "no unresolved CRITICAL/HIGH finding on verified evidence" - - "error_class value set is consistent end-to-end (event trigger enum -> projector -> read surfaces)" - - "workflow.md/dag-flow.txt guidance matches actual runtime semantics" - - "regenerated SDK diff is exactly the intended field; repo contract obligations (check:generated, httpapi exercise) satisfied" - evidence_required: - - "file:line citations for static claims" - - "executed test-suite and typecheck results for runtime claims" - risks: - - "prompt guidance diverging from runtime semantics (e.g. timeout handling, terminal irreversibility)" - - "wake-path races or ordering regressions in loop.ts delivery" - - "documentation drift between the new triage section and existing guidance sections" - review_plan: - - "wave 1: consolidated diff scope map + reference manifest" - - "wave 2: six parallel dimension reviewers (dataflow integrity, runtime semantics, httpapi/SDK contract, prompt accuracy, test coverage, style)" - - "wave 3: claim verification + objective test-suite execution" - - "wave 4: advanced-tier arbiter with PASS/LOOP/BLOCKED verdict and bounded-loop continuation" - open_questions: [] - blocking_questions: [] -config: - name: deep-review-diff-error-class - max_concurrency: 8 - max_node_replan_attempts: 3 - max_total_nodes: 25 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 600000 - nodes: - - id: scope-diff - name: "Scope: consolidated diff map + manifest" - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - You are the SCOPE MAPPER for a deep review of the uncommitted working-tree diff - in this repository. Read-only — do not modify any file. - - Run `git diff HEAD --stat` and `git diff HEAD` (split by file if large; also - `git status --short` for untracked context) and produce: - - 1. Consolidated change map: every changed file grouped by area: - - persistence: packages/core/src/database/migration/20260803073521_workflow_node_error_class.ts, sql.ts, store.ts, projector.ts, migration.gen.ts, schema.gen.ts, schema.json - - read surfaces: packages/opencode/src/tool/workflow.ts (status output), src/dag/runtime/loop.ts (wake summary), src/server/routes/instance/httpapi/groups/dag.ts + handlers/dag.ts - - SDK: packages/sdk/js/src/v2/gen/types.gen.ts - - guidance docs: packages/core/src/plugin/command/workflow.md, dag-flow.txt - - tests: packages/opencode/test/dag/{fixtures.ts,workflow-tool.test.ts,dag-wake-integration.test.ts} - 2. For each area, the intent of the change (infer from content; the stated - purpose: persist the node failure class that previously existed only in - dag.node.failed events, expose it on every agent-visible surface, and add - failure-triage guidance so the parent agent repairs the failed node instead - of restarting the workflow). - 3. Verify and emit the reference manifest exactly as follows, correcting only - factual errors: - - reference_template: deep-review-dag-module - - added nodes: scope-diff, review-dataflow, review-runtime, review-contract, review-prompts, review-tests, review-style, verify-suite - - pruned lanes: {node: explore-core, prune_reason: target is a bounded 16-file diff, not the whole module, replacement_coverage: scope-diff consolidated change map injected into every reviewer lane} and the same prune_reason/replacement_coverage for explore-runtime, explore-templates, explore-integrations - 4. List any changed file whose hunks appear UNRELATED to the stated purpose - (these become arbiter audit items). - 5. Note pre-existing uncommitted content: the dag-flow.txt "Resume-first" - section predates this session's triage sentence — review it as part of the - joint diff per the user's request. - - Output the full map; downstream reviewers receive it as context. - - - id: review-dataflow - name: "Review: error_class dataflow integrity" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a DATAFLOW INTEGRITY REVIEWER. Read-only — do not modify any file. - Review target: the uncommitted working-tree diff (`git diff HEAD`). - Upstream scope map is provided as context; verify against actual code. - - Trace the error_class value end to end: - - Producer: packages/schema/src/dag-event.ts NodeFailed trigger literals - (exec_failed/push_exhausted/verdict_fail/timeout) and every - dag.nodeFailed call site (packages/opencode/src/dag/{dag.ts,runtime/loop.ts,runtime/spawn.ts,runtime/recovery.ts}) — which triggers are actually produced? Note any literal with no producer (push_exhausted) and whether that matters. - - Persistence: migration 20260803073521_workflow_node_error_class, sql.ts - column, store.ts NodeRow.errorClass + mapNode, projector.ts NodeFailed - projection — check the projector sets error_class from event.data.trigger, - that NodeCancelled/NodeSkipped leave it null deliberately, and that the - projector race from-guards are untouched/correct. - - Read surfaces: tool/workflow.ts status output, loop.ts wake node-line - suffix and failed-workflow digest filter (status failed && errorClass != - null), httpapi NodeResponse optional field + handler mapper, SDK - types.gen.ts DagNode.error_class. - - Consistency: same value vocabulary on every surface; any consumer that - assumes values the producer never emits; any surface that strips or - renames the field inconsistently. - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-runtime - name: "Review: wake-loop runtime semantics" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a RUNTIME SEMANTICS REVIEWER. Read-only — do not modify any file. - Review target: packages/opencode/src/dag/runtime/loop.ts uncommitted hunks - (the new failed-workflow attribution read and the node-line class suffix) - plus their interaction with surrounding delivery logic. - - Review criteria: - - The new failuresByWorkflow read happens inside the wake delivery generator: - correctness of the Effect.catch fallback to [], lock/ordering hazards vs - the existing evalLock/workflowLock discipline, and whether the extra store - read can delay or reorder wake delivery or wake_reported persistence. - - Double reporting: a failed wake-eligible node can appear both in the node - line (with class suffix) and in its workflow's Failed nodes digest — assess - whether that is acceptable, confusing, or harmful to the parent's decision. - - Truncation: per-line slice(0, 300) and output slice(0, 500) — multibyte - safety, worst-case summary size for a workflow with many failed nodes. - - Interaction with orchestrator_unresponsive and the actionableDagIDs - mandatory-action line; any path where a failed workflow now delivers - attribution but the mandatory-action guidance contradicts it. - - Regression risk for unchanged paths: completed/cancelled workflows must - produce exactly the old terminal line; skipped-node wakes unchanged. - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. failure_scenarios: array of {scenario, impact, likelihood: HIGH|MEDIUM|LOW} - 4. summary: 2-3 sentences - - - id: review-contract - name: "Review: HTTP API + SDK contract" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are an API CONTRACT REVIEWER. Read-only — do not modify any file. - Review target: the uncommitted httpapi + SDK hunks plus the repo's contract - obligations documented in AGENTS.md (repo root and packages/opencode). - - Review criteria: - - NodeResponse schema (packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts) - vs handler mapper (handlers/dag.ts) vs regenerated SDK type - (packages/sdk/js/src/v2/gen/types.gen.ts DagNode): exact field-name and - optionality alignment; encoder strips undeclared fields — is anything - emitted but undeclared or declared but never emitted? - - Regeneration freshness: run `git diff HEAD -- packages/sdk/js/src/v2/gen` - and confirm the ONLY change is the optional error_class field; check no - other generated file drifted. - - AGENTS.md obligations: route response shape changes require updating - test/server/httpapi-exercise scenarios — inspect the dag scenarios under - packages/opencode/test/server/httpapi-exercise/ and determine whether an - optional response-field addition needs scenario updates (check how the - scenario asserts responses). Also check whether packages/client duplicates - DagNode anywhere that now drifts. - - TUI consumers of DagNode (packages/tui dag-inspector): any consumer that - could display the new field or breaks on it. - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-prompts - name: "Review: guidance accuracy vs runtime semantics" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a PROMPT-ACCURACY REVIEWER. Read-only — do not modify any file. - Review target: packages/core/src/plugin/command/workflow.md new "Node - failure triage" section and the dag-flow.txt Resume-first triage sentence, - checked against ACTUAL runtime behavior. These prompts steer the parent - agent's wake-time repair decisions; any semantic drift is a bug. - - Verify every factual claim against code: - - "timeout: the runtime cancelled its child session at the deadline" — check - packages/opencode/src/dag/runtime/spawn.ts timeout path (promptSvc.cancel) - and recovery.ts deadline handling. Is "Check its child_session_id for - partial artifacts" accurate (session/messages persist after cancel)? - - "exec_failed: unknown/wrong model, auth, rate-limit, connection errors, or - recovery reasons ('no child session on recovery', 'child session failed - (recovered)')" — confirm these exact reason strings/classes exist - (recovery.ts ~79/108, spawn.ts ~300 Cause.pretty path) and that a wrong - model-config error actually surfaces as exec_failed with discoverable text. - - "verdict_fail: missing submit_result, schema rejection, review fingerprint - mismatch" — check capture.ts settleCapturedOutput reasons plus - loop.ts unresolved-placeholder/review-input-contract paths. - - "Budget exhaustion: replan attempt ceiling exceeded / Total node ceiling - exceeded" — confirm exact strings (dag.ts) and that the Escalation - cross-reference exists. - - "Workflow still live: control(pause) -> control(replan) with replacement - node under NEW id ... extend also works" — verify against dag.ts _extend/_replan. - - "Workflow terminal failed: terminal status is irreversible — you cannot - replan it" — verify the _replan terminal guard and - getValidNextWorkflowStatuses FAILED -> [ARCHIVED]; verify the continuation - workflow advice (reuse completed outputs as static input) is feasible. - - Cross-doc consistency: no contradiction with the Crash recovery section, - Verdict Disposal Contract, Bounded Repair, Adaptive Replanning, or the - pre-existing dag-flow.txt Resume-first steps; both docs agree on the - replacement-node-under-new-id mechanic. - - Triage table row "reason starts with Dependency X failed/skipped" — is - that how cascade actually appears to the agent (check loop.ts dependency - interpolation and scheduler behavior for failed deps)? - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-tests - name: "Review: test coverage of the diff" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a TEST COVERAGE REVIEWER. Read-only — do not modify any file. - Review target: the uncommitted test hunks (packages/opencode/test/dag/ - fixtures.ts, workflow-tool.test.ts, dag-wake-integration.test.ts) versus the - behaviors the diff introduces. - - Review criteria: - - Do the new assertions actually cover: projector persistence of error_class - (asserted via store.getNode), status-tool error_class output, wake - node-line class suffix, wake failed-workflow Failed nodes digest - (exact string assertions — are they too brittle or exactly right)? - - Missing coverage candidates — assess each: recovery.ts failure paths - (timeout/exec_failed/verdict_fail on recovery), NodeCancelled rows keeping - errorClass null, multiple failed nodes digest ordering/truncation, empty - errorReason fallback "unknown error", httpapi node endpoint returning the - field. - - Test hygiene: fixture defaults keep old tests valid; added mock node in - workflow-tool.test.ts is realistic (status transition, seq, timestamps); - no duplicated logic into tests. - - Do existing suites elsewhere depend on NodeRow shape and were they all - updated (search for NodeRow literals in packages/core tests too)? - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line or test path", recommendation} - 2. unverified_claims: array of strings - 3. coverage_gaps: array of {path, untested_scenarios: string[]} - 4. summary: 2-3 sentences - - - id: review-style - name: "Review: style & repo conventions" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a STYLE & CONVENTIONS REVIEWER. Read-only — do not modify any file. - Review target: all uncommitted code hunks (`git diff HEAD`). - - Review against the root AGENTS.md Style Guide and package AGENTS.md files: - - Comments: allowed only for non-obvious constraints — judge the new - comments (sql.ts column comment, groups/dag.ts field comment, migration - none, loop.ts none) against that rule. - - const over let, no else, no unnecessary destructuring, no import aliases, - functional style; Effect conventions from packages/opencode/AGENTS.md - (Effect.fn naming, catch usage, no nested service yields). - - Migration conventions: file naming/id format vs existing migrations; - drizzle snake_case column rule; registry/schema.gen regenerated properly. - - Doc style: workflow.md section placement/heading level consistency with - neighbors; table formatting; dag-flow.txt sentence integration. - - Test style: matches existing assertion patterns in the touched files. - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: verify-claims - name: "Verify Disputed Claims" - worker_type: general - depends_on: [review-dataflow, review-runtime, review-contract, review-prompts, review-tests, review-style] - required: true - output_schema: - type: object - required: [verdict, verified_claims, disputed_findings_resolution, critical_findings_status, coverage_gaps, evidence_quality] - properties: - verdict: - type: string - enum: [VERIFIED, GAPS, BLOCKED] - verified_claims: { type: array, items: { type: object } } - disputed_findings_resolution: { type: array, items: { type: object } } - critical_findings_status: { type: array, items: { type: object } } - coverage_gaps: { type: array, items: { type: object } } - evidence_quality: { type: string } - prompt_template: - inline: | - You are a CLAIM VERIFIER. Read-only — do not modify any file. - - Six reviewers produced findings and unverified_claims about the uncommitted - diff. You are the fresh-context verification wave. Check every - unverified/disputed/CRITICAL/HIGH claim against the actual code; sample - MEDIUM/LOW claims instead of trusting self-report. - - Upstream context contains all 6 reviewer outputs. Extract: - 1. All unverified_claims items - 2. Findings where reviewers disagree - 3. All CRITICAL/HIGH findings (must be verified regardless) - 4. Any diff area with no evidence-bearing reviewer output - 5. Any prompt-guidance factual claim (review-prompts) — these ALL need code - confirmation because the prompts steer parent-agent repair decisions - - For each claim, read the actual source at the cited location and determine - CONFIRMED / REFUTED / PARTIALLY_CONFIRMED / UNRESOLVABLE with the exact line. - - Verdict: - - VERIFIED: every material scope/criterion covered, no material claim unresolved - - GAPS: a bounded fresh review can close named gaps - - BLOCKED: required evidence cannot be obtained - - Submit the structured result. coverage_gaps must name the missing scope, - evidence, and the smallest reviewer lane that should be added in a LOOP. - - - id: verify-suite - name: "Verify: execute tests and gates" - worker_type: general - depends_on: [scope-diff] - required: true - worker_config: - timeout_ms: 900000 - output_schema: - type: object - required: [verdict, results, anomalies] - properties: - verdict: - type: string - enum: [PASS, FAIL, BLOCKED] - results: - type: array - items: - type: object - required: [gate, command, outcome, detail] - properties: - gate: { type: string } - command: { type: string } - outcome: { type: string, enum: [PASS, FAIL, SKIPPED] } - detail: { type: string } - anomalies: { type: array, items: { type: string } } - prompt_template: - inline: | - You are an OBJECTIVE GATE RUNNER. You execute commands and report results - faithfully. Do NOT modify any file. Do not fix failures — report them. - - Run these gates in order (each from the stated directory): - 1. typecheck core: bun run typecheck (packages/core) - 2. typecheck opencode: bun run typecheck (packages/opencode) - 3. opencode DAG suites: bun test test/dag (packages/opencode) - 4. core DAG suites: bun test test/dag-core.test.ts test/dag-projector-drift.test.ts test/dag-store-summaries.test.ts test/plugin/command.test.ts (packages/core) - 5. migration check: bun script/migration.ts --check (packages/core) - 6. HttpAPI contract: bun run test:httpapi --fail-on-missing (packages/opencode) - 7. SDK freshness: in packages/sdk/js run bun run build, then - `git diff -- packages/sdk/js/src/v2/gen`. IMPORTANT interpretation: this - working tree intentionally adds DagNode.error_class uncommitted — the gate - PASSES iff the ONLY diff lines in src/v2/gen are the added error_class - field (one insertion in types.gen.ts) and the regeneration produced no - OTHER drift. Do not fail on the intended field itself. - - For each gate record outcome + the decisive output fragment. Verdict PASS - only if all gates pass under that interpretation; FAIL lists each failing - gate with evidence; BLOCKED if a gate cannot run (state why). - - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [verify-claims, verify-suite] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action, prune_decisions] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: { type: string } - description: { type: string } - evidence: { type: string } - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: { type: string } - loop_scope: - type: array - items: { type: string } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prune_decisions: - type: array - items: - type: object - required: [node, prune_reason, replacement_coverage] - properties: - node: { type: string } - prune_reason: { type: string } - replacement_coverage: { type: string } - prompt_template: - inline: | - You are the ARBITER for this deep review of the uncommitted diff. You rule on - VERIFIED evidence only. The verification wave (verify-claims) and the - objective gate runner (verify-suite) outputs are your primary evidence base; - the 6 reviewer outputs are context. - - Your job: - 1. For each CONFIRMED finding, assess true severity (reviewers may over/under-rate) - 2. Discard REFUTED claims; correct PARTIALLY_CONFIRMED descriptions - 3. Deduplicate findings sharing a root cause; rank by impact - 4. Weight verify-suite gate failures: any real gate failure under the stated - interpretation is at least HIGH; a broken prompt-guidance factual claim is - at least HIGH because it mis-steers future parent-agent repair decisions - 5. Audit the reference manifest from scope-diff: prune decisions must carry - prune_reason + replacement_coverage; flag any changed file unrelated to the - stated purpose - 6. Verdict: - - PASS: no unresolved material finding; scope and evidence coverage complete - - LOOP: a bounded targeted review can resolve specific omissions/disputes - - BLOCKED: evidence insufficient, critical contradiction unresolved, or ceiling reached - 7. LOOP names the minimal new scope; never rerun the whole graph - 8. State reason, evidence, stop_reason, next_action explicitly - - Parent disposal contract: PASS -> finalize; LOOP -> pause/replan/resume fresh - targeted review + verification + arbiter nodes; BLOCKED -> stop. - - Submit your structured verdict via submit_result. - - - id: deep-dive - name: "Plan the bounded fresh review loop" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "LOOP"' - required: true - report_to_parent: true - prompt_template: - inline: | - The arbiter required LOOP. Produce a minimal replan fragment proposal for a - NEW local review wave. Include only the missing or disputed scope from the - arbiter's loop_scope, assign NEW node IDs, preserve real artifact - dependencies, add a fresh verifier and a new arbiter, and stay within the - workflow caps. Read-only: do not fix code. - - Return the loop reason, new nodes, dependencies, evidence each node must - collect, acceptance condition, and stop reason. The parent must pause, replan, - and resume; it must never restart completed nodes or create a cycle. - - - id: finalize-review - name: "Publish the accepted deep-review report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - Publish the final evidence-backed review report for the uncommitted diff. - Include: scope coverage, confirmed findings (severity-ranked with file:line - evidence), discarded/refuted claims, gate-runner results, residual low-risk - issues, and the final PASS reason. Do not introduce new findings or claims - that were not verified upstream. diff --git a/.opencode/.dag-specs/deep-review-pr167-continue.yaml b/.opencode/.dag-specs/deep-review-pr167-continue.yaml deleted file mode 100644 index a0b7d202f5..0000000000 --- a/.opencode/.dag-specs/deep-review-pr167-continue.yaml +++ /dev/null @@ -1,217 +0,0 @@ -title: "Deep Review: PR #167 dag-config-repo (continue from completed waves)" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "从已完成的探索(3)/审查(5)波续跑 PR #167 深度 review:仅执行声明验证 → 仲裁 → 续波,上游输出已落盘在 .opencode/.dag-specs/review-parts/" - scope: - in: - - "verify-claims:对 .opencode/.dag-specs/review-parts/*.md 中的 8 份探索/审查输出做 fresh-context 声明验证(含 CRITICAL/HIGH、unverified_claims、争议、prune manifest、测试执行)" - - "arbitrate:基于验证结果给出 PASS/LOOP/BLOCKED" - - "continuation:deep-dive(LOOP 时)或 finalize-review(PASS 时)" - out: - - "重新运行探索/审查波(其结果已存在且被直接复用)" - constraints: - - "只读:不修改任何代码文件" - - "verify 必须对照实际代码(file:line)检查声明,禁止基于审查者自报通过" - - "上游报告文件只读,不改写" - assumptions: - - "8 份上游报告完整且可信(已提取自上一轮 workflow 的 completed 节点)" - - "模型 400 为偶发,重试即可" - acceptance_criteria: - - "所有 material 声明被验证并给出 CONFIRMED/REFUTED/PARTIALLY_CONFIRMED/UNRESOLVABLE" - - "无未解析的 CRITICAL/HIGH(除非证据确实无法获取且已指名)" - - "仲裁给出结构化 PASS/LOOP/BLOCKED 判决" - - "prune manifest 被审计" - evidence_required: - - "file:line 引用(verify 对照实际源码)" - - "测试执行结果(bun test dag-workflows / workflow-tool)" - risks: - - "上游报告可能含审查者误读,verify 波必须独立对照源码" - - "模型 400 偶发可能导致本节点再次失败" - review_plan: - - "verify-claims(读取 8 份落盘报告 + 对照源码验证)" - - "arbitrate(PASS/LOOP/BLOCKED,report_to_parent)" - - "continuation:LOOP → deep-dive 提案 replan;PASS → finalize-review 发布报告" - open_questions: [] - blocking_questions: [] -config: - name: deep-review-pr167-continue - max_concurrency: 2 - max_node_replan_attempts: 2 - max_total_nodes: 10 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - # Continuation graph: explore(3) + review(5) were COMPLETED in workflow - # dag_03996cfb; their outputs are on disk at .opencode/.dag-specs/review-parts/. - # Wave 3: Claim Verification (reads the persisted upstream reports) - - id: verify-claims - name: "Verify Disputed Claims (from persisted reports)" - worker_type: general - depends_on: [] - required: true - worker_config: - timeout_ms: 1800000 - output_schema: - type: object - required: [verdict, verified_claims, disputed_findings_resolution, critical_findings_status, coverage_gaps, evidence_quality, prune_audit] - properties: - verdict: - type: string - enum: [VERIFIED, GAPS, BLOCKED] - verified_claims: { type: array, items: { type: object } } - disputed_findings_resolution: { type: array, items: { type: object } } - critical_findings_status: { type: array, items: { type: object } } - coverage_gaps: { type: array, items: { type: object } } - evidence_quality: { type: string } - prune_audit: { type: array, items: { type: object } } - prompt_template: - inline: | - 你是声明验证者(fresh-context)。只读,禁止修改任何文件。工作目录:本仓库。 - - 探索波(3 节点)与审查波(5 节点)已在上一轮 workflow 中完成,其报告已落盘,你必须先读取它们: - - .opencode/.dag-specs/review-parts/explore-runtime.md(内置 scope 解析 + PRUNE MANIFEST) - - .opencode/.dag-specs/review-parts/explore-build.md(构建注入 + 命令) - - .opencode/.dag-specs/review-parts/explore-ci.md(release 流水线 + 测试) - - .opencode/.dag-specs/review-parts/review-architecture.md - - .opencode/.dag-specs/review-parts/review-logic.md - - .opencode/.dag-specs/review-parts/review-style.md - - .opencode/.dag-specs/review-parts/review-testability.md - - .opencode/.dag-specs/review-parts/review-robustness.md - - 审查目标仍是 git diff origin/dev...HEAD(PR #167,4 commits)。你负责对整个本地审查波的 fresh-context 复核。 - - 时间预算(30 分钟硬上限):按下列优先级推进,不要在单个声明上耗尽预算: - 1. 所有 CRITICAL/HIGH —— 逐一对照 file:line 验证(各报告内已给出 evidence,务必读实际源码确认) - 2. 所有审查者 unverified_claims —— 对照实际代码验证 - 3. 审查者之间冲突的 findings - 4. 抽样 3-5 条 MEDIUM/LOW 声明 - 5. PRUNE MANIFEST 审计(来自 explore-runtime.md) - 6. 执行测试/类型检查证据:cd packages/opencode && bun test test/dag/dag-workflows.test.ts(应 14 pass)&& bun test test/dag/workflow-tool.test.ts(应 26 pass)&& bun run typecheck。 - 若时间不足,以 GAPS 判决结束并指名未验证的剩余部分——不要含糊带过。 - - 特别留意审查波已经标注的候选阻塞项,逐一核实其真实性: - - review-architecture:/dag-template-update 命令只注册了 core plugin draft 一处,未接入应用 Command 服务,运行时可能不可达 - - review-logic H1:Windows 发布构建 DAG_TEMPLATES_DIR 路径未做 cygpath 转换 - - review-logic H2 / review-testability:builtin scope 零测试覆盖,被删的仓库自检无替代 - 对每条声明,读实际源码(file:line)判定: - - CONFIRMED: 代码确如审查者所述(引用确切行) - - REFUTED: 代码并非审查者所述(解释原因) - - PARTIALLY_CONFIRMED: 方向正确但表述不精确 - - UNRESOLVABLE: 静态分析无法确定 - - 判决: - - VERIFIED: 每个 material 范围/准则都被覆盖且无 material 声明悬而未决 - - GAPS: 有界的 fresh review 可关闭已命名的覆盖/证据缺口 - - BLOCKED: 无法获得所需证据或审查波不可审计 - - 通过 submit_result 提交结构化结果。coverage_gaps 必须命名缺失的范围、证据、以及 LOOP 时应新增的最小审查 lane。 - # ---- Wave 4: Arbitration ---- - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action, prune_decisions] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: - type: string - description: - type: string - evidence: - type: string - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: - type: string - loop_scope: - type: array - items: - type: string - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prune_decisions: - type: array - items: - type: object - required: [node, prune_reason, replacement_coverage] - properties: - node: { type: string } - prune_reason: { type: string } - replacement_coverage: { type: string } - prompt_template: - inline: | - 你是 PR #167 深度 review 的仲裁者。你只对已验证的证据下结论。 - - 验证波已检查所有 unverified/disputed 声明,其输出是你的主要证据基础(verify-claims 的 submit_result 输出)。5 个审查者报告在 .opencode/.dag-specs/review-parts/review-*.md,可作为补充上下文。 - - 你的职责: - 1. 对每个 CONFIRMED finding 评估真实 severity(审查者可能高估/低估) - 2. REFUTED 声明直接丢弃,不进 findings - 3. PARTIALLY_CONFIRMED 以修正后的描述纳入 - 4. 去重描述同一根因的 findings - 5. 按影响排序 - 6. 给出 fail-closed 门禁判决: - - PASS: 无未解决的 material finding;范围与证据覆盖完整 - - LOOP: 有界定向审查可解决特定遗漏或争议 - - BLOCKED: 证据不足、关键矛盾未解决、进度停滞、或图上限耗尽 - 7. LOOP 必须命名最小新审查/验证范围。绝不意味着重跑整个图或重启已完成节点。 - 8. 审计每个父声明的 prune:缺失 prune_reason 或 replacement_coverage 禁止 PASS。 - 9. 陈述 reason、evidence、stop_reason 与确切 next_action。裸结论不是有效判决。 - - 父会话处置契约:PASS → finalize;LOOP → pause/replan/resume 全新定向审查+验证+仲裁节点;BLOCKED → stop。父会话不得把 LOOP 重新解释为建议性接受。 - - 通过 submit_result 提交结构化判决。 - # ---- Continuation ---- - - id: deep-dive - name: "Plan the bounded fresh review loop" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "LOOP"' - required: true - report_to_parent: true - prompt_template: - inline: | - 仲裁者要求 LOOP。为新的定向审查波产出最小 replan fragment 提案:只包含缺失或有争议的范围,分配新节点 ID,保持真实工件依赖,新增 fresh 验证者与新仲裁者,并保持在图上限内。只读:不要修代码。 - - 返回 loop 原因、新节点、依赖、每个节点必须收集的证据、验收条件与 stop reason。父会话必须 pause → replan → resume;绝不重启已完成节点或创建环。 - - id: finalize-review - name: "Publish the accepted deep-review report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - 发布最终证据驱动的 review 报告:范围覆盖、确认的 findings、丢弃/反驳的声明、验证证据、残余低风险项、最终 PASS 理由。不引入上游未验证的新 findings 或声明。报告应直接写入 .opencode/.dag-specs/review-parts/FINAL-REPORT.md,并在会话最终文本中给出完整摘要。 diff --git a/.opencode/.dag-specs/deep-review-pr167.yaml b/.opencode/.dag-specs/deep-review-pr167.yaml deleted file mode 100644 index 055db0f6ff..0000000000 --- a/.opencode/.dag-specs/deep-review-pr167.yaml +++ /dev/null @@ -1,424 +0,0 @@ -title: "Deep Review: PR #167 dag-config-repo" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "深度 review PR #167(feat/dag-config-repo → dev)的全部变更:DAG 参考模板抽离到独立配置仓库 + /dag-template-update 命令 + release 资产打包 + 二进制内置模板(builtin 三级 scope)" - scope: - in: - - "git diff origin/dev...HEAD 的所有变更:packages/opencode/src/dag/workflows.ts、packages/opencode/src/tool/workflow.ts、packages/opencode/script/generate.ts、packages/opencode/script/build.ts、packages/core/src/plugin/command.ts、packages/core/src/plugin/command/dag-flow.txt、packages/core/src/plugin/command/dag-template-update.txt、.github/workflows/release-fork.yml、测试文件变更(dag-workflows.test.ts)" - - "相关集成上下文:opencode-dag-config 仓库(唯一权威源)、全局 workflows 目录、测试覆盖" - out: - - "本 PR 未改动的 DAG 运行时核心(scheduler/recovery/loop/状态机)" - - "TUI 深挖" - - "8 个未跟踪 yaml 模板内容逐行审查(非主对象,仅作上下文)" - constraints: - - "只读:不修改任何文件" - - "每个 reviewer 必须引用 file:line 证据" - - "无法静态确认的声明必须列入 unverified_claims" - - "verify 波必须对照实际代码检查所有未验证/争议声明,禁止基于 reviewer 自报通过" - assumptions: - - "diff 基准为 origin/dev...HEAD(4 个 commit:676e0463e/6ffc7a712/98e4c0624/2ee59d874)" - - "已修复项(ReferenceError 守卫、Entry 去重、M4 空 glob、重复下载删除)属于审查对象的一部分,需确认修复正确而非回退" - acceptance_criteria: - - "所有 material findings 有 file:line 证据" - - "无未解析的 CRITICAL/HIGH 发现" - - "测试执行(bun test dag-workflows/workflow-tool)与类型检查结果作为证据" - - "prune manifest 被 verify 波审计" - evidence_required: - - "file:line 引用" - - "测试执行结果" - - "release-fork.yml 静态行为分析(job 图、环境变量注入链)" - risks: - - "release 流水线/配置仓库生态的实际行为无法在本地完全验证,只能静态审查" - - "builtin 注入仅在构建时生效,运行时行为需靠代码审查推断" - review_plan: - - "波1 探索:3 个并行 lane(运行时解析/构建注入/CI 流水线)" - - "波2 审查:5 个并行维度(架构/逻辑/风格/测试/健壮性)" - - "波3 验证:fresh-context 对照实际代码检查所有 unverified/disputed/CRITICAL/HIGH 声明" - - "波4 仲裁:PASS/LOOP/BLOCKED,report_to_parent" - - "续波:LOOP → deep-dive 提案 replan fragment;PASS → finalize-review 发布报告" - open_questions: [] - blocking_questions: [] -config: - name: deep-review-pr167 - max_concurrency: 5 - max_node_replan_attempts: 3 - max_total_nodes: 30 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 600000 - nodes: - # ---- Wave 1: Exploration (parallel) ---- - # Derived from reference_template: deep-review-dag-module.yaml. - # PRUNE manifest (audited by verify-claims): - # - explore-core -> replaced by explore-runtime (targets workflows.ts/workflow.ts instead of dag lifecycle core) - # prune_reason: review target is the PR diff, not the untouched dag lifecycle module - # replacement_coverage: review-logic/architecture cover the changed runtime surface - # - explore-templates -> folded into explore-build (prompt .txt + command registration + generate/build injection) - # prune_reason: template rendering pipeline untouched by this PR; only builtin data injection changed - # replacement_coverage: explore-build covers generate.ts/build.ts/command.ts/prompt files - # - explore-integrations -> pruned; TUI/schema/SDK untouched by this PR - # prune_reason: out of scope per admission brief - # replacement_coverage: explore-ci covers the release/artifact integration surface - # - explore-runtime -> retargeted to explore-runtime (builtin scope resolution) with new scope - # prune_reason: original lane targets dag/runtime/ which this PR does not modify - # replacement_coverage: workflows.ts resolve/list/readWorkflowSpec fully covered by explore-runtime - - id: explore-runtime - name: "Explore: builtin scope resolution" - worker_type: explore - depends_on: [] - prompt_template: - inline: | - TASK CONTEXT: 深度 review PR #167。diff 命令:git diff origin/dev...HEAD(工作目录:本仓库)。变更集中在 DAG 模板库三级 scope(project/global/builtin)。 - - MANIFEST(派生自 reference deep-review-dag-module.yaml,需记录输出供 verify 审计): - pruned: explore-core (targeted dag lifecycle core, untouched by PR), explore-templates (folded into explore-build), explore-integrations (TUI/schema/SDK untouched); retargeted: explore-runtime (was dag/runtime/, now builtin scope resolution) - - Explore the changed runtime resolution surface: - - packages/opencode/src/dag/workflows.ts (full file): Entry interface, scopes(), resolve(), list(), searchPaths(), isBuiltinPath(), builtinName(), builtinEntry(), parseMeta(), describe(), builtinTemplates() with the typeof OPENCODE_DAG_TEMPLATES guard - - packages/opencode/src/tool/workflow.ts: readWorkflowSpec() builtin branch, resolveSpecPath() bare-name resolution, searchedScopes() helper, list action empty-library message - - Output: - 1. Resolution order and shadowing semantics (project > global > builtin) with line refs - 2. The builtin data flow: where OPENCODE_DAG_TEMPLATES is declared/read, what happens when undefined (dev/test) vs injected (release build) - 3. readWorkflowSpec builtin path: how builtin:// paths are parsed, error handling when content missing, YAML parse failure paths - 4. list() behavior: dedup, sorting, metadata extraction (parseMeta tolerance for malformed specs) - 5. searchedScopes() and not-found/empty messages: what scopes are named, when builtin is mentioned - 6. Any boundary/edge cases: empty builtin map, builtin name shadowed by project, malformed builtin content - - id: explore-build - name: "Explore: build injection & commands" - worker_type: explore - depends_on: [] - prompt_template: - inline: | - TASK CONTEXT: 深度 review PR #167。diff 命令:git diff origin/dev...HEAD。变更集中在模板内嵌构建注入 + /dag-template-update 命令。 - - Explore: - - packages/opencode/script/generate.ts: loadDagTemplatesData() (DAG_TEMPLATES_DIR env contract, Glob scan, name extraction, JSON.stringify output), dagTemplatesData export, comparison with loadModelsData() pattern - - packages/opencode/script/build.ts: the define injection of OPENCODE_DAG_TEMPLATES, how generated outputs are consumed - - packages/core/src/plugin/command.ts: the /dag-template-update registration (draft.update pattern), how .txt content is compiled in - - packages/core/src/plugin/command/dag-template-update.txt (full): zip download flow, dry-run classification (NEW/UNCHANGED/UPDATE), QA options, timestamped backup, concurrency lock (mkdir atomicity, retry, cleanup), verify content comparison, failure handling (download/extract/backup abort), config dir resolution order - - packages/core/src/plugin/command/dag-flow.txt: two-scope (+builtin?) description consistency with the actual three-tier resolution - - Output: - 1. The full build-time data path: DAG_TEMPLATES_DIR → generate.ts → dagTemplatesData → build.ts define → binary constant - 2. What happens in dev when DAG_TEMPLATES_DIR is unset (both generate.ts behavior and the builtinTemplates() guard) - 3. The command registration pattern (how dag-flow and dag-template-update compare) - 4. The prompt file's lock/backup/verify/failure semantics line by line (M1/M2/M3 requirements: lock exists+retry+cleanup, backup-failure aborts overwrite, verify compares content not just list) - 5. Any inconsistency between dag-flow.txt's described scopes and the actual three-tier runtime - 6. Edge cases: empty template dir, non-yaml files in dir, name collision with project/global templates - - id: explore-ci - name: "Explore: release pipeline & tests" - worker_type: explore - depends_on: [] - prompt_template: - inline: | - TASK CONTEXT: 深度 review PR #167。diff 命令:git diff origin/dev...HEAD。变更集中在 release 流水线 + 测试。 - - Explore: - - .github/workflows/release-fork.yml (full): package-templates job (clone opencode-dag-config, nullglob guard, cp inside non-empty branch, tar packaging), Upload Templates Artifact, build-cli job (needs package-templates, download artifact, extract, DAG_TEMPLATES_DIR via GITHUB_ENV), Build CLI step env inheritance, release job (needs build-cli + package-templates, --target github.sha, download-artifact merge-multiple, dag-templates.tar.gz as release asset, SHA256SUMS) - - packages/opencode/test/dag/dag-workflows.test.ts (full): what the 14 tests cover (resolve/list/shadowing/parse tolerance), what was deleted (change-review test) and why that is consistent - - packages/opencode/test/dag/workflow-tool.test.ts: spot-check coverage of the workflow tool list/resolve behavior - - packages/opencode/test/dag/dag-workflow-lock.test.ts: concurrency lock tests (what lock semantics are exercised) - - Output: - 1. Job graph: package-templates → build-cli → release, with needs chains and if conditions - 2. The env injection chain: artifact download → tar extraction → GITHUB_ENV DAG_TEMPLATES_DIR → Build CLI step (verify env inheritance without explicit env:) - 3. Empty-glob behavior: what happens when opencode-dag-config has no yaml (warning + empty archive vs fail) - 4. Release asset flow: which artifacts land in the release, --target github.sha semantics - 5. Test coverage inventory: what dag-workflows.test.ts tests, what the deleted change-review test verified, coverage of builtin scope (note: no injection in test env — how is builtin tested, if at all?) - 6. Any CI regression risk: job-level if conditions, checkout behavior on push vs workflow_dispatch - # ---- Wave 2: Review (parallel, 5 dimensions) ---- - - id: review-architecture - name: "Review: Architecture" - worker_type: general - depends_on: [explore-runtime, explore-build, explore-ci] - prompt_template: - inline: | - 你是 PR #167 的架构审查者。只读,禁止修改任何文件。工作目录:本仓库。 - - 审查目标:git diff origin/dev...HEAD 的全部变更(4 commits:676e0463e / 6ffc7a712 / 98e4c0624 / 2ee59d874)。先运行 git diff origin/dev...HEAD 获取完整变更,再结合上游探索结果(三个 explore 输出已提供)对照实际代码验证。 - - 设计背景:DAG 参考模板从主仓库抽离到独立配置仓库(LeWxDeX/opencode-dag-config)成为唯一权威源;/dag-template-update 命令做日常更新(zip 下载、预演分类、备份 QA);每次 release 打包 dag-templates.tar.gz 资产 + 通过 DAG_TEMPLATES_DIR 构建注入把模板内嵌进二进制(builtin 三级 scope:project > global > builtin),封闭网络可用;主仓库不再 tracked 模板。 - - 审查准则: - - 三级 scope 设计的合理性:项目 > 全局 > builtin 的优先级/遮蔽语义是否清晰且与文档一致(dag-flow.txt / workflow.md / README) - - 单一权威源迁移是否完整:主仓库删除模板后,测试/文档/命令引用是否还有残留指向已删除的模板 - - 构建注入契约:DAG_TEMPLATES_DIR → generate.ts → define 注入的架构边界(与 OPENCODE_MODELS_DEV 先例对比);dev 环境(未注入)与 release 环境(注入)的双模式是否正确 - - 发布流水线架构:资产打包 + 二进制内嵌双渠道的职责划分;package-templates job 与 build-cli 的耦合度是否合理 - - 模块边界:模板解析(workflows.ts)与命令层(workflow.ts)的职责切分;generate.ts 的数据注入是否保持了 generate 脚本的单一职责 - - 与仓库既有模式的一致性(AGENTS.md 扩展不变量:自包含 layer、服务注入等与本 PR 相关的部分) - - 输出(强制格式): - 1. findings: [{severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation}] - 2. unverified_claims: string[] —— 无法用 file:line 确认的断言 - 3. summary: 2-3 句总体评估 - - id: review-logic - name: "Review: Logic Correctness" - worker_type: general - depends_on: [explore-runtime, explore-build, explore-ci] - prompt_template: - inline: | - 你是 PR #167 的逻辑正确性审查者。只读,禁止修改任何文件。工作目录:本仓库。 - - 审查目标:git diff origin/dev...HEAD 全部变更。先运行 git diff origin/dev...HEAD,再对照实际代码。 - - 审查准则: - - resolve() 三级回退:项目文件 → 全局文件 → builtin map;extension 循环与 scope 循环的嵌套顺序;describe()/parseMeta() 对坏 YAML 的容错 - - list() 去重/排序:project 遮蔽 global 遮蔽 builtin 的顺序一致性;builtin 条目 path(builtin://name)与文件条目的混合排序 - - readWorkflowSpec() builtin 分支:builtinName() 解析、content 缺失的报错、YAML 解析失败路径(workflowSpecParseError)是否与文件路径分支一致 - - builtinTemplates() 的 typeof 守卫:dev/test(无全局绑定)与 release(define 注入)两种环境的行为;`declare const` + typeof 守卫的 TS 语义 - - searchedScopes():builtin 存在与否条件下的消息构造;searchPaths 可变性 - - generate.ts loadDagTemplatesData():Glob 扫描、文件名 → 名称提取(.yaml/.yml)、路径拼接安全、JSON.stringify 输出;DAG_TEMPLATES_DIR 未设置时返回 "undefined" 字符串的语义 - - dag-template-update.txt 提示词逻辑(M1/M2/M3):mkdir 锁的原子性/重试/清理、备份失败中止、verify 内容对比、下载/解压失败处理、配置目录解析顺序(OPENCODE_CONFIG_DIR / XDG_CONFIG_HOME) - - release-fork.yml:nullglob + 空数组 cp 守卫、GITHUB_ENV 注入时机、--target github.sha、job needs 链 - - 边界条件:空 builtin map、builtin 被项目同名遮蔽、builtin 内容坏 YAML、DAG_TEMPLATES_DIR 指向空目录、模板名含特殊字符 - - 输出(强制格式): - 1. findings: [{severity, title, description, evidence: "file:line", recommendation}] - 2. unverified_claims: string[] - 3. summary: 2-3 句总体评估 - - id: review-style - name: "Review: Code Style & Conventions" - worker_type: general - depends_on: [explore-runtime, explore-build, explore-ci] - prompt_template: - inline: | - 你是 PR #167 的风格与惯例审查者。只读。工作目录:本仓库。 - - 审查目标:git diff origin/dev...HEAD 中所有 TS/TSX 与 YAML 变更。 - - 对照 AGENTS.md Style Guide: - - 无多余解构(用点号访问) - - 无 import 别名 / star import - - const 优先于 let;三元/早退优先于重赋值 - - 无 else(早退) - - 不预提取单次 helper(除非复用或命名真实概念) - - Effect 生成器:服务绑定到命名变量 - - 非显而易见的约束加注释,明显赋值不加 - - 动态 import 用于启动敏感路径的重模块 - - 提示词 .txt 文件的可读性与结构一致性(dag-template-update.txt 与 dag-flow.txt 的风格对齐) - - 也检查:命名一致性(builtinTemplates/builtinEntry/builtinName/isBuiltinPath)、类型标注纪律(依赖推断)、文件组织与导出模式(workflows.ts 顶层导出 + namespace 投影)。 - - 输出(强制格式): - 1. findings: [{severity, title, description, evidence: "file:line", recommendation}] - 2. unverified_claims: string[] - 3. summary: 2-3 句总体评估 - - id: review-testability - name: "Review: Testability & Coverage" - worker_type: general - depends_on: [explore-runtime, explore-build, explore-ci] - prompt_template: - inline: | - 你是 PR #167 的测试与覆盖审查者。只读。工作目录:本仓库。 - - 审查目标:git diff origin/dev...HEAD 的变更及对应测试。运行相关测试确认状态: - - cd packages/opencode && bun test test/dag/dag-workflows.test.ts(应 14 pass 0 fail) - - bun test test/dag/workflow-tool.test.ts(应 26 pass 0 fail) - - bun run typecheck - - 审查准则: - - 已删测试的合理性:change-review 测试删除后,"主仓库不再 ship 模板"的行为是否仍有测试锚点?删除是否导致某行为失去回归保护? - - builtin scope 的测试覆盖:测试环境无 OPENCODE_DAG_TEMPLATES 注入(typeof 守卫返回 {}),builtin 路径(resolve builtin 分支、readWorkflowSpec builtin 分支、list builtin 条目)实际**没有**被测试——这是覆盖缺口还是可接受? - - 测试是否测试真实实现而非复制逻辑(AGENTS.md 测试纪律) - - M1 并发锁是否有测试(dag-workflow-lock.test.ts 覆盖什么) - - release-fork.yml 的 bash 逻辑(nullglob 守卫、GITHUB_ENV)是否有任何形式的验证(或只能靠静态审查) - - generate.ts 的 DAG_TEMPLATES_DIR 加载是否有测试 - - 输出(强制格式): - 1. findings: [{severity, title, description, evidence: "file:line 或 test 路径", recommendation}] - 2. unverified_claims: string[] - 3. coverage_gaps: [{path, untested_scenarios[]}] - 4. summary: 2-3 句总体评估 - - id: review-robustness - name: "Review: Runtime Robustness" - worker_type: general - depends_on: [explore-runtime, explore-build, explore-ci] - prompt_template: - inline: | - 你是 PR #167 的运行时健壮性审查者。只读。工作目录:本仓库。 - - 审查目标:git diff origin/dev...HEAD 变更在真实环境下的失败模式。 - - 审查准则: - - 封闭网络场景:二进制内嵌模板(builtin)离线可用性;无全局目录、无项目目录、无内置注入的"全空"场景行为链 - - 发布失败模式:opencode-dag-config clone 失败 / 空模板 / tar 失败 / artifact 上传失败 → 各 job 的失败传播(needs 链是否会导致 release 中止) - - 环境变量注入:DAG_TEMPLATES_DIR 经 GITHUB_ENV 的跨步骤可见性;如果 build-cli 在某 runner 上无该变量,构建是否静默无内置模板(静默降级 vs 显式报错) - - /dag-template-update 的并发:mkdir 锁在进程崩溃后残留(孤儿锁)的处理;锁重试次数;备份文件累积(.bak 文件是否清理) - - 下载/解压失败:codeload 404/网络错误/坏 zip → 提示词是否强制原文报错且不伪造成功 - - 模板内容信任边界:内置模板与配置仓库同信任级(说明文档是否明确);模板内容中的恶意 YAML 是否只影响元数据解析(parseMeta 容错) - - 路径安全:Glob 扫描结果与 path.join 的路径穿越风险(模板名 ../ 等) - - 输出(强制格式): - 1. findings: [{severity, title, description, evidence: "file:line", recommendation}] - 2. unverified_claims: string[] - 3. failure_scenarios: [{scenario, impact, likelihood: HIGH|MEDIUM|LOW}] - 4. summary: 2-3 句总体评估 - # ---- Wave 3: Claim Verification ---- - - id: verify-claims - name: "Verify Disputed Claims" - worker_type: general - depends_on: [review-architecture, review-logic, review-style, review-testability, review-robustness] - required: true - worker_config: - timeout_ms: 1800000 - output_schema: - type: object - required: [verdict, verified_claims, disputed_findings_resolution, critical_findings_status, coverage_gaps, evidence_quality, prune_audit] - properties: - verdict: - type: string - enum: [VERIFIED, GAPS, BLOCKED] - verified_claims: { type: array, items: { type: object } } - disputed_findings_resolution: { type: array, items: { type: object } } - critical_findings_status: { type: array, items: { type: object } } - coverage_gaps: { type: array, items: { type: object } } - evidence_quality: { type: string } - prune_audit: { type: array, items: { type: object } } - prompt_template: - inline: | - 你是声明验证者(fresh-context)。只读,禁止修改任何文件。工作目录:本仓库。 - - 五个审查者对 PR #167(git diff origin/dev...HEAD)产出 findings 与 unverified_claims。你是对整个本地审查波的 fresh-context 复核。任务:检查每个 unverified/disputed/CRITICAL/HIGH 声明,并审计请求的范围/验收标准是否真的被覆盖。抽样检查 MEDIUM/LOW 声明,不要信任审查者自报。 - - 时间预算(30 分钟硬上限):按下列优先级推进,不要在单个声明上耗尽预算: - 1. 所有 CRITICAL/HIGH —— 逐一对照 file:line 验证 - 2. 所有审查者 unverified_claims —— 对照实际代码验证 - 3. 审查者之间冲突的 findings - 4. 抽样 3-5 条 MEDIUM/LOW 声明 - 5. PRUNE MANIFEST 审计(来自 explore-runtime 输出) - 6. 执行测试/类型检查证据:cd packages/opencode && bun test test/dag/dag-workflows.test.ts(应 14 pass)&& bun test test/dag/workflow-tool.test.ts(应 26 pass)&& bun run typecheck。除非审查者点名,否则不跑完整 dag-workflow-lock 套件。 - 若时间不足,以 GAPS 判决结束并指名未验证的剩余部分——不要含糊带过。 - - 上游上下文包含 3 个探索 + 5 个审查者输出。提取: - 1. 每个审查者 unverified_claims 数组的所有条目 - 2. 审查者之间冲突的 findings(severity 或结论矛盾) - 3. 所有 CRITICAL/HIGH —— 必须逐一验证 - 4. 没有任何证据性输出的请求文件区域/集成/风险/审查准则 - 5. explore-runtime 输出中的 PRUNE MANIFEST:每个 prune 必须有 prune_reason + replacement_coverage,缺失即审计失败 - - 对每条声明,读实际源码(file:line)判定: - - CONFIRMED: 代码确如审查者所述(引用确切行) - - REFUTED: 代码并非审查者所述(解释原因) - - PARTIALLY_CONFIRMED: 方向正确但表述不精确 - - UNRESOLVABLE: 静态分析无法确定 - - 实际执行验证(允许运行命令,仅在审查者点名或时间允许时): - - cd packages/opencode && bun test test/dag/dag-workflows.test.ts 与 workflow-tool.test.ts 确认测试状态 - - DAG_TEMPLATES_DIR= bun script/generate.ts 验证加载路径(如果审查涉及) - - 判决: - - VERIFIED: 每个 material 范围/准则都被覆盖且无 material 声明悬而未决 - - GAPS: 有界的 fresh review 可关闭已命名的覆盖/证据缺口 - - BLOCKED: 无法获得所需证据或审查波不可审计 - - 通过 submit_result 提交结构化结果。coverage_gaps 必须命名缺失的范围、证据、以及 LOOP 时应新增的最小审查 lane。 - # ---- Wave 4: Arbitration ---- - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action, prune_decisions] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: - type: string - description: - type: string - evidence: - type: string - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: - type: string - loop_scope: - type: array - items: - type: string - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prune_decisions: - type: array - items: - type: object - required: [node, prune_reason, replacement_coverage] - properties: - node: { type: string } - prune_reason: { type: string } - replacement_coverage: { type: string } - prompt_template: - inline: | - 你是 PR #167 深度 review 的仲裁者。你只对已验证的证据下结论。 - - 验证波已检查所有 unverified/disputed 声明,其输出是你的主要证据基础。5 个审查者输出作为上下文。 - - 你的职责: - 1. 对每个 CONFIRMED finding 评估真实 severity(审查者可能高估/低估) - 2. REFUTED 声明直接丢弃,不进 findings - 3. PARTIALLY_CONFIRMED 以修正后的描述纳入 - 4. 去重描述同一根因的 findings - 5. 按影响排序 - 6. 给出 fail-closed 门禁判决: - - PASS: 无未解决的 material finding;范围与证据覆盖完整 - - LOOP: 有界定向审查可解决特定遗漏或争议 - - BLOCKED: 证据不足、关键矛盾未解决、进度停滞、或图上限耗尽 - 7. LOOP 必须命名最小新审查/验证范围。绝不意味着重跑整个图或重启已完成节点。 - 8. 审计每个父声明的 prune:缺失 prune_reason 或 replacement_coverage 禁止 PASS。 - 9. 陈述 reason、evidence、stop_reason 与确切 next_action。裸结论不是有效判决。 - - 父会话处置契约:PASS → finalize;LOOP → pause/replan/resume 全新定向审查+验证+仲裁节点;BLOCKED → stop。父会话不得把 LOOP 重新解释为建议性接受。 - - 通过 submit_result 提交结构化判决。 - # ---- Continuation ---- - - id: deep-dive - name: "Plan the bounded fresh review loop" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "LOOP"' - required: true - report_to_parent: true - prompt_template: - inline: | - 仲裁者要求 LOOP。为新的定向审查波产出最小 replan fragment 提案:只包含缺失或有争议的范围,分配新节点 ID,保持真实工件依赖,新增 fresh 验证者与新仲裁者,并保持在图上限内。只读:不要修代码。 - - 返回 loop 原因、新节点、依赖、每个节点必须收集的证据、验收条件与 stop reason。父会话必须 pause → replan → resume;绝不重启已完成节点或创建环。 - - id: finalize-review - name: "Publish the accepted deep-review report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - 发布最终证据驱动的 review 报告:范围覆盖、确认的 findings、丢弃/反驳的声明、验证证据、残余低风险项、最终 PASS 理由。不引入上游未验证的新 findings 或声明。 diff --git a/.opencode/.dag-specs/deep-review-round2.yaml b/.opencode/.dag-specs/deep-review-round2.yaml deleted file mode 100644 index 90bf3c3514..0000000000 --- a/.opencode/.dag-specs/deep-review-round2.yaml +++ /dev/null @@ -1,513 +0,0 @@ -title: "Deep review round 2: joint re-review of error_class exposure, /goal restoration, and remediation wave" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "Jointly re-review the entire uncommitted working-tree diff: (1) DAG error_class exposure + wake attribution + failure-triage guidance, (2) /goal feature restoration, (3) the remediation wave that fixed round-1 findings — including cross-bundle consistency" - scope: - in: - - "git diff HEAD across packages/core (dag persistence, goal sql, migrations, workflow.md, dag-flow.txt, schema/migration registries)" - - "packages/opencode (goal module restore, session/prompt/system/command wiring, tool registry, dag runtime loop.ts, httpapi groups/handlers, tests)" - - "packages/plugin + packages/tui (goal sidebar, sync slice, adapters, builtins)" - - "packages/sdk/js regenerated surface (goal + error_class fields)" - - "cross-bundle consistency: guidance docs vs runtime semantics, schema vs SDK vs httpapi, test coverage of all new behavior" - out: - - "committed baseline behavior unchanged by the diff" - - "untracked .opencode/workflows/*.yaml experiment files and .opencode/.dag-specs/ artifacts" - constraints: - - "reviewers are read-only; do not modify any file" - - "every material finding must cite file:line evidence" - assumptions: - - "the working tree diff against HEAD is the complete review target" - - "migration/baseline/registry files were generated by bun script/migration.ts; SDK gen files were generated by packages/sdk/js/script/build.ts" - - "intentional adaptations to accept, not flag: goal restoration without the migration-window /goal deprecation scaffolding; IF NOT EXISTS hardening in restore_goal_state migration; serviceOption/deferred-resolution layer patterns introduced after goal retirement; Pick in plugin/tui.ts" - acceptance_criteria: - - "no unresolved CRITICAL/HIGH finding on verified evidence" - - "error_class value set consistent end-to-end; wake attribution and status surfaces correct" - - "workflow.md/dag-flow.txt guidance matches actual runtime semantics after the remediation wave" - - "goal restoration functionally complete vs pre-retire baseline (state management, tool registration, dispatch, judge loop, TUI, HTTP API)" - - "regenerated SDK diff limited to intended additions; repo contract obligations satisfied" - evidence_required: - - "file:line citations for static claims" - - "executed test-suite, typecheck, migration-check, and httpapi contract results" - risks: - - "cross-bundle interference (goal wiring vs dag changes in shared files: app-runtime, bootstrap, registry, prompt.ts, httpapi session group)" - - "guidance docs drifting from runtime semantics again after remediation edits" - - "layer-wiring regressions from restoring heavyweight GoalLoop into bootstrap/app layers" - review_plan: - - "wave 1: consolidated diff scope map with per-bundle change inventory + reference manifest" - - "wave 2: six parallel dimension reviewers" - - "wave 3: claim verification + objective gate execution" - - "wave 4: advanced-tier arbiter (PASS/LOOP/BLOCKED) with bounded-loop continuation" - open_questions: [] - blocking_questions: [] -config: - name: deep-review-round2 - max_concurrency: 8 - max_node_replan_attempts: 3 - max_total_nodes: 20 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - - id: scope-diff - name: "Scope: consolidated diff map + manifest" - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - You are the SCOPE MAPPER for a joint deep review of the uncommitted - working-tree diff in this repository. Read-only — do not modify any file. - - Run `git diff HEAD --stat`, `git diff HEAD` per area, and - `git status --short` (untracked files included) and produce: - - 1. Consolidated change map grouping EVERY changed/new file into one of - these bundles (flag any file fitting none as AUDIT-FLAG): - - BUNDLE A (error_class exposure + wake attribution): core/src/dag/{sql,store,projector}.ts, migrations 20260803073521_*, tool/workflow.ts status, dag/runtime/loop.ts wake digest, httpapi dag group/handler, sdk DagNode.error_class, related tests - - BUNDLE B (/goal restoration): core/src/goal/sql.ts + migration 20260803083938_*, opencode/src/goal/*, session/prompt/goal.txt, tool/goal.{ts,txt}, command/index.ts GOAL/SUBGOAL, session/{prompt,session,system}.ts wiring, effect/{app-runtime,bootstrap-runtime}.ts + project/bootstrap.ts GoalLoop wiring, tool/registry.ts, httpapi session goal route + handler + server Goal.node, plugin/tui.ts goal types, tui sidebar/goal.tsx + sync + builtins + adapters, test fixture/goal tests/httpapi-exercise goal scenario, sdk Goal surface - - BUNDLE C (remediation wave from prior review): core/src/plugin/command/workflow.md triage section rewrite, dag-flow.txt triage sentence, loop.ts catchCause/store/predicate fixes, new test assertions (cancelled-errorClass-null, recovery errorClass), packages/sdk/js/.gitignore - - GENERATED: schema.json, migration.gen.ts, schema.gen.ts, sdk gen files (verify they contain ONLY goal + error_class related additions) - 2. For each bundle, the stated intent (A: persist/expose node failure - class so the parent agent repairs failed nodes instead of restarting; - B: restore the retired /goal feature faithfully incl. state management - (goal_state table, goal.updated/cleared events) and tool registration - (GoalTool status/complete); C: fix guidance-doc defects found by the - prior review's arbiter). - 3. Verify and emit the reference manifest: - - reference_template: deep-review-dag-module - - added nodes: scope-diff, review-error-class, review-guidance, review-goal-core, review-goal-surface, review-tests, review-style, verify-suite - - pruned lanes: explore-core, explore-runtime, explore-templates, explore-integrations — prune_reason: target is a bounded working-tree diff, not the whole module; replacement_coverage: this scope-diff consolidated change map feeds every reviewer lane - 4. List every changed file once, with bundle tag; AUDIT-FLAG anything unclassifiable. - - Downstream reviewers receive this map as context. - - - id: review-error-class - name: "Review: error_class dataflow + wake semantics" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a DATAFLOW & RUNTIME REVIEWER. Read-only — do not modify any file. - Review target: BUNDLE A + its BUNDLE C remediation hunks in the - uncommitted diff (`git diff HEAD`), per the upstream scope map. - - Criteria: - - error_class end-to-end: schema trigger literals -> projector -> store -> - status tool output -> wake node-line suffix -> wake failed-workflow digest - -> httpapi NodeResponse -> SDK DagNode. Same vocabulary everywhere. - - loop.ts wake digest after remediation: bound `store.getNodes`, - Effect.catchCause + logWarning fallback, type-predicate filter; verify no - lock/ordering hazards vs wake_reported persistence and no behavior - regression for completed/cancelled workflows (old terminal line intact). - - Double reporting (failed node in both node line and workflow digest): - acceptable or harmful for parent triage? - - Truncation (300/500 slices), digest size for many failed nodes. - - push_exhausted: reserved, never produced — confirm no producer exists and - surfaces handle its absence. - - Test pins: cancelled-node errorClass-null assertion, wake digest - attribution test, recovery errorClass assertions — adequate? - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-guidance - name: "Review: guidance docs vs runtime semantics" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a PROMPT-ACCURACY REVIEWER. Read-only — do not modify any file. - Review target: BUNDLE C guidance hunks — packages/core/src/plugin/command/ - workflow.md "Node failure triage" section (rewritten in the remediation - wave) + dag-flow.txt triage sentence + Budget Declaration wording — - checked against ACTUAL runtime behavior. These prompts steer parent-agent - repair decisions; any semantic drift is a bug. - - Verify every factual claim against code: - - timeout row: runtime cancels child session at deadline (spawn.ts); - partial-artifact advice accurate. - - exec_failed row (a)(b)(c) gating: do the cited reason strings/classes - exist exactly as written (recovery.ts, dag.ts terminateNonTerminalNodes - failReason propagation `required node(s) failed: ...`, loop.ts - orchestrator_unresponsive, template/condition errors)? Is the - per-reason response correct? - - verdict_fail row two shapes: ran-but-broke-contract vs never-ran pre-spawn - (unresolved placeholders loop.ts, review input contract) — confirm both - paths and that the fixes advised actually address them. - - cascade detection two shapes: required-failure leaves dependents pending - (check scheduler/transitions: do dependents stay pending or become - skipped?); optional-failure interpolation text `Dependency "X" failed:` - lands in dependent PROMPT text not error_reason (loop.ts resolveInputMapping). - - Qualifier "Every node failed via dag.node.failed carries an error_class" - + replan-cancel exception (projector NodeCancelled keeps error_class - null) — accurate? - - Value-set sentence (push_exhausted reserved) — matches schema + producers? - - Budget Declaration paragraph (defaults are floors; verifier/aggregator - lanes need 20-30min) — consistent with node_defaults usage elsewhere? - - Cross-doc consistency: Crash recovery / Escalation / Verdict Disposal - Contract / Bounded Repair / Resume-first — any contradiction? - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-goal-core - name: "Review: goal state management + loop correctness" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a CORE-CORRECTNESS REVIEWER for the restored /goal feature. - Read-only — do not modify any file. Review target: BUNDLE B core hunks: - packages/opencode/src/goal/* (goal.ts state service, state.ts, events.ts, - judge.ts, loop.ts GoalLoop, prompts.ts), core/src/goal/sql.ts + restore - migration, session/{prompt,session,system}.ts wiring, effect layer wiring - (app-runtime, bootstrap-runtime, project/bootstrap), tool/{registry,goal}.ts. - - Criteria: - - Goal state service: load/set/pause/resume/clear/markDone/subgoals - correctness; DB row shape vs GoalStateTable; event emission matches - schema SessionGoal.Definitions (goal.updated/goal.cleared) with correct - payload mapping. - - GoalLoop: idle-event subscription lifecycle (init via serviceOption in - bootstrap), continuation prompt flow, judge transport failure budget, - kick dispatch, turn budget/pause semantics; fiber cleanup on session end - (session.ts clear on remove). - - Layer wiring: Goal.defaultLayer placement (mergeAll group1), - GoalLoop.defaultLayer via provideMerge with self-provided deps (loop.ts - defaultLayer) — verify self-containment and memoMap dedup claims; - BootstrapLayer inclusion consistency with the bootstrap.ts lazy comment. - - prompt.ts dispatch: ordering (early return before commands.get), error - branch, announce/kick handling, startContext drain; no leftover - deprecation scaffolding. - - system.ts goal injection: degradation note vs active goal block; prompt - bloat behavior. - - Migration IF NOT EXISTS correctness for upgrade/fresh-DB paths. - - Compatibility with post-retire architecture (V2 session paths, hook - deferred-import cycle discipline). - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-goal-surface - name: "Review: goal HTTP/SDK/TUI surface" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are an API/UI-SURFACE REVIEWER for the restored /goal feature. - Read-only — do not modify any file. Review target: BUNDLE B surface hunks: - httpapi groups/session.ts goal endpoint + handlers/session.ts goal handler - + server.ts Goal.node, packages/sdk/js gen additions (Goal type + - session.goal method), packages/plugin/src/tui.ts TuiSidebarGoalItem - (Pick) + TuiState.goal, packages/tui sidebar/goal.tsx + - sync.tsx goal slice/reducer/hydration + builtins + adapters, command/index.ts - GOAL/SUBGOAL entries, httpapi-exercise goal scenario + runner/runtime/types. - - Criteria: - - Schema vs handler vs SDK alignment for session.goal (optionality, field - names goal/status/turnsUsed/maxTurns/subgoals/pausedReason). - - TUI: sync reducer cases for goal.updated/goal.cleared; hydration fetch - with catch fallback; adapters Number() coercion vs SDK JSON-Schema number - union; builtins registration order; sidebar widget renders only when goal - present. - - plugin tui.ts Pick — does the adapter still satisfy the type - (finite numbers assignable)? - - Exercise scenario assertions match handler behavior; runner goal helper + - runtime Goal module wiring (service resolvable at runtime — flag if the - scenario layer lacks Goal provision). - - Command entries: descriptions accurate, dispatch handled in prompt.ts - (template "" never reaches LLM path). - - SDK gen delta limited to goal additions (no unrelated drift). - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-tests - name: "Review: test coverage across all bundles" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a TEST-COVERAGE REVIEWER. Read-only — do not modify any file. - Review target: all test hunks in the uncommitted diff across BUNDLES A/B/C: - test/dag/{fixtures.ts,workflow-tool.test.ts,dag-wake-integration.test.ts, - dag-replan-stale-nodefailed.test.ts,dag-loop-recovery-integration.test.ts}, - test/goal/*, test/tool/goal-tool.test.ts, test/event-manifest.test.ts, - test/fixture/tui-plugin.ts, test/server/httpapi-exercise/*. - - Criteria: - - Bundle A pins: projector persistence, status output, wake digest - attribution (exact strings), cancelled-null invariant, recovery classes — - adequate and non-brittle? - - Bundle B: restored suites cover state service, judge parse/budget, - loop lifecycle (continue/done, dispatch-failure pause/resume), tool - status/complete; what CURRENT-architecture risks are untested (e.g. - /goal dispatch ordering vs /trust, bootstrap serviceOption absence path, - TUI reducer)? - - Bundle C: cancelled-null pin placement correct; recovery errorClass pin; - guidance-doc behavior has no executable guard (expected — note only). - - Hygiene: no duplicated logic into tests, mocks realistic, fixtures - representative, no tests asserting on unreachable states. - - Cross-bundle: any shared fixture change (fixtures.ts errorClass) breaking - other suites? - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line or test path", recommendation} - 2. unverified_claims: array of strings - 3. coverage_gaps: array of {path, untested_scenarios: string[]} - 4. summary: 2-3 sentences - - - id: review-style - name: "Review: style & repo conventions" - worker_type: general - depends_on: [scope-diff] - prompt_template: - inline: | - You are a STYLE & CONVENTIONS REVIEWER. Read-only — do not modify any file. - Review target: ALL uncommitted code hunks across BUNDLES A/B/C - (`git diff HEAD` + new files). - - Review against root AGENTS.md Style Guide and package AGENTS.md files: - - Comments: only non-obvious constraints; judge every new/changed comment. - - const over let, no else, no unnecessary destructuring, no import aliases, - functional style; Effect conventions (Effect.fn naming, catchCause vs - catch, no nested service yields). - - Layer invariants: self-contained defaultLayers, provideMerge placements, - LayerNode .node lists complete for every consumer of Goal/GoalLoop. - - Migrations: naming, id format, drizzle snake_case, registry freshness. - - Docs: workflow.md section placement/heading consistency, table - formatting; dag-flow.txt sentence integration; budget paragraph wording. - - Module shape: restored goal files put self-reexport at file top vs repo - convention (bottom) — flag with severity per repo rule, noting these are - faithful pre-retire restorations. - - httpapi: declared errors present, optional fields declared iff emitted. - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: verify-claims - name: "Verify Disputed Claims" - worker_type: general - depends_on: [review-error-class, review-guidance, review-goal-core, review-goal-surface, review-tests, review-style] - required: true - worker_config: - timeout_ms: 1800000 - output_schema: - type: object - required: [verdict, verified_claims, disputed_findings_resolution, critical_findings_status, coverage_gaps, evidence_quality, prune_audit] - properties: - verdict: - type: string - enum: [VERIFIED, GAPS, BLOCKED] - verified_claims: { type: array, items: { type: object } } - disputed_findings_resolution: { type: array, items: { type: object } } - critical_findings_status: { type: array, items: { type: object } } - coverage_gaps: { type: array, items: { type: object } } - evidence_quality: { type: string } - prune_audit: { type: array, items: { type: object } } - prompt_template: - inline: | - You are a CLAIM VERIFIER. Read-only — do not modify any file. - - Six reviewers produced findings and unverified_claims about the uncommitted - diff. You are the fresh-context verification wave. Check every - unverified/disputed/CRITICAL/HIGH claim against the actual code; sample - MEDIUM/LOW claims instead of trusting self-report. - - Upstream context contains all 6 reviewer outputs + the scope map. Extract: - 1. All unverified_claims items - 2. Findings where reviewers disagree (especially cross-bundle claims about - shared files: app-runtime, bootstrap, prompt.ts, registry, httpapi session group) - 3. All CRITICAL/HIGH findings (must be verified regardless) - 4. Every factual claim in review-guidance — these ALL need code confirmation - 5. Any diff area with no evidence-bearing reviewer output (AUDIT-FLAG files - from the scope map must be resolved) - - For each claim, read the actual source at the cited location and determine - CONFIRMED / REFUTED / PARTIALLY_CONFIRMED / UNRESOLVABLE with the exact line. - Record prune_audit verdicts for the scope manifest. - - Verdict: - - VERIFIED: every material scope/criterion covered, no material claim unresolved - - GAPS: a bounded fresh review can close named gaps - - BLOCKED: required evidence cannot be obtained - - Submit via submit_result. coverage_gaps must name the missing scope, - evidence, and the smallest reviewer lane to add in a LOOP. - - - id: verify-suite - name: "Verify: execute tests and gates" - worker_type: general - depends_on: [scope-diff] - required: true - output_schema: - type: object - required: [verdict, results, anomalies] - properties: - verdict: - type: string - enum: [PASS, FAIL, BLOCKED] - results: - type: array - items: - type: object - required: [gate, command, outcome, detail] - properties: - gate: { type: string } - command: { type: string } - outcome: { type: string, enum: [PASS, FAIL, SKIPPED] } - detail: { type: string } - anomalies: { type: array, items: { type: string } } - prompt_template: - inline: | - You are an OBJECTIVE GATE RUNNER. Execute commands and report faithfully. - Do NOT modify any file. Do not fix failures — report them. - - Gates (each from the stated directory): - 1. typecheck core: bun run typecheck (packages/core) - 2. typecheck opencode: bun run typecheck (packages/opencode) - 3. opencode DAG suites: bun test test/dag (packages/opencode) - 4. goal suites: bun test test/goal test/tool/goal-tool.test.ts (packages/opencode) - 5. core DAG suites: bun test test/dag-core.test.ts test/dag-projector-drift.test.ts test/dag-store-summaries.test.ts test/plugin/command.test.ts (packages/core) - 6. migration check: bun script/migration.ts --check (packages/core) - 7. HttpAPI contract: bun run test:httpapi --fail-on-missing (packages/opencode) - 8. SDK freshness: in packages/sdk/js run bun run build, then - `git diff -- packages/sdk/js/src/v2/gen`. Interpretation: this working - tree intentionally adds (uncommitted) Goal types + session.goal method + - DagNode.error_class — PASS iff the gen diff contains ONLY those intended - additions and no other drift. - - Record outcome + decisive output fragment per gate. Verdict PASS only if all - gates pass under that interpretation; FAIL lists each failing gate with - evidence; BLOCKED states why a gate cannot run. - - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [verify-claims, verify-suite] - required: true - report_to_parent: true - worker_config: - timeout_ms: 1200000 - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action, prune_decisions] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: { type: string } - description: { type: string } - evidence: { type: string } - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: { type: string } - loop_scope: - type: array - items: { type: string } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prune_decisions: - type: array - items: - type: object - required: [node, prune_reason, replacement_coverage] - properties: - node: { type: string } - prune_reason: { type: string } - replacement_coverage: { type: string } - prompt_template: - inline: | - You are the ARBITER for this joint deep review of the uncommitted diff. - You rule on VERIFIED evidence only. verify-claims + verify-suite outputs - are your primary evidence base; the 6 reviewer outputs and the scope map - are context. - - Your job: - 1. For each CONFIRMED finding, assess true severity - 2. Discard REFUTED claims; correct PARTIALLY_CONFIRMED descriptions - 3. Deduplicate findings sharing a root cause; rank by impact - 4. Weight guidance-doc inaccuracies as at least HIGH (they steer - parent-agent repair decisions); any objective-gate failure at least HIGH - 5. Accept (do not penalize) the admission-listed intentional adaptations; - resolve all AUDIT-FLAG files from the scope map - 6. Audit prune_decisions from the scope manifest (via prune_audit); missing - prune_reason/replacement_coverage forbids PASS - 7. Verdict: - - PASS: no unresolved material finding; scope and evidence coverage complete - - LOOP: a bounded targeted review can resolve specific omissions/disputes - - BLOCKED: evidence insufficient, critical contradiction unresolved, or ceiling reached - 8. LOOP names the minimal new scope in loop_scope; never rerun completed waves - 9. State reason, evidence, stop_reason, next_action explicitly - - Parent disposal contract: PASS -> finalize; LOOP -> pause/replan/resume - fresh targeted nodes; BLOCKED -> stop. Submit via submit_result. - - - id: deep-dive - name: "Plan the bounded fresh review loop" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "LOOP"' - required: true - report_to_parent: true - prompt_template: - inline: | - The arbiter required LOOP. Produce a minimal replan fragment proposal for a - NEW local review wave. Include only the missing or disputed scope from the - arbiter's loop_scope, assign NEW node IDs, preserve real artifact - dependencies, add a fresh verifier and a new arbiter, stay within caps. - Read-only: do not fix code. Keep proposals compact and copy-paste-ready as a - replan fragment — the parent applies it via pause/replan/resume. - - Return the loop reason, new nodes, dependencies, evidence each node must - collect, acceptance condition, and stop reason. - - - id: finalize-review - name: "Publish the accepted deep-review report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - Publish the final evidence-backed joint review report for the uncommitted - diff. Include: scope coverage per bundle (A error_class, B goal restoration, - C remediation), confirmed findings (severity-ranked with file:line - evidence), discarded/refuted claims, objective gate results, residual - low-risk issues, and the final PASS reason. Do not introduce new findings - or claims not verified upstream. diff --git a/.opencode/.dag-specs/deep-review-round3-continue.yaml b/.opencode/.dag-specs/deep-review-round3-continue.yaml deleted file mode 100644 index f03105cba7..0000000000 --- a/.opencode/.dag-specs/deep-review-round3-continue.yaml +++ /dev/null @@ -1,177 +0,0 @@ -title: "Round 3 final audit (continuation): re-run timed-out closure review, reuse green gate results" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "Continue the failed round-3 audit (dag_038dbcba0ffeAZRBDq2cwyIPr6): review-final timed out at 900s (environmental timeout class). Reuse the completed verify-suite result (8/8 PASS, persisted on disk); re-run only the closure review with a 30-minute budget, then arbitrate." - scope: - in: - - "review-final-2: closure verification of the five round-2 HIGH findings + regression scan (same mission, larger budget)" - - "arbitrate: final PASS/LOOP/BLOCKED on review-final-2 + persisted verify-suite gate results" - - "finalize-review on PASS" - out: - - "re-running the objective gate suite (verify-suite completed: 8/8 PASS)" - constraints: - - "reviewers are read-only; do not modify any file" - - "upstream gate result file is read-only input" - assumptions: - - "verify-suite result at .opencode/.dag-specs/review-parts-round3/verify-suite.md is complete and trustworthy (verdict PASS, 8/8 gates)" - - "review-final timeout was environmental (budget too small for the closure scan over ~40 files), fixed by raising timeout_ms to 1800000" - - "reused_nodes: verify-suite" - acceptance_criteria: - - "each round-2 HIGH finding verifiably closed with file:line evidence" - - "no NEW CRITICAL/HIGH on verified evidence" - - "arbiter emits structured PASS/LOOP/BLOCKED" - evidence_required: - - "file:line citations for closure claims" - - "persisted gate results as objective evidence" - risks: - - "review-final-2 could time out again despite 30-min budget" - review_plan: - - "review-final-2 (fresh context, reads working tree + gate result file)" - - "arbitrate (advanced tier, report_to_parent)" - - "PASS -> finalize-review" - open_questions: [] - blocking_questions: [] -config: - name: deep-review-round3-continue - max_concurrency: 2 - max_node_replan_attempts: 2 - max_total_nodes: 8 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - - id: review-final - name: "Review: remediation closure + regression scan (30min)" - worker_type: general - depends_on: [] - required: true - worker_config: - timeout_ms: 1800000 - prompt_template: - inline: | - You are a FRESH-CONTEXT FINAL REVIEWER. Read-only — do not modify any file. - Work efficiently: targeted reads/greps first, full reads only where needed. - - The repo's uncommitted working-tree diff contains a joint change set (DAG - error_class exposure, /goal restoration, two remediation waves). A prior - review round returned LOOP with five HIGH findings; a remediation wave claims - closure. Objective gates already ran: read - .opencode/.dag-specs/review-parts-round3/verify-suite.md (8/8 PASS). - - Verify each round-2 HIGH is closed (cite fixing lines): - 1. workflow.md "Cascade detection" section: required-failure shape must state - dependents are terminalized to `skipped` with error_reason - `workflow_failed` (pending only while paused) — cross-check against - dag.ts terminateNonTerminalNodes + loop.ts dag.fail wiring. - 2. workflow.md exec_failed row (c): must NOT rely on a surfaced workflow-level - reason or universal primary-node attribution; must carry an - orchestrator_unresponsive recipe (zero attribution; use status; - extend/replan or change approach). Cross-check dag.ts fail(), - loop.ts `required node(s) failed:` producer, wake digest visibility. - 3. dag-flow.txt: error_class sentence carries replan-cancel + pre-migration - exceptions. - 4. packages/opencode/test/session/prompt.test.ts: tests exist and meaningfully - cover /goal set+kick, /goal status, /subgoal, Goal-absent fallthrough - (judge assertion strength, not just existence). - 5. packages/opencode/src/session/system.ts: Goal.defaultLayer provided + - Goal.node in the LayerNode deps (goal block reachable); no import cycle - hazard vs the deferred SettingsHook pattern. - - Regression spot-checks (brief, evidence-based): - - error_class pipeline intact (projector, store, tool status, wake digest, - httpapi NodeResponse, SDK) - - app-runtime provideMerge comment accurate vs loop.ts/settings.ts - self-provides - - GOAL command description includes done; dispatch handles done (goal.ts) - - Known deferred follow-ups (non-blocking unless clearly material): GET - /session/:id/goal 200-null vs SDK Goal typing; httpapi error_class - field-level fixture; TUI sync reducer tests; GoalLoop e2e fixed sleeps. - - MANDATORY output format: - 1. closure: array of {round2_finding, status: CLOSED|PARTIAL|OPEN, evidence: "file:line", note} - 2. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} (NEW issues only) - 3. unverified_claims: array of strings - 4. summary: 2-3 sentences - - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [review-final] - required: true - report_to_parent: true - worker_config: - timeout_ms: 1200000 - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: { type: string } - description: { type: string } - evidence: { type: string } - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: { type: string } - loop_scope: - type: array - items: { type: string } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prompt_template: - inline: | - You are the ARBITER for the FINAL audit round of the joint uncommitted diff. - Rule on VERIFIED evidence only: review-final's closure verification + - the persisted gate results in - .opencode/.dag-specs/review-parts-round3/verify-suite.md (8/8 PASS). - - This is round 3 of a bounded loop (max_node_replan_attempts: 2). Judgment: - - PASS: all five round-2 HIGH findings CLOSED, gates PASS, no new - CRITICAL/HIGH; the documented deferred follow-ups remain explicitly - non-blocking (list them in evidence as accepted residual items) - - LOOP: at most ONE more bounded wave, only for a NEW or reopened HIGH with - concrete loop_scope; do not loop on deferred follow-ups or cosmetics - - BLOCKED: evidence missing or ceiling reached; report residual findings - - Deduplicate, discard REFUTED, rank by impact. State reason, evidence, - stop_reason, next_action explicitly. Submit via submit_result. - - - id: finalize-review - name: "Publish the final audit report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - Publish the final audit report for the joint uncommitted diff: closure table - for the round-2 HIGH findings (with fix evidence), gate results, confirmed - residual items with their accepted non-blocking status, and the PASS reason. - Do not introduce new findings. diff --git a/.opencode/.dag-specs/deep-review-round3-final.yaml b/.opencode/.dag-specs/deep-review-round3-final.yaml deleted file mode 100644 index 68117d7d84..0000000000 --- a/.opencode/.dag-specs/deep-review-round3-final.yaml +++ /dev/null @@ -1,223 +0,0 @@ -title: "Round 3 final audit: confirm remediation closure on the joint diff" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "Final audit round of the uncommitted joint diff: verify every round-2 LOOP finding is closed by in-tree remediation, re-run all objective gates, and confirm no unresolved material bug remains" - scope: - in: - - "Round-2 HIGH cluster resolutions: workflow.md cascade rewrite (skipped terminalization + paused-pending nuance), exec_failed (c) row no-attribution rewrite + orchestrator_unresponsive recipe, dag-flow.txt exception clause" - - "Round-2 test item: new prompt.test.ts goal/subgoal dispatch tests (set+kick loop, status, subgoal, Goal-absent fallthrough)" - - "Round-2 MEDIUM follow-up pulled into the fix wave: SystemPrompt Goal.node + Goal.defaultLayer wiring so the goal system block renders" - - "LOW cleanups: app-runtime provideMerge comment correction, GOAL command description 'done'" - - "Regression spot-check over previously reviewed areas (error_class pipeline, wake digest, goal restoration surfaces)" - out: - - "Arbiter-deferred follow-ups explicitly accepted as non-blocking: GET /session/:id/goal null-vs-Goal typing, httpapi error_class field-level fixture, TUI sync reducer tests, GoalLoop e2e fixed sleeps" - - "committed baseline behavior outside the diff" - constraints: - - "reviewers are read-only; do not modify any file" - - "every material finding must cite file:line evidence" - assumptions: - - "the remediation wave is complete: docs rewritten, SystemPrompt wired, 4 dispatch tests added and passing locally" - - "deferred follow-ups are documented findings, not unresolved bugs, unless re-graded material by this round" - acceptance_criteria: - - "each round-2 HIGH finding verifiably closed (quote the fixing code/doc line)" - - "no NEW CRITICAL/HIGH finding on verified evidence" - - "all objective gates PASS under the SDK-freshness interpretation (intended uncommitted additions allowed)" - evidence_required: - - "file:line citations for closure claims" - - "executed gate results" - risks: - - "remediation wording introducing fresh guidance-vs-runtime drift" - - "SystemPrompt wiring changing layer construction for existing consumers" - review_plan: - - "wave 1: fresh-context remediation review (closure verification + regression scan)" - - "wave 2: objective gate runner" - - "wave 3: advanced-tier arbiter PASS/LOOP/BLOCKED" - open_questions: [] - blocking_questions: [] -config: - name: deep-review-round3-final - max_concurrency: 4 - max_node_replan_attempts: 2 - max_total_nodes: 10 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - - id: review-final - name: "Review: remediation closure + regression scan" - worker_type: general - depends_on: [] - required: true - prompt_template: - inline: | - You are a FRESH-CONTEXT FINAL REVIEWER. Read-only — do not modify any file. - The repo's uncommitted working-tree diff contains a joint change set (DAG - error_class exposure, /goal restoration, two remediation waves). A prior - review round returned LOOP with five HIGH findings; a remediation wave claims - they are closed. Your job is closure verification + regression scan. - - Verify each round-2 HIGH is actually closed (cite the fixing lines): - 1. workflow.md "Cascade detection" section: the required-failure shape must - state dependents are terminalized to `skipped` with error_reason - `workflow_failed` (pending only while paused) — check this matches - dag.ts terminateNonTerminalNodes + loop.ts dag.fail wiring. - 2. workflow.md exec_failed row (c): must NOT reference a surfaced - workflow-level reason or a universally-available primary-node attribution; - must carry an orchestrator_unresponsive recipe (zero attribution; use - status; extend/replan or change approach). Cross-check against - dag.ts fail() + loop.ts:272 `required node(s) failed:` producer and the - wake digest surface (loop.ts failuresByWorkflow) to confirm what IS visible. - 3. dag-flow.txt: the error_class sentence must carry the replan-cancel and - pre-migration exceptions. - 4. packages/opencode/test/session/prompt.test.ts: new tests must exist and - meaningfully cover /goal set+kick, /goal status, /subgoal, and Goal-absent - fallthrough (read the tests; judge assertion strength, not just existence). - 5. SystemPrompt wiring: packages/opencode/src/session/system.ts must provide - Goal.defaultLayer + Goal.node so the goal block is reachable; verify no - layer-cycle or construction-order hazard (goal imports must not close a - TDZ cycle like the deferred SettingsHook pattern guards against). - - Regression scan (spot-check, evidence-based): - - error_class pipeline untouched by the remediation wave? (projector, store, - tool status, wake digest, httpapi NodeResponse, SDK) - - app-runtime provideMerge comment now accurate vs loop.ts/settings.ts - self-provides? - - GOAL command description includes done; dispatch handles done (goal.ts)? - - Known deferred follow-ups (do NOT re-grade unless clearly material): - GET /session/:id/goal returns 200 JSON null vs SDK Goal (non-null); httpapi - error_class field-level fixture absent; TUI sync reducer untested; GoalLoop - e2e fixed sleeps. - - MANDATORY output format: - 1. closure: array of {round2_finding, status: CLOSED|PARTIAL|OPEN, evidence: "file:line", note} - 2. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} (NEW issues only) - 3. unverified_claims: array of strings - 4. summary: 2-3 sentences - - - id: verify-suite - name: "Verify: execute all gates" - worker_type: general - depends_on: [] - required: true - output_schema: - type: object - required: [verdict, results, anomalies] - properties: - verdict: - type: string - enum: [PASS, FAIL, BLOCKED] - results: - type: array - items: - type: object - required: [gate, command, outcome, detail] - properties: - gate: { type: string } - command: { type: string } - outcome: { type: string, enum: [PASS, FAIL, SKIPPED] } - detail: { type: string } - anomalies: { type: array, items: { type: string } } - prompt_template: - inline: | - You are an OBJECTIVE GATE RUNNER. Execute commands and report faithfully. - Do NOT modify any file. Do not fix failures — report them. - - Gates (each from the stated directory): - 1. typecheck core: bun run typecheck (packages/core) - 2. typecheck opencode: bun run typecheck (packages/opencode) - 3. opencode DAG suites: bun test test/dag (packages/opencode) - 4. goal + dispatch suites: bun test test/goal test/tool/goal-tool.test.ts test/session/prompt.test.ts (packages/opencode) - 5. core suites: bun test test/dag-core.test.ts test/dag-projector-drift.test.ts test/dag-store-summaries.test.ts test/plugin/command.test.ts (packages/core) - 6. migration check: bun script/migration.ts --check (packages/core) - 7. HttpAPI contract: bun run test:httpapi --fail-on-missing (packages/opencode) - 8. SDK freshness: in packages/sdk/js run bun run build, then - `git diff -- packages/sdk/js/src/v2/gen`. Interpretation: the working - tree intentionally adds (uncommitted) Goal types + session.goal method + - DagNode.error_class — PASS iff the gen diff contains ONLY those intended - additions and no other drift. - - Record outcome + decisive output fragment per gate. Verdict PASS only if all - gates pass under the interpretation; FAIL lists failing gates with evidence; - BLOCKED states why. - - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [review-final, verify-suite] - required: true - report_to_parent: true - worker_config: - timeout_ms: 1200000 - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: { type: string } - description: { type: string } - evidence: { type: string } - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: { type: string } - loop_scope: - type: array - items: { type: string } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prompt_template: - inline: | - You are the ARBITER for the FINAL audit round of the joint uncommitted diff. - Rule on VERIFIED evidence only: review-final's closure verification + - verify-suite's gate results. - - This is round 3 of a bounded loop (max_node_replan_attempts: 2). Judgment: - - PASS: all five round-2 HIGH findings CLOSED, all gates PASS, no new - CRITICAL/HIGH; the documented deferred follow-ups remain explicitly - non-blocking (list them in evidence as accepted residual items) - - LOOP: at most ONE more bounded wave, only for a NEW or reopened HIGH with - concrete loop_scope; do not loop on deferred follow-ups or cosmetics - - BLOCKED: evidence missing or ceiling reached; report residual findings - - Deduplicate, discard REFUTED, rank by impact. State reason, evidence, - stop_reason, next_action explicitly. Submit via submit_result. - - - id: finalize-review - name: "Publish the final audit report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - Publish the final audit report for the joint uncommitted diff: closure table - for the round-2 HIGH findings (with fix evidence), gate results, confirmed - residual items with their accepted non-blocking status, and the PASS reason. - Do not introduce new findings. diff --git a/.opencode/.dag-specs/final-confirmation-continue.yaml b/.opencode/.dag-specs/final-confirmation-continue.yaml deleted file mode 100644 index 949732e703..0000000000 --- a/.opencode/.dag-specs/final-confirmation-continue.yaml +++ /dev/null @@ -1,203 +0,0 @@ -title: "Final confirmation (continuation): split post-remediation review, reuse 3 completed lanes" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "Continue the failed final-confirmation workflow (dag_035b6a590ffeyPe2ofvgYNTwXR): the monolithic post-remediation reviewer timed out at 1200s for the third consecutive time — CHANGED APPROACH per Escalation: split it into two small parallel lanes instead of retrying a bigger monolith. Reuse the 3 completed lane outputs (config-repo review, stack integrity, 9/9 gate results) persisted on disk." - scope: - in: - - "review-remediation-docs: workflow.md cascade wording, prompt.test.ts fall-through positive assertions, dag-replan paused required-failure test, e2e-loop SUBSCRIPTION_SETTLE_MS" - - "review-remediation-contract: goal 404 handler + route error declaration + consumer tolerance audit (sync.tsx hydration + any session.goal callers), httpapi-exercise goal.absent scenario + dagFailNode fixture + wire assertions, sync-goal reducer tests" - - "arbitrate over all five lane outputs (2 new + 3 persisted)" - out: - - "re-running gates (9/9 PASS persisted), re-reviewing config-repo/stack lanes (completed)" - constraints: - - "reviewers are read-only; do not modify any file" - - "persisted upstream lane outputs are read-only inputs" - assumptions: - - "persisted outputs complete: .opencode/.dag-specs/review-parts-final/{review-config-repo.md, review-stack.md, verify-suite.md}" - - "reused_nodes: review-config-repo, review-stack, verify-suite" - - "timeout root cause was monolithic lane size, not budget alone; splitting is the approach change" - acceptance_criteria: - - "every R1-R10 remediation item reviewed by one of the two new lanes with file:line evidence" - - "no NEW CRITICAL/HIGH on verified evidence" - - "arbiter emits PASS/LOOP/BLOCKED" - evidence_required: - - "file:line citations" - - "persisted gate results (9/9 PASS)" - risks: - - "split lanes missing an item at the boundary between docs/tests and contract/wiring" - review_plan: - - "two parallel small reviewers (explicit per-item assignment, boundary covered twice where cheap)" - - "arbiter with persisted upstream context" - - "finalize on PASS" - open_questions: [] - blocking_questions: [] -config: - name: final-confirmation-continue - max_concurrency: 4 - max_node_replan_attempts: 2 - max_total_nodes: 8 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - - id: review-remediation-docs - name: "Review: remediation docs + test assertions" - worker_type: general - depends_on: [] - required: true - prompt_template: - inline: | - You are a FRESH-CONTEXT REVIEWER. Read-only — do not modify any file. - Branch feat/goal-restore holds the final state. Review FOUR remediation - items (post-round-3 fixes, gate-verified but never agent-reviewed): - - 1. packages/core/src/plugin/command/workflow.md "Cascade detection": - required-failure shape says pending/queued/paused dependents are - terminalized to `skipped` with error_reason workflow_failed, untouched - only while the workflow stays paused. Cross-check against - packages/opencode/src/dag/dag.ts terminateNonTerminalNodes (failRunning - branch fails running nodes, else-branch skips every other non-terminal) - and the PAUSED->FAILED guard rejection. - 2. packages/opencode/test/session/prompt.test.ts goal-absent fall-through - test: positive assertions ("/goal orphan request" persisted + exactly 1 - LLM input). Verify they cannot pass vacuously: trace the registry path - in prompt.ts (empty template -> text part + single prompt/loop run). - 3. packages/opencode/test/dag/dag-replan-stale-nodefailed.test.ts paused - required-failure test: scenario constructs the paused window correctly - (pause before settlement; empty reply fails the required node), and the - assertions prove pending-during-pause then skipped(workflow_failed) - after resume. - 4. packages/opencode/test/goal/e2e-loop.test.ts SUBSCRIPTION_SETTLE_MS: - constant used at all three former sleep sites; comment claims match - loop.ts forkScoped subscription wiring and the absence of a readiness - latch. - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-remediation-contract - name: "Review: remediation contract + fixture wiring" - worker_type: general - depends_on: [] - required: true - prompt_template: - inline: | - You are a FRESH-CONTEXT REVIEWER. Read-only — do not modify any file. - Branch feat/goal-restore holds the final state. Review FOUR remediation - items (post-round-3 fixes, gate-verified but never agent-reviewed): - - 1. Goal endpoint 404 contract: packages/opencode/src/server/.../handlers/ - session.ts goal handler fails with notFound(...) when goalless; verify - `notFound` import, ApiNotFoundError declared in the goal route's error - list (groups/session.ts), wire shape honesty (200 always carries Goal). - CONSUMER AUDIT: find every session.goal call site (SDK client usage in - packages/tui — sync.tsx hydration .catch fallback — and anywhere else) - and verify each tolerates 404. - 2. httpapi-exercise session.goal.absent scenario: 404 expectation matches - handler behavior; seeded goalless session. - 3. httpapi-exercise error_class fixture: runner.ts failDagNodeFixture - (Dag.Service.nodeFailed via run() wrapper — check run() provides the app - layer so no residual requirement leaks), types.ts dagFailNode signature, - index.ts dag.nodes scenario seeds a timeout failure and asserts - error_class present on failed node / absent on pristine node. - 4. packages/tui/test/cli/cmd/tui/sync-goal.test.tsx: emitted events match - the schema definitions (goal.updated carries goal Info object; - goal.cleared carries sessionID only); reducer assertions cover write / - replace / clear transitions. - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [review-remediation-docs, review-remediation-contract] - required: true - report_to_parent: true - worker_config: - timeout_ms: 1200000 - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: { type: string } - description: { type: string } - evidence: { type: string } - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: { type: string } - loop_scope: - type: array - items: { type: string } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prompt_template: - inline: | - You are the ARBITER for the final confirmation review of the three-PR stack - (#171 config-repo, #169 error_class, #170 goal restore). - - Evidence base — read all five inputs: - - Two new reviewer outputs (upstream context): review-remediation-docs, - review-remediation-contract - - Three persisted completed lanes from the interrupted workflow: - .opencode/.dag-specs/review-parts-final/review-config-repo.md - .opencode/.dag-specs/review-parts-final/review-stack.md - .opencode/.dag-specs/review-parts-final/verify-suite.md (9/9 gates PASS, - including SDK regen zero-diff determinism) - - Judgment: - - PASS: no CRITICAL/HIGH on verified evidence across all five lanes; gates - PASS; stack attribution sound; config-repo mechanism sound - - LOOP: at most ONE bounded wave for concrete HIGH findings (name loop_scope); - do NOT loop on MEDIUM/LOW or deferred follow-ups - - BLOCKED: evidence missing or ceiling reached; report residuals - - Deduplicate, discard REFUTED, rank by impact. State reason, evidence, - stop_reason, next_action explicitly. Submit via submit_result. - - - id: finalize-review - name: "Publish the final confirmation report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - Publish the final confirmation report for the three-PR stack. Include: - per-PR review coverage, the five lane conclusions, gate results (9/9), - confirmed residual items with accepted non-blocking status, and the PASS - reason declaring the stack merge-ready. Write the report to - .opencode/.dag-specs/review-parts-final/final-confirmation-report.md and - submit a concise summary as your final output. Do not introduce new findings. diff --git a/.opencode/.dag-specs/final-confirmation-three-pr-stack.yaml b/.opencode/.dag-specs/final-confirmation-three-pr-stack.yaml deleted file mode 100644 index f293b6836c..0000000000 --- a/.opencode/.dag-specs/final-confirmation-three-pr-stack.yaml +++ /dev/null @@ -1,291 +0,0 @@ -title: "Final confirmation review: three-PR stack (config-repo, error_class, goal restore)" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "Final confirmation review closing the last two review gaps before merge: (1) the post-round-3 remediation wave (R1-R10 fixes) which was gate-verified but never agent-reviewed, and (2) the config-repo template mechanism (#171) whose original deep review was interrupted and never reached a verdict" - scope: - in: - - "Post-remediation delta: workflow.md paused/queued/paused wording; prompt.test.ts positive fall-through assertions; dag-replan-stale-nodefailed.test.ts paused required-failure test; handlers/session.ts goal 404 + session.goal.absent scenario; httpapi-exercise dagFailNode fixture (runner/types/index); tui sync-goal.test.tsx; e2e-loop.test.ts SUBSCRIPTION_SETTLE_MS" - - "Config-repo mechanism commits 676e0463e, 6ffc7a712, 98e4c0624, 2ee59d874: release-fork.yml package-templates job, dag-flow.txt library scope text, /dag-template-update command template + registration, builtin template embedding (workflows.ts + build injection)" - - "Stack integrity: per-branch content attribution (feat/dag-config-repo / feat/dag-error-class / feat/goal-restore), generated-file coherence per branch, committed .opencode artifacts hygiene (no secrets)" - - "Objective gates at feat/goal-restore HEAD (full tree): typecheck, test suites, httpapi contract, migration check, SDK regen determinism" - out: - - "Re-reviewing already-adjudicated round-1/2/3 findings (closure already PASS-adjudicated)" - - "opencode-dag-config remote repo content itself" - constraints: - - "reviewers are read-only; do not modify any file" - - "every material finding must cite file:line evidence" - assumptions: - - "current branch feat/goal-restore contains the complete stacked final state" - - "prior adjudications stand: round-3 PASS on the pre-remediation state; R1-R10 argumentations recorded in-session" - acceptance_criteria: - - "no NEW CRITICAL/HIGH on verified evidence across remediation delta and config-repo mechanism" - - "config-repo mechanism verified sound: release job, update command, library resolution tier" - - "all objective gates PASS at HEAD" - - "stack branches carry exactly their attributed bundles" - evidence_required: - - "file:line citations" - - "executed gate results" - risks: - - "remediation fixes introducing subtle contract drift (goal 404 vs consumers)" - - "release-template job failure modes never exercised" - review_plan: - - "wave 1: three parallel reviewers (post-remediation delta, config-repo mechanism, stack integrity)" - - "wave 2: objective gate runner" - - "wave 3: arbiter PASS/LOOP/BLOCKED with bounded continuation" - open_questions: [] - blocking_questions: [] -config: - name: final-confirmation-three-pr-stack - max_concurrency: 6 - max_node_replan_attempts: 2 - max_total_nodes: 12 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - - id: review-post-remediation - name: "Review: post-round-3 remediation delta" - worker_type: general - depends_on: [] - required: true - worker_config: - timeout_ms: 1200000 - prompt_template: - inline: | - You are a FRESH-CONTEXT REVIEWER. Read-only — do not modify any file. - Target: the remediation wave applied AFTER the round-3 PASS adjudication of - the joint diff. These fixes were gate-verified but never agent-reviewed. - Current branch (feat/goal-restore) contains the final committed state. - - Review each fix for correctness, regressions, and contract drift: - 1. packages/core/src/plugin/command/workflow.md "Cascade detection": - required-failure shape says dependents pending/queued/PAUSED are skipped - with error_reason workflow_failed (untouched only while workflow paused). - Cross-check dag.ts terminateNonTerminalNodes (failRunning branch vs - NodeSkipped branch) and the PAUSED->FAILED guard. - 2. packages/opencode/test/session/prompt.test.ts goal-absent fall-through - test: positive assertions (/goal orphan request persisted + exactly 1 LLM - input). Check they cannot pass vacuously and match the registry path - behavior in prompt.ts (empty template expansion). - 3. packages/opencode/test/dag/dag-replan-stale-nodefailed.test.ts paused - required-failure test: scenario valid (pause before failure settlement, - empty-reply failing the node), assertions prove the paused nuance. - 4. packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts - goal handler returns notFound when goalless (was 200+null): check error - import, ApiNotFoundError declared on the route (groups/session.ts), and - EVERY consumer tolerates 404 — audit packages/tui/src/context/sync.tsx - hydration (.catch fallback) and any other session.goal call sites. - 5. httpapi-exercise: session.goal.absent scenario (404 expectation), - dag.nodes scenario seeded failure (failDagNodeFixture in runner.ts via - Dag.Service.nodeFailed + run() wrapper), wire assertions - (error_class present on failed, absent on pristine). - 6. packages/tui/test/cli/cmd/tui/sync-goal.test.tsx: events match schema - (goal.updated properties goal object; goal.cleared), reducer assertions - cover write/replace/clear. - 7. packages/opencode/test/goal/e2e-loop.test.ts SUBSCRIPTION_SETTLE_MS - constant: comment accuracy vs loop.ts forkScoped; all three waits use it. - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-config-repo - name: "Review: config-repo template mechanism (#171)" - worker_type: general - depends_on: [] - required: true - prompt_template: - inline: | - You are a FRESH-CONTEXT REVIEWER. Read-only — do not modify any file. - Target: the config-repo reference-template mechanism on branch - feat/dag-config-repo (commits 676e0463e, 6ffc7a712, 98e4c0624, 2ee59d874, - plus chore commit 494b3b463 artifacts). Its original deep review was - interrupted; this is the first completed review. Use `git show ` - and read current files on this branch. - - Review dimensions: - 1. .github/workflows/release-fork.yml package-templates/sync job: clone of - LeXwDeX/opencode-dag-config at release time, packaging dag-templates - asset, write-safety (must not touch main repo contents outside the - asset), failure behavior (job failure vs release), pinning (branch/tag/ - SHA?) and supply-chain trust implications. - 2. /dag-template-update command: packages/core/src/plugin/command.ts - registration + packages/core/src/plugin/command/dag-template-update.txt - template — the update flow (first-run clone, pull --ff-only, or zip - download per commit message), preview classification - (NEW/UNCHANGED/UPDATE), backup-before-overwrite, QA decision gate, - directory assumptions (/workflows), failure modes (network, - dirty target dir, divergence). - 3. Library resolution: builtin scope as third tier in command/workflows.ts - (project overrides global overrides builtin), list/resolve coverage, - dag-flow.txt guidance text consistency with actual resolution order. - 4. Build injection: how OPENCODE_DAG_TEMPLATES is injected (generate/build - scripts touched by 98e4c0624) — determinism, embedding correctness. - 5. Fix commit 2ee59d874: what review findings did it address, are they - actually resolved? - 6. Chore artifacts (494b3b463 .opencode/.dag-specs + workflows yamls): - YAML sanity scan + NO secrets/credentials/absolute-path leakage - (grep for tokens, keys, ~/.local/share paths that would break for others). - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: review-stack - name: "Review: three-PR stack integrity" - worker_type: general - depends_on: [] - prompt_template: - inline: | - You are a FRESH-CONTEXT STACK-INTEGRITY REVIEWER. Read-only — do not - modify any file. Three stacked PR branches carry the joint change set: - - feat/dag-config-repo (PR #171, base dev): config-repo mechanism + - workflow library + review artifacts - - feat/dag-error-class (PR #169, base feat/dag-config-repo): error_class - exposure + triage guidance + its remediation - - feat/goal-restore (PR #170, base feat/dag-error-class): /goal restoration - + its remediation - - Verify (git commands: git log/diff between branches): - 1. Attribution: diff(feat/dag-config-repo -> feat/dag-error-class) contains - ONLY error_class-bundle files; diff(feat/dag-error-class -> - feat/goal-restore) contains ONLY goal-bundle files. List any file that - appears in the wrong layer (generated files included — migration registry, - schema baseline, SDK gen must be layered correctly: error_class-only - additions before goal additions). - 2. Independence: feat/dag-error-class compiles conceptually without goal - (no dangling goal imports in its tree — grep for @/goal references in - files attributed to it). - 3. Generated coherence: migration.gen.ts on feat/dag-error-class lists only - the error_class migration; on feat/goal-restore both; schema baseline and - SDK gen likewise layered. - 4. No cross-branch leftovers: nothing from the wip snapshot committed by - accident (compare feat/goal-restore against expectations). - - MANDATORY output format: - 1. findings: array of {severity: CRITICAL|HIGH|MEDIUM|LOW, title, description, evidence: "file:line or git range", recommendation} - 2. unverified_claims: array of strings - 3. summary: 2-3 sentences - - - id: verify-suite - name: "Verify: execute all gates at HEAD" - worker_type: general - depends_on: [] - required: true - output_schema: - type: object - required: [verdict, results, anomalies] - properties: - verdict: - type: string - enum: [PASS, FAIL, BLOCKED] - results: - type: array - items: - type: object - required: [gate, command, outcome, detail] - properties: - gate: { type: string } - command: { type: string } - outcome: { type: string, enum: [PASS, FAIL, SKIPPED] } - detail: { type: string } - anomalies: { type: array, items: { type: string } } - prompt_template: - inline: | - You are an OBJECTIVE GATE RUNNER. Execute and report faithfully. Do NOT - modify any file. Current branch feat/goal-restore holds the full stacked - state. Gates (from the stated directories): - 1. typecheck core: bun run typecheck (packages/core) - 2. typecheck opencode: bun run typecheck (packages/opencode) - 3. typecheck tui: bun run typecheck (packages/tui) - 4. opencode suites: bun test test/dag test/goal test/tool/goal-tool.test.ts test/session/prompt.test.ts (packages/opencode) - 5. core suites: bun test test/dag-core.test.ts test/dag-projector-drift.test.ts test/dag-store-summaries.test.ts test/plugin/command.test.ts (packages/core) - 6. tui sync suites: bun test test/cli/cmd/tui/sync-goal.test.tsx test/cli/cmd/tui/sync-dag.test.tsx (packages/tui) - 7. migration check: bun script/migration.ts --check (packages/core) - 8. HttpAPI contract: bun run test:httpapi --fail-on-missing (packages/opencode) - 9. SDK regen determinism: in packages/sdk/js run bun run build, then - `git diff -- packages/sdk/js/src/v2/gen` — PASS iff no diff at all (the - committed gen files must be exactly reproduced). - Record outcome + decisive fragment per gate. Verdict PASS only if all pass; - FAIL lists failing gates; BLOCKED states why. - - - id: arbitrate - name: "Arbiter: Final Verdict" - worker_type: general - depends_on: [review-post-remediation, review-config-repo, review-stack, verify-suite] - required: true - report_to_parent: true - worker_config: - timeout_ms: 1200000 - output_schema: - type: object - required: [verdict, reason, evidence, findings, loop_scope, stop_reason, next_action] - properties: - verdict: - type: string - enum: [PASS, LOOP, BLOCKED] - reason: { type: string } - evidence: - type: array - items: { type: string } - findings: - type: array - items: - type: object - required: [severity, title, evidence, status] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - title: { type: string } - description: { type: string } - evidence: { type: string } - status: - type: string - enum: [CONFIRMED, REFUTED, PARTIALLY_CONFIRMED] - recommendation: { type: string } - loop_scope: - type: array - items: { type: string } - stop_reason: - type: string - enum: [goal_met, correction_required, evidence_missing, no_progress, round_cap, budget_cap] - next_action: - type: string - enum: [finalize, replan, stop] - prompt_template: - inline: | - You are the ARBITER for the final confirmation review of the three-PR stack. - Rule on VERIFIED evidence only: three reviewer outputs + gate results. - - Judgment: - - PASS: no CRITICAL/HIGH on verified evidence; gates PASS; stack layers - correctly attributed; config-repo mechanism sound (release job safe, - update command robust, resolution tiers correct, no secrets in artifacts) - - LOOP: at most ONE bounded wave for concrete HIGH findings (name loop_scope) - - BLOCKED: evidence missing or ceiling reached; report residuals - - Deduplicate, discard REFUTED, rank by impact. State reason, evidence, - stop_reason, next_action explicitly. Submit via submit_result. - - - id: finalize-review - name: "Publish the final confirmation report" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict == "PASS"' - required: true - report_to_parent: true - prompt_template: - inline: | - Publish the final confirmation report for the three-PR stack: per-PR - review coverage status, gate results, confirmed residual items (accepted - non-blocking), and the PASS reason declaring the stack merge-ready. Do not - introduce new findings. diff --git a/.opencode/.dag-specs/review-parts-diff/review-contract.md b/.opencode/.dag-specs/review-parts-diff/review-contract.md deleted file mode 100644 index 8a16b8c6dc..0000000000 --- a/.opencode/.dag-specs/review-parts-diff/review-contract.md +++ /dev/null @@ -1 +0,0 @@ -"# API Contract Review — httpapi + SDK + contract obligations\n\n## 1. findings\n\n- **{severity: LOW, title: \"httpapi-exercise scenarios not updated for the NodeResponse shape change — functionally inert, but the AGENTS.md contract letter is unmet\"}**\n - description: AGENTS.md obliges scenario updates when a route response shape changes. The dag scenarios under `packages/opencode/test/server/httpapi-exercise/index.ts:1807-1850` assert with spot checks only (`check(typeof n.status === \"string\")`, `Array.isArray(n.depends_on)`, `check(body.id === \"n1\")`), never exact-shape equality — an additive optional field cannot break them. Route-coverage enforcement (`--fail-on-missing`/`--fail-on-skip`, routing.ts:50-51; the package.json script uses `--fail-on-skip`) only guards route presence, not field shape. Functionally CI stays green; but the new persisted field is never exercised over HTTP (fixtures create nodes that never fail, so `error_class` is always null on the wire in tests).\n - evidence: \"packages/opencode/test/server/httpapi-exercise/index.ts:1822-1832\", \"packages/opencode/test/server/httpapi-exercise/index.ts:1845-1849\", \"packages/opencode/package.json test:httpapi\", \"AGENTS.md (repo root, Extending the Codebase)\"\n - recommendation: add a `check(typeof n.error_class === \"string\")`-style assertion only if a failing-node fixture is feasible; otherwise document (in the scenario comment) that optional failure-class fields are covered by the schema-level contract and the SDK typecheck. No CI risk either way.\n\n- **{severity: LOW, title: \"error_class value set is unconstrained on every read surface — wire contract enforces no enum\"}**\n - description: The 4-value set (`exec_failed`/`push_exhausted`/`verdict_fail`/`timeout`) exists only at the event source `packages/schema/src/dag-event.ts:253` (Schema.Literals) and in comments (sql.ts:65, groups/dag.ts:42-43). NodeResponse declares `Schema.optional(Schema.String)` (groups/dag.ts:44) and the SDK type is plain `error_class?: string` (types.gen.ts:3909). Field-level alignment is exact (see summary), but any malformed/stale persisted value would pass through projector → store → handler → HTTP/SDK unvalidated, and guidance docs enumerate only 3 of 4 values.\n - evidence: \"packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts:44\", \"packages/sdk/js/src/v2/gen/types.gen.ts:3909\", \"packages/schema/src/dag-event.ts:253\"\n - recommendation: optionally derive the HTTP field from the shared trigger literal (export a `FailedTrigger` union from packages/schema and reuse in NodeResponse + `error_class` comments) so the wire type constrains the set; acceptable to leave as `string` since pass-through persistence is intentional.\n\n- **{severity: LOW, title: \"TUI consumers neither break on nor display error_class — triage field invisible in the inspector\"}**\n - description: All DagNode consumers were checked: `dag-inspector.tsx:659-661` renders only `error_reason` via `formatDagError` (dag-inspector-utils.ts:64-66, verbatim string munging); `dag-panel.tsx` uses status/glyph only (lines 81-83); `dag-inspector-utils.ts:18-61` (computeWaves/computeNodeRowIndex) is field-agnostic. The additive optional field breaks nothing (no exhaustive destructuring, no `satisfies`). However the new field — the primary triage input for the workflow.md guidance — cannot be shown by the failure pane until a TUI change lands; consistent with the diff's scope-out (no TUI files modified).\n - evidence: \"packages/tui/src/feature-plugins/system/dag-inspector.tsx:659-661\", \"packages/tui/src/feature-plugins/sidebar/dag-panel.tsx:81-83\"\n - recommendation: none required for this diff; note as follow-up: the inspector's failure pane could display `error_class` alongside the reason string once TUI changes are in scope.\n\n- **{severity: LOW, title: \"Untracked regenerated openapi.json build artifact visible in git status\"}**\n - description: `packages/sdk/js/openapi.json` was removed from tracking by commit 881ca8643 (\"chore: generate\", pure deletion) and is not covered by any .gitignore; each SDK build regenerates it, so it perpetually shows as `??` and an accidental `git add -A` would re-add it. It is inert for CI: `check:generated` diffs only `src/v2/gen` (packages/sdk/js/package.json:10).\n - evidence: \"packages/sdk/js/openapi.json (untracked)\", \"packages/sdk/js/package.json:10\", \"git show 881ca8643\"\n - recommendation: add `openapi.json` to `packages/sdk/js/.gitignore` to prevent accidental commits.\n\n## 2. unverified_claims\n\n- The claim that httpapi-exercise scenarios remain green with the new optional field is inferred from reading the assertion logic (spot checks cannot fail on an additive optional field), not from executing `bun run test:httpapi`.\n- \"packages/client has no dag surface, hence nothing to drift\" — verified by grep (zero case-insensitive `dag` matches across `packages/client/src`), not by running the client generator; the client `check:generated` guards `src/generated src/generated-effect` (package.json:13), both untouched.\n- I did not execute `bun typecheck` in packages/opencode; the handler/schema alignment claim rests on static comparison of the mapper object literal against the Schema.Struct declaration.\n\n## 3. summary\n\nField-level alignment is exact end-to-end: `NodeRow.errorClass` (store.ts:42) → handler emits `error_class` only when non-null (handlers/dag.ts:65) → `Schema.optional(Schema.String)` declared (groups/dag.ts:44) → SDK `error_class?: string` (types.gen.ts:3909) — all 16 NodeResponse fields match the mapper 1:1, nothing emitted-but-undeclared and nothing declared-but-never-emitted, so the encoder strips nothing unintended. Regeneration freshness holds: `git diff HEAD -- packages/sdk/js/src/v2/gen` shows exactly one added line, no other generated file drifted, and packages/client has no dag types at all. No CRITICAL/HIGH findings; the four LOW items are contract-convention (scenario update per AGENTS.md), wire-level enum enforcement, TUI invisibility of the new field, and the untracked openapi.json artifact." diff --git a/.opencode/.dag-specs/review-parts-diff/review-dataflow.md b/.opencode/.dag-specs/review-parts-diff/review-dataflow.md deleted file mode 100644 index e42c889e1b..0000000000 --- a/.opencode/.dag-specs/review-parts-diff/review-dataflow.md +++ /dev/null @@ -1 +0,0 @@ -"Verification complete: 22 + 26 + 17 tests pass, typecheck clean. Final review follows.\n\n---\n\n# DATAFLOW INTEGRITY REVIEW — error_class end-to-end\n\n**Trace verified end to end**: schema literal → projector → column → all three read surfaces, plus the dead `push_exhausted` value and the wake-digest race window.\n\n## findings\n\n```json\n[\n {\n \"severity\": \"MEDIUM\",\n \"title\": \"workflow.md claims every failed node carries error_class, but cancelled-via-replan and pre-migration nodes have it null\",\n \"description\": \"The triage section states 'Every failed node carries an error_class in status output and in the wake summary.' Two classes of failed nodes have error_class = null: (1) nodes cancelled via replan — NodeCancelled projects status='failed' with error_reason 'cancelled via replan' and deliberately does NOT set error_class; (2) failed rows written before this migration. The read surfaces correctly tolerate null (truthiness guards in tool/workflow.ts:204, handlers/dag.ts:65, loop.ts failureClass), and the wake digest's `errorClass !== null` filter is precisely what excludes cancelled-via-replan nodes — so the runtime is right and the doc is overbroad. An agent triaging a failed node with no error_class finds no matching table row and no fallback guidance.\",\n \"evidence\": \"packages/core/src/plugin/command/workflow.md:400; packages/core/src/dag/projector.ts:320-327 (NodeCancelled set: no error_class); packages/opencode/src/dag/runtime/loop.ts:859 (filter `status === \\\"failed\\\" && errorClass !== null`)\",\n \"recommendation\": \"Qualify the sentence, e.g. 'Every node failed via dag.node.failed carries an error_class...' and add one line: nodes cancelled via replan are status failed with error_reason 'cancelled via replan' and no error_class.\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"push_exhausted is a dead value: schema/comments advertise 4 classes, runtime emits 3, docs say 3 (docs are correct)\",\n \"description\": \"All 19 producer call sites emit only exec_failed (11), verdict_fail (5), or timeout (4). push_exhausted appears only in the schema Literals (dag-event.ts:253), the FallbackTrigger enum (core/types.ts:43, unused member — only EXEC_FAILED is referenced as DEFAULT_FALLBACK_TRIGGER), and the sql.ts:65 / groups/dag.ts:43 comments. Pipeline is pure string pass-through (schema → projector → column → surfaces), so the dead value breaks nothing; if a future producer emits it, persistence and every surface work unchanged. The docs listing 3 values exactly match observable behavior; the comments listing 4 are aspirational. Impact: negligible, but the vocabulary surfaces disagree with each other.\",\n \"evidence\": \"packages/schema/src/dag-event.ts:253; packages/core/src/dag/core/types.ts:43; packages/core/src/dag/sql.ts:65; packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts:43; all nodeFailed call sites: packages/opencode/src/dag/dag.ts:488,599 / runtime/recovery.ts:79,99,108,130,138 / runtime/spawn.ts:76,163,176,231,248,258,300 / runtime/loop.ts:114,153,173,186,245\",\n \"recommendation\": \"Align the two comments to the 3 produced values, or document push_exhausted as reserved in the FallbackTrigger enum. No code change required.\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"Wake digest has a narrow torn-read window for running nodes killed by dag.fail; miss is not re-delivered\",\n \"description\": \"dag.fail publishes WorkflowFailed (dag.ts:525) BEFORE terminateNonTerminalNodes publishes NodeFailed (dag.ts:526 → 488), so projection order commits the workflow row 'failed' before the killed running-node rows become 'failed'. The digest's getNodes (loop.ts:852-860) is a second read after the snapshot transaction and can transiently miss those node rows, yielding '[DAG Workflow failed] ...' with no 'Failed nodes:' attribution. Terminal workflows are marked wakeReported after delivery, so the miss is not re-delivered; recovery is via the status tool. The node-line path (batch.nodes) is unaffected for wake-eligible nodes; the digest was the only surface covering non-wake-eligible killed nodes. The codebase already tolerates this torn-read class elsewhere (loop.ts:794-795 'A terminal event can commit between either query'), and the digest degrades silently by design (Effect.catch → []). Impact: cosmetic, one wake, narrow window.\",\n \"evidence\": \"packages/opencode/src/dag/dag.ts:524-527; packages/opencode/src/dag/runtime/loop.ts:852-862, 859\",\n \"recommendation\": \"Accept as-is (documented degradation) or, for strictness, re-read the failed workflows' nodes inside the same transaction the snapshot uses, or treat an empty digest on a failed workflow as 'attribution pending' and allow one coalesced re-read. LOW because consequence is cosmetic.\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"Workflow-collateral kills are stamped exec_failed with the fail reason — doc's exec_failed row doesn't mention them\",\n \"description\": \"terminateNonTerminalNodes emits trigger 'exec_failed' for running nodes killed when the workflow fails (reason e.g. 'orchestrator_unresponsive'), and for the cancel path these are NodeSkipped (no error_class). These rows are not 'child session or provider error' — the doc's exec_failed row — but a workflow-level cascade. They flow correctly into the digest and surfaces, and the doc's advice ('read error_reason; infra/config errors; don't restart') fits the common case, but 'orchestrator_unresponsive' and similar fail reasons are absent from the doc's example list, so the mapping is incomplete.\",\n \"evidence\": \"packages/opencode/src/dag/dag.ts:487-495, 524-527; packages/core/src/plugin/command/workflow.md:406-410\",\n \"recommendation\": \"Add one line to the exec_failed row: 'reason is the workflow's fail reason (e.g. orchestrator_unresponsive) when the node was killed by the workflow failing' — or map collateral kills to their own treatment in the table.\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"workflow-tool.test.ts fixture indentation anomaly + no test guards the null-error_class filter\",\n \"description\": \"The new mock node is formatted `}, {` at odd indentation (workflow-tool.test.ts:189-212) — cosmetic only. More materially: no test asserts that a cancelled-via-replan node (status failed, error_class null) is EXCLUDED from the wake digest, which is the exact behavior the `errorClass !== null` filter exists for; a future refactor could drop the filter and no test would catch it.\",\n \"evidence\": \"packages/opencode/test/dag/workflow-tool.test.ts:189; packages/opencode/src/dag/runtime/loop.ts:859\",\n \"recommendation\": \"Fix indentation; optionally add a digest assertion with a null-error_class failed node.\"\n }\n]\n```\n\n## unverified_claims\n\n- Projection order follows publish order (WorkflowFailed seq < NodeFailed seq in the `dag.fail` path) — inferred from the publish sequence at dag.ts:524-527, not verified against the event-pipeline internals; the LOW torn-read finding stands only if this ordering holds.\n- `check:generated` not executed locally (would regenerate files; CI enforces the tracked `src/v2/gen` diff).\n- No test covers the digest on a workflow failed via `dag.fail` with a running non-wake-eligible node (the kill path); the tested path is required-node-failure → terminal.\n- No test covers pre-migration legacy failed rows (error_class null) through the read surfaces.\n- The httpapi-exercise `dag.nodes`/`dag.nodeDetail` scenarios assert only `length >= N`, so the additive optional field cannot break them — verified by inspection, not by running the exercise suite.\n\n## summary\n\nThe error_class dataflow is clean end to end: all four surfaces (tool status, wake summaries, HttpAPI, SDK) are string pass-throughs of the projector's `event.data.trigger` write, the `errorClass !== null` filters correctly exclude cancelled-via-replan rows, and the migration/schema/SDK regenerations are exactly the one intended field (tests 22+26+17 pass, typecheck clean). No CRITICAL/HIGH findings. The material issues are one overbroad doc claim (MEDIUM) — \"every failed node carries an error_class\" is false for cancelled-via-replan and pre-migration rows — plus four LOWs: the dead `push_exhausted` value (docs correctly list 3, comments advertise 4), a narrow cosmetic torn-read window in the wake digest for nodes killed by `dag.fail`, the unlisted workflow-collateral `exec_failed` stamping, and a missing test guard on the null-filter behavior." diff --git a/.opencode/.dag-specs/review-parts-diff/review-prompts.md b/.opencode/.dag-specs/review-parts-diff/review-prompts.md deleted file mode 100644 index 6628ea1a0c..0000000000 --- a/.opencode/.dag-specs/review-parts-diff/review-prompts.md +++ /dev/null @@ -1 +0,0 @@ -"Verification complete. All claims checked against code. Here is the review:\n\n---\n\n## 1. findings\n\n| # | Severity | Title | Description | Evidence | Recommendation |\n|---|----------|-------|-------------|----------|----------------|\n| 1 | **HIGH** | \"reason starts with `Dependency \"X\" failed/skipped`\" never appears as an error_reason — the row's detection criterion cannot match anything in status/wake | The strings `Dependency \"X\" failed: ...` / `Dependency \"X\" skipped: ...` are input-mapping **placeholder values** injected into the dependent node's PROMPT when resolving `{{dep}}` (loop.ts:133/136). They are never an error_reason: an exhaustive inventory of all 20+ `dag.nodeFailed` call sites (spawn.ts, loop.ts, recovery.ts, dag.ts) contains no path producing this prefix. Actual cascade semantics: (a) **required** root fails → `cascadeUnsatisfied` marks all transitive dependents unsatisfied, they never spawn and stay durable `pending` with **no error_reason at all** (scheduling.ts:109-121); the wake shows only the root in \"Failed nodes\". (b) **optional** root fails → dependents DO run, receiving the `Dependency \"X\" failed:` text inside their prompt Context (verified by the repo's own test asserting the text in the *arbitrate child's prompt input*, dag-wake-integration.test.ts:438). The row's response \"replace the cascaded subtree\" is also wrong for case (a) — dependents are untouched pending nodes, not failed ones. | workflow.md:412; loop.ts:133,136; scheduling.ts:109-121,162-174; dag-wake-integration.test.ts:438 | Rewrite the row: cascade is detected by (a) required-root failure leaving dependents `pending` in `status` while the workflow is `failed` (wake shows \"required node(s) failed: X\", loop.ts:272), or (b) the `Dependency \"X\" failed:` text inside a *running/completed* dependent's prompt context — never in error_reason. Response: fix root X; in case (a) simply resume/continue with the root replaced; in case (b) re-run dependents that consumed placeholder input. |\n| 2 | **MEDIUM** | exec_failed row's reason list omits major sub-causes and its \"fix the config first\" response misdirects for them | Real exec_failed reasons beyond those listed: `execution ownership lost on recovery` (recovery.ts:141 — the dominant crash-recovery reason, and the exact string the doc's own Crash recovery section at workflow.md:390 tells the agent to expect), workflow-level collateral kills `workflow_failed`/`orchestrator_unresponsive`/`required node(s) failed: ...` published to still-running nodes via terminateNonTerminalNodes with hardcoded trigger `exec_failed` (dag.ts:492, loop.ts:272,830), `replan attempt ceiling exceeded` (dag.ts:599), condition-eval errors (loop.ts:114), `Template resolution failed` (loop.ts:173). For ownership-loss and collateral-kill reasons, \"Fix the config first (dag.jsonc tier, provider credentials, model id), then replace and rerun ONLY that node\" is wrong advice — no config is broken, and the collateral-killed node's failure is a symptom of a sibling's root cause. Also `\"no child session on recovery\"` is a substring of the actual `\"node was running but had no child session on recovery\"` (recovery.ts:79) — fine for substring matching but not an exact string as quoted. | workflow.md:409; recovery.ts:79,108,141; dag.ts:492,599; loop.ts:272,830 | Expand the exec_failed row: list ownership-loss and workflow-collateral reasons explicitly, and gate the response on the reason: config fixes only for model/auth/provider/template/condition reasons; replace+rerun for ownership loss; look at sibling root causes for collateral-killed nodes. |\n| 3 | **MEDIUM** | verdict_fail row claims \"The node ran\" — false for pre-spawn verdict_fail failures, whose fix is not \"state the contract explicitly\" | Two verdict_fail reasons fire **before the child session exists**: `Unresolved template placeholders: ...` (loop.ts:186 — the node never spawned; the aggregate-node test at dag-wake-integration.test.ts:693-731 confirms it) and `Review input contract failed: ...` (loop.ts:153). For these, \"Rerun the node with the contract stated explicitly\" cannot fix a template with unresolved `{{placeholder}}` or a review node whose input mapping lacks implementation evidence — the template/mapping must be fixed. Listed reasons themselves are accurate (missing submit_result → \"output_schema declared but submit_result was never successfully called\", capture.ts:140-141; schema rejection → child gets \"Validation failed\" and captured stays null, submit_result.ts:49; fingerprint → \"Review result contract failed: ...\", capture.ts:143; plus \"provider returned empty output\", spawn.ts:258). | workflow.md:410; loop.ts:153,186; capture.ts:140-145; spawn.ts:258; dag-wake-integration.test.ts:706-709 | Split verdict_fail into ran-but-broke-contract (submit_result/schema/fingerprint → rerun with contract explicit) vs never-ran (unresolved placeholder/input-contract → fix template/mapping, then rerun). |\n| 4 | **LOW** | timeout row's \"Check its child_session_id for partial artifacts\" holds only for the main timeout path | Only `node exceeded timeout of {X}ms` (spawn.ts:231) has a cancellable child session whose messages persist (SessionPrompt.cancel only aborts the in-flight prompt; the NodeFailed projection keeps child_session_id on the row — projector.ts:279-291). The pre-permit (spawn.ts:163) and permit-wait (spawn.ts:176) timeout reasons have **no child session at all** (child_session_id null), and `deadline exceeded on recovery` (recovery.ts:130) cancels the session during recovery, not \"at the deadline\". So \"the runtime cancelled its child session at the deadline\" is accurate only for the dominant path. | workflow.md:408; spawn.ts:163,176,230-231; recovery.ts:130; projector.ts:279-291 | Note in the row that child_session_id may be null for permit-queue timeout variants; partial-artifact checking applies to the running-timeout case. |\n| 5 | **LOW** | \"Every failed node carries an error_class in ... the wake summary\" — wake attribution silently drops null-class rows | The wake \"Failed nodes\" block filters `errorClass !== null` (loop.ts:857), and the node line shows the class only when set (loop.ts:868). Fresh runs always carry a trigger (every nodeFailed publishes one), so the doc holds in practice; only pre-migration rows are invisible. Related: both docs enumerate 3 classes omitting `push_exhausted` (dag-flow.txt:37, workflow.md:407-412) — harmless because push_exhausted exists only in the schema enum and comments (dag-event.ts:253, sql.ts:65, groups/dag.ts:43) and is **never emitted by any runtime path** (exhaustive nodeFailed grep). | workflow.md:404; loop.ts:857,868; dag-flow.txt:37; dag-event.ts:253 | Optionally add \"every failed node carries an error_class (null only on pre-migration rows)\" and a note that push_exhausted is reserved/unused; not a behavior gap. |\n\n**Verified accurate (no finding):** timeout cancel at deadline (spawn.ts:230-231); wrong-model/auth/rate-limit/connection → exec_failed with discoverable Cause.pretty text (spawn.ts:78 \"unknown worker_type\"/\"no model configured for agent\", spawn.ts:300, loop.ts:245); budget strings exact — `replan attempt ceiling exceeded` (dag.ts:599, as a node-failed event with class exec_failed) and `Total node ceiling exceeded` (dag.ts:354,577, as a create/replan tool error) with working `(see Escalation)` cross-ref (workflow.md:382 mentions \"replan-attempt ceiling rejection\"); live repair path — pause valid from running/stepping (types.ts:198-207), replan valid while paused (dag.ts:542 terminal-guard only), `restart: true` semantics match tool schema (workflow.ts:53), `extend` works on live workflows (_extend → _replan, dag.ts:682-722), completed siblings preserved; terminal-failed irreversibility — `TerminalViolationError` on replan (dag.ts:542-546) and `getValidNextWorkflowStatuses(FAILED) → [ARCHIVED]` (types.ts:209-211); continuation workflow feasible — child sessions/messages durable after cancel, child_session_id exposed in status (workflow.ts:204), `reused_nodes`/manifest convention exists (dag-flow.txt:23,40); both docs agree on replacement-under-new-id mechanic with the Crash recovery section (workflow.md:386-399) and dag-flow.txt:39; no contradiction with Verdict Disposal Contract/Bounded Repair/Adaptive Replanning (workflow.md:106,249,363). Runtime check: `bun test test/dag/workflow-tool.test.ts` → 26 pass, incl. the new error_class-in-status assertion.\n\n## 2. unverified_claims\n\n- dag-wake-integration.test.ts:693-731 wake-text assertions (verdict_fail + \"Failed nodes:\" attribution) not executed — live integration harness, not run in this read-only review; statically consistent.\n- That a real wrong-model/provider auth error lands in error_reason via Cause.pretty — path verified statically (spawn.ts:300), not exercised against a live provider.\n- Session-part durability after promptSvc.cancel — inferred from SessionPrompt.cancel only aborting the in-flight prompt (prompt.ts:165-167); not executed end-to-end.\n- Replan-from-STEPPING (in \"still live (running/paused/stepping)\") — allowed by the status table (types.ts:204), not exercised at runtime.\n- `push_exhausted` truly never emitted — based on exhaustive grep of all nodeFailed call sites (no emitter found), not on runtime observation.\n\n## 3. summary\n\nThe triage section is substantially runtime-accurate: timeout-cancel, exec_failed surfaces, budget strings, live-vs-terminal repair paths, and cross-doc consistency (including the replacement-under-new-id mechanic) all verified against code, with workflow-tool tests passing. One HIGH drift: the Dependency-cascade row's \"reason starts with `Dependency \"X\" failed/skipped`\" criterion does not exist in any error_reason — cascade appears as pending dependents (required root) or as placeholder text inside dependents' prompts (optional root), so the parent would search for a signal that never occurs. Two MEDIUM gaps: exec_failed and verdict_fail rows under-describe their real reason sets and prescribe fixes (config-first; \"state the contract\") that misdirect for ownership-loss, collateral-kill, and pre-spawn verdict_fail failures." diff --git a/.opencode/.dag-specs/review-parts-diff/review-runtime.md b/.opencode/.dag-specs/review-parts-diff/review-runtime.md deleted file mode 100644 index 956e68ff19..0000000000 --- a/.opencode/.dag-specs/review-parts-diff/review-runtime.md +++ /dev/null @@ -1 +0,0 @@ -"All tests pass. Here is my review.\n\n---\n\n# RUNTIME SEMANTICS REVIEW — `packages/opencode/src/dag/runtime/loop.ts` (uncommitted hunks)\n\n## 1. findings\n\n**f-1 | MEDIUM | Collateral `exec_failed` attribution is indistinguishable from genuine node failures in the digest — mis-triage risk on the orchestrator_unresponsive path**\n`Dag.fail` (packages/opencode/src/dag/dag.ts:523-527) publishes `WorkflowFailed` then `terminateNonTerminalNodes` publishes `NodeFailed` with a hardcoded `trigger: \"exec_failed\"` (dag.ts:492) for every running node killed by the workflow's failure. The new digest (loop.ts:855-864) renders these as `(exec_failed)` alongside genuinely failed nodes, and the workflow row carries no failure reason (projector.ts:117-123 sets only status/seq/completed_at), so the digest alone cannot distinguish \"node's own execution failed\" from \"node killed by workflow failure\". The `workflow.md` triage table (workflow.md:410) instructs `exec_failed` → \"fix config first … replan the graph only when the reason shows the task itself failed\" — a parent following the table on a workflow failed by the unresponsive net (nodes show reason \"orchestrator_unresponsive\", class \"exec_failed\") gets table guidance that fits the class but mislabels the mechanism. The mandatory-action line (loop.ts:882-884) and the prior wake's threat make it recoverable for an attentive parent, but the new digest amplifies the mislabeling rather than mitigating it.\nRecommendation: render collateral kills distinctly in the digest (e.g., a `(killed by workflow failure)` marker derived from the node's errorReason matching the workflow-fail reason, or pass the real trigger through `terminateNonTerminalNodes`) — or at minimum document in the triage table that `exec_failed` with reason `orchestrator_unresponsive` means collateral.\n\n**f-2 | LOW | Double reporting of every failed node — same-wake redundancy plus full-history repetition**\nThe batch composition guarantees the same failed node appears twice in one wake when a node failure and its workflow terminalization land in the same batch: once as `[DAG Node Result] Node \"x\" failed (exec_failed): …` (from `batch.nodes`, loop.ts:867-873) and again inside `Failed nodes:\\n- \"x\" (exec_failed): …` (loop.ts:874-878). `getNodes` (loop.ts:855) reads the full history with no `wake_reported` filter, so nodes whose failures were delivered in earlier batches also reappear in the terminal digest; a node that failed between the batch snapshot and the digest read is included too (digest is a superset of the batch — never a missing-node case, verified by construction). The new test exercises exactly this double report (dag-wake-integration.test.ts:729-730 asserts the digest while the same wake contains the node line). Impact: ~300 chars duplicated per failed node; information is consistent, not contradictory — acceptable, but redundant.\nRecommendation: acceptable as-is; if prompt budget matters, drop the node line for failed nodes already covered by a workflow digest in the same batch, or cap digest entries to nodes not already reported this batch.\n\n**f-3 | LOW | Silent `Effect.catch` fallback diverges from the file's failure-handling convention and can silently drop attribution**\n`getNodes` uses `Effect.orDie` (packages/core/src/dag/store.ts:241), so its errors surface as defects — caught by `Effect.catch` (v4 unified channel) and replaced with `[]` with no logging (loop.ts:857). Every other store read in this delivery path uses `Effect.catchCause` + `Effect.logWarning` (loop.ts:945-946, 958-961, 969-974). A busy/locked DB at delivery time silently degrades the digest to the old text with zero trace, which is exactly the class of silent failure the surrounding code deliberately logs.\nRecommendation: `Effect.catchCause((cause) => Effect.logWarning(\"DagLoop failed to read failed nodes for digest\", { workflowID, cause }).pipe(Effect.as([])))` to match local convention.\n\n**f-4 | LOW | Multibyte truncation can split surrogate pairs**\n`slice(0, 300)` (loop.ts:862) and the pre-existing `slice(0, 500)` (loop.ts:869-870) count UTF-16 code units; an astral character (emoji in node name/reason) at the cut boundary yields a lone surrogate embedded in the prompt part, rendering as U+FFFD in most parsers/encoders. Cosmetic; pre-existing pattern, now also applied to digest lines.\nRecommendation: use `Intl.Segmenter`/`Array.from`-based truncation or leave as-is (documented known wart).\n\n**f-5 | LOW | Digest size introduces a full-history size class (bounded but non-trivial)**\nThe digest is a full-history read of all failed nodes per failed workflow, each line capped at 300 chars; node count is bounded by the declared graph (default budget `max_total_nodes: 100`, packages/opencode/src/tool/workflow.ts:82) → worst case ≈ 30KB per failed workflow in one prompt part, multiplied across multiple failed workflows in one session batch. Pre-existing node lines were also per-node (500×N) but only for unreported rows in the terminal batch; the digest adds another full-history copy. Bounded, not a new order of magnitude, but the largest single-prompt-part regression this diff introduces.\nRecommendation: acceptable; consider a digest entry cap (e.g., first 20 failed nodes + \"… N more\") for large graphs.\n\n**f-6 | LOW | Digest reads through `dag.store` — a second DagStore instance — instead of the loop's local `store`**\nloop.ts:855 uses `dag.store.getNodes` while every other read in the delivery path uses the local `store` (DagStore.Service, loop.ts:52). `Dag.Service.store` (dag.ts:243) is bound to the Dag layer's own `Layer.provide(DagStore.defaultLayer)` (dag.ts:790), so it is a different service instance/connection than the one used for `getWakeSnapshot`/`markWakeBatchReported`. Both point at the same DB file under the same InstanceRef, so committed-row consistency holds (verified: tests pass), and the fresh-read-on-another-connection can only add rows beyond the batch snapshot (benign superset). No functional defect — a consistency/style issue on the most-sensitive delivery path.\nRecommendation: use `store.getNodes` for uniformity with the rest of the generator.\n\n**No CRITICAL/HIGH findings.** Verified non-issues: (1) lock/ordering — the digest reads acquire no locks and don't touch the evalLock/workflowLock discipline (which governs runtime/fibers state, not store rows; the torn-read comment at loop.ts:807-819 is unaffected); `failuresByWorkflow` is scoped per batch iteration (recreated at loop.ts:852 inside `for(;;)`), no stale-map leak. (2) `wake_reported` persistence ordering unchanged — batch rows are still marked only after successful `promptIfIdle` (loop.ts:894-915); the digest's already-reported rows need no marking; a failed delivery leaves everything unreported for retry. (3) Preemption semantics preserved — `promptIfIdle` atomically claims idle via `startIfIdle` (packages/opencode/src/session/prompt.ts:1394); the new reads only widen the existing guard→admission window. (4) `actionableDagIDs` excludes terminal workflows (loop.ts:760-762), so a failed workflow never sits in both the digest and the mandatory-action line of one wake. (5) When the unresponsive net fails a workflow inside this delivery (loop.ts:830), the terminal handler's fork → `wakePending` → retry (loop.ts:918-921, 707-708) delivers the digest in a fresh call. (6) Regression paths — completed/cancelled workflow lines are byte-identical (`attribution` is `\"\"` since only `status === \"failed\"` is processed), node lines identical for non-failed nodes and for legacy failed nodes with `errorClass === null`, skipped-node lines untouched. Tests executed: `dag-wake-integration.test.ts` (new aggregate-failure test PASS, exec_failed persistence PASS), `workflow-tool.test.ts` 26/26 PASS.\n\n## 2. unverified_claims\n\n- The same-wake double report (f-2) is inferred from `readWakeBatch` composition (loop.ts:721-775), not asserted by the test — the test checks only the digest substring, so the node-line half of the double report is unobserved.\n- `Effect.catch` in this repo's Effect v4 vendored build catches `orDie` defects — high confidence (unified channel semantics) but not verified against the vendored version.\n- SQLite journal mode (WAL vs DELETE) for the cross-connection read in f-6 — not verified; DELETE mode would serialize readers behind writers, making f-3's silent-fallback path more reachable.\n- `max_total_nodes` (100) is a tool-level budget; whether the store can hold a graph with more declared nodes than the budget (which would raise f-5's worst case) — not verified.\n- Digest behavior when the parent session was busy across the entire failure window (digest delivered from the restart scan, loop.ts:957-978, with the batch rows still unreported) — path traced, not executed.\n\n## 3. failure_scenarios\n\n- {scenario: getNodes hits a busy/locked DB at delivery time → digest silently degraded to old text, no log trace, impact: lost attribution for triage, likelihood: LOW}\n- {scenario: workflow failed by orchestrator_unresponsive → digest shows all collateral nodes as (exec_failed); parent follows the triage table and replans/repairs the wrong thing or starts an unnecessary continuation, impact: wasted provider work / wrong repair, likelihood: MEDIUM (fires whenever the unresponsive net kills a running workflow)}\n- {scenario: large failed workflow (≈100 nodes) terminalizes → single prompt part ≈30KB digest + node lines, impact: context dilution in the parent session, likelihood: LOW}\n- {scenario: node name/errorReason contains astral chars at the 300-char cut → lone surrogate in wake text, impact: cosmetic garbling, likelihood: LOW}\n- {scenario: legacy rows (pre-migration) with error_class NULL → failed nodes omitted from digest while still listed as individual node lines, impact: digest appears incomplete, likelihood: LOW (one-time migration window)}\n\n## 4. summary\n\nThe new digest read is safe under the existing delivery discipline — no lock interaction, no `wake_reported` reorder, terminal/actionable sets never overlap, and completed/cancelled/skipped paths are byte-identical regressions (all executed tests pass). The material risks are semantic, not mechanical: collateral `exec_failed` kills from the orchestrator-unresponsive net are presented in the digest indistinguishably from genuine node failures (f-1, MEDIUM — the only finding that could steer a parent wrong), and the digest doubles reported content and a silent-fallback path that break the file's own conventions. No CRITICAL/HIGH; recommended fixes are small (mark collateral kills, use `store` + `catchCause` with logging, cap digest entries)." diff --git a/.opencode/.dag-specs/review-parts-diff/review-style.md b/.opencode/.dag-specs/review-parts-diff/review-style.md deleted file mode 100644 index 27bc3fee2c..0000000000 --- a/.opencode/.dag-specs/review-parts-diff/review-style.md +++ /dev/null @@ -1 +0,0 @@ -"# STYLE & CONVENTIONS REVIEW — error_class diff\n\n审查基线:`git diff HEAD` 全部 16 个已跟踪 hunk + 未跟踪迁移文件。对照 root AGENTS.md Style Guide、packages/opencode/AGENTS.md、test/AGENTS.md。\n\n## 通过项(无 finding)\n\n- **注释合规**:`sql.ts:65` 与 `groups/dag.ts:42-44` 两条新注释均解释非显而易见语义(值域 + 事件来源),且分别匹配文件内既有注释约定(`sql.ts` 的 captured_output/deadline_ms/wake_eligible 均为解释型单行注释;`groups/dag.ts` 的 deadline_ms 同为双行注释)。迁移与 loop.ts 无注释,符合\"仅非显然约束加注释\"规则。\n- **迁移约定**:`20260803073521_workflow_node_error_class.ts` 的 id=文件名、时间戳格式与近半年描述性 snake_case 命名(`20260720013828_dag-workflow-node-identity`)一致;`migration.gen.ts:50` 按时序末位注册;`schema.gen.ts:86` 列序(error_reason→error_class→captured_output)与 `sql.ts` 定义序一致;`schema.json` id 轮换、实体插序正确;`version` 保持 \"7\" 与最近四个迁移提交一致;列名 `error_class` 直接 snake_case 无字符串覆盖(drizzle 规则 ✓)。\n- **Effect 约定**:无新增 `Effect.fn`;无嵌套 service yield;`Effect.catch` 用法与同文件 line 841 形状一致;未引入 `any`。\n- **测试风格**:`dag-wake-integration.test.ts:726,727-731` 完整沿用既有 parent-wake 模式(`takeWithin` + `promptText` + `Deferred.succeed(parent.release, \"success\")`,对齐 line 514-522 先例);`:448` 的 `?.errorClass` 断言镜像 `:447` 的 `?.status`;`fixtures.ts:18` 与 mock 默认值补齐符合文件模式。\n\n## findings\n\n```json\n[\n {\n \"severity\": \"LOW\",\n \"title\": \"loop.ts 新代码绕过已绑定的 store 变量改用 dag.store\",\n \"description\": \"文件顶部(loop.ts:52)已绑定 `const store = yield* DagStore.Service`,同文件其余 7 处 getNodes 调用(97/261/367/411/660 等)全部使用 `store`。新代码 loop.ts:855 却写 `dag.store.getNodes(...)`,绕过了已绑定的命名变量。已核验 dag.ts:284+765-767:`dag.store` 由同一 `DagStore.Service` yield 构造,功能上等价,纯约定偏差。\",\n \"evidence\": \"packages/opencode/src/dag/runtime/loop.ts:855(对比 loop.ts:52, 97, 261, 367, 411, 660)\",\n \"recommendation\": \"改为 `store.getNodes(workflow.id)`,与文件内既有调用点一致。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"errorClass 过滤缺类型谓词,下游类型未收窄\",\n \"description\": \"loop.ts:856 的 `nodes.filter((node) => node.status === \\\"failed\\\" && node.errorClass !== null)` 无类型谓词,`map` 中 `node.errorClass` 类型仍为 `string | null`,模板 `(${node.errorClass})`(:862)仅靠运行时过滤保证非空,typecheck 无法证明。root AGENTS.md 明确要求 \\\"use type guards on filter to maintain type inference downstream\\\",且同文件 loop.ts:736 已有 `(workflow): workflow is DagStore.WorkflowRow` 谓词先例。\",\n \"evidence\": \"packages/opencode/src/dag/runtime/loop.ts:856-862(对照 loop.ts:736)\",\n \"recommendation\": \"给 filter 加类型谓词(如 `(node): node is DagStore.NodeRow & { errorClass: string; status: \\\"failed\\\" }`),使下游 `node.errorClass` 收窄为 string。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"冗余的 `as DagStore.NodeRow[]` 类型断言,与文件既有模式不一致\",\n \"description\": \"loop.ts:857 `Effect.catch(() => Effect.succeed([] as DagStore.NodeRow[]))` — `[]` 推断为 `never[]` 可赋值给 `NodeRow[]`,断言大概率多余;同文件 :841 的同类 catch 写作 `Effect.succeed([])` 无断言。\",\n \"evidence\": \"packages/opencode/src/dag/runtime/loop.ts:857(对照 loop.ts:841)\",\n \"recommendation\": \"去掉断言,与 :841 保持一致(需 typecheck 复核)。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"workflow-tool.test.ts mock 数组分隔符缩进错位\",\n \"description\": \"新增的 `}, {`(:190)与 `}]`(:214)缩进 10 空格,而数组元素对象(含既有 node_running)为 8 空格,新增行与文件自身约定错位 2 格。\",\n \"evidence\": \"packages/opencode/test/dag/workflow-tool.test.ts:190,214(对比 :167, :189)\",\n \"recommendation\": \"将 `}, {` 与 `}]` 对齐到 8 空格。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"两处文档将 3/4 的 error_class 值列举为完整集合\",\n \"description\": \"workflow.md:409-412 分诊表与 dag-flow.txt:37 的括号列举均只含 timeout/exec_failed/verdict_fail,且无 \\\"e.g.\\\" 或省略标记,读作穷举;而 schema 枚举(dag-event.ts:253)含第 4 值 push_exhausted,同一变更内两处代码注释(sql.ts:65、groups/dag.ts:42-44)均列全 4 值。文档与代码注释在同一 diff 内已出现值集不一致,存在漂移风险。遗漏是否构成行为指导缺口属 review-prompts/仲裁范畴,此处仅指风格一致性。\",\n \"evidence\": \"packages/core/src/plugin/command/workflow.md:409-412, packages/core/src/plugin/command/dag-flow.txt:37(对照 packages/schema/src/dag-event.ts:253, packages/core/src/dag/sql.ts:65, packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts:43-44)\",\n \"recommendation\": \"补全 push_exhausted(或明确标注非穷举),使三处枚举一致。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"dag-flow.txt 收尾过渡句被新章节隔断\",\n \"description\": \"文件原为编号指令块(1-13)后直接接收尾过渡句 \\\"Use the orchestration guidance below to design and manage the workflow.\\\"。新 \\\"## Resume-first\\\" 节插在两者之间,使过渡句读作新章节的尾部而非指令块的收尾,编号块与指向语之间的衔接被破坏。\",\n \"evidence\": \"packages/core/src/plugin/command/dag-flow.txt:35-46(新节插于 :34 与收尾句之间)\",\n \"recommendation\": \"将收尾过渡句移回编号块正后方(新节之前),或若有意为之,确认章节顺序的语义(Resume-first 属启动前决策,可考虑放在编号块前)。\"\n }\n]\n```\n\n## unverified_claims\n\n- \"loop.ts:857 的 `as DagStore.NodeRow[]` 断言删去后 typecheck 仍通过\" — 依据 `never[]` 可赋值性推断,未实际运行 `bun typecheck` 验证。\n- \"`dag.store` 与绑定 `store` 在含 LayerNode/dag.jsonc 组合路径下恒等\" — 仅核验了 `Dag.layer` 构造路径(dag.ts:284,765-767),未追踪 `LayerNode.make` 组合(dag.ts:768)。\n- \"workflow.md:412 的 `Dependency \\\"X\\\" failed/skipped` 前缀真实存在于生产的 error_reason 文本\" — 仅由既有测试断言 dag-wake-integration.test.ts:438 佐证,未查证产生该字符串的运行时代码。\n- \"schema.json `version` 保持 7 符合生成脚本约定\" — 核验最近四个迁移提交均为 7,未检查 `script/migration.ts` 的版本递增逻辑。\n- \"新增 wake 文本断言、errorClass 持久化断言在测试套件中实际通过\" — 未执行测试(属 verify 通道)。\n\n## summary\n\n该 diff 风格上整体干净:两条新注释均满足\"仅非显然约束\"规则且贴合文件本地约定,迁移命名/id/注册/生成物(schema.gen.ts、schema.json、types.gen.ts)完全合规,测试新增均镜像既有断言模式,全 diff 无 `let`、`else`、冗余解构、import 别名或 `any`。共 6 个 LOW finding:loop.ts 绕过已绑定 `store` 变量、过滤缺类型谓词(违反 root AGENTS.md 显式规则且有同文件先例)、冗余 cast、测试 mock 缩进错位、两文档漏列第 4 枚举值且读作穷举、dag-flow.txt 过渡句被新节隔断。无 CRITICAL/HIGH;push_exhausted 遗漏的内容定性已移交 prompts/仲裁通道。" diff --git a/.opencode/.dag-specs/review-parts-diff/review-tests.md b/.opencode/.dag-specs/review-parts-diff/review-tests.md deleted file mode 100644 index c795a0cb74..0000000000 --- a/.opencode/.dag-specs/review-parts-diff/review-tests.md +++ /dev/null @@ -1 +0,0 @@ -"All evidence gathered. Both modified suites pass (26 + 22 tests), the fixture-dependent recovery/guard suites pass (38 tests), and `bun typecheck` is green. Here is the review.\n\n---\n\n## TEST COVERAGE REVIEW — error_class test hunks\n\n### 1. Findings\n\n| # | Severity | Title | Description | Evidence | Recommendation |\n|---|----------|-------|-------------|----------|-----------------|\n| F1 | MEDIUM | httpapi `error_class` surface has zero coverage | The handler (non-null → `error_class`) and the new `NodeResponse` optional field are never exercised: `httpapi-exercise` seeds only fresh pending nodes and asserts loose fields (`id`, `status`, `depends_on`, `replan_attempts`); no scenario seeds a failed node, so neither the presence of `error_class` on a failed node nor its absence on a healthy node is checked. The loose checks mean CI does not break — but the field ships untested to the SDK/TUI consumers. | packages/opencode/test/server/httpapi-exercise/index.ts:1807-1850; handler map at packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts:65 | Extend the `dag.nodeDetail`/`dag.nodes` scenarios to seed a failed node (insert a `dag.node.failed` event or DB row) and assert `error_class` present with the expected value; optionally a negative check on a completed node. |\n| F2 | MEDIUM | Recovery-invented failure classes never asserted | recovery.ts emits `NodeFailed` with `timeout`/`exec_failed`/`verdict_fail` triggers (recovery.ts:79, 99, 108, 130, 142), but neither `dag-recovery.test.ts` nor `dag-loop-recovery-integration.test.ts` asserts the resulting `errorClass` — recovery tests track events/outcomes, not the new column. These paths are exactly the \"recovered workflow failure\" cases the triage docs tell agents to read (`error_class` on recovery failures). | packages/opencode/src/dag/runtime/recovery.ts:79-145; dag-recovery.test.ts (event assertions only) | In a recovery integration test, assert `store.getNode(...).errorClass` for a deadline-exceeded node (`\"timeout\"`) and a no-child-session node (`\"exec_failed\"`), and assert the wake digest attribution when the recovered workflow terminalizes failed. |\n| F3 | MEDIUM | NodeCancelled null-`errorClass` invariant untested | NodeCancelled projection sets `status: \"failed\"` + `error_reason: \"cancelled via replan\"` but deliberately leaves `error_class` null (projector.ts:320-323); the wake digest filter `status === \"failed\" && errorClass !== null` (loop.ts:856) excludes exactly those rows. Neither side is pinned by a test — a future change setting the class in NodeCancelled (or dropping the filter) would silently change digest contents. | packages/core/src/dag/projector.ts:320-323; packages/opencode/src/dag/runtime/loop.ts:856 | In an existing replan-cancel test, assert the cancelled row has `errorClass === null`; in a wake test with a cancelled-then-terminal workflow, assert the cancelled node is absent from `Failed nodes:`. |\n| F4 | MEDIUM | Multi-failed-node digest ordering and truncation untested | `getNodes` orders `desc(seq)` (store.ts:239) so the digest lists newest failure first; each line is `.slice(0, 300)` (loop.ts:862). Only a single-failed-node digest is asserted; the workflow-level cascading fail (`dag.ts:492`, hardcoded `exec_failed`) would put collateral failed nodes in the digest alongside the primary — semantics no test documents. | packages/core/src/dag/store.ts:234-243; packages/opencode/src/dag/runtime/loop.ts:852-865 | Add a scenario with ≥2 failed nodes (primary + collateral from workflow fail) asserting both appear, order (desc seq), and a >300-char reason truncates. |\n| F5 | LOW | Wake node-line class suffix exercised but unasserted | In the aggregate-placeholder test the failed `summary` node is also in `batch.nodes` (registered `report_to_parent: true` → `wake_eligible` never cleared by NodeFailed projection; `getWakeSnapshot` selects `status IN (completed, failed)`), so the text contains `[DAG Node Result] Node \"summary\" failed (verdict_fail): …` — but the test asserts only the workflow line and the digest (line 729-730), leaving loop.ts:871's node-line suffix logic unasserted. | packages/opencode/test/dag/dag-wake-integration.test.ts:727-731; loop.ts:867-873 | Add one `toContain('Node \"summary\" failed (verdict_fail):')` to the existing wake assertions. |\n| F6 | LOW | Mock failed node encodes an impossible runtime state | `node_failed` has `dependsOn: [\"node_running\"]` while `node_running` is still `status: \"running\"` — a dependent can never fail before its dependency completes in the real runtime. Acceptable for a rendering unit test, but the fixture models an unreachable state; also no negative assertion that the running node's JSON omits `error_class`. | packages/opencode/test/dag/workflow-tool.test.ts:166-214, 409-414 | Use `dependsOn: []` (or a completed dependency) for the failed node; add `expect(result.output).not.toContain('\"error_class\"')` for the running node. |\n| F7 | LOW | Indentation drift in the mock array | `}, {` at lines 190/214 sits at 10-space indent vs the 8-space array style around it. | packages/opencode/test/dag/workflow-tool.test.ts:190, 214 | Run the formatter (or align to 8 spaces). |\n| F8 | LOW | `\"unknown error\"` fallback untested | `node.errorReason ?? \"unknown error\"` (loop.ts:862) is unreachable through any current producer (every NodeFailed carries a reason); only DB tampering reaches it. Deferrable, but worth a one-line assertion if a fixture ever sets `failed + class + null reason`. | packages/opencode/src/dag/runtime/loop.ts:862 | Optional: seed a row with `errorClass` set and `errorReason` null in the digest test. |\n\nNo CRITICAL/HIGH: every primary behavior (projector persistence, status tool output, wake digest attribution) has at least one passing assertion, and hygiene checks pass.\n\n### 2. unverified_claims\n\n- The aggregate test's wake text contains the node-line `[DAG Node Result] Node \"summary\" failed (verdict_fail): …` — derived from static analysis (registration `wake_eligible: true` via `report_to_parent`, NodeFailed projection never clears `wake_eligible`, `getWakeSnapshot` includes `status IN (\"completed\",\"failed\")` rows, terminal workflows are delivery boundaries), not from an executed assertion (F5).\n- `push_exhausted` has no runtime producer — verified by `rg` over `packages/opencode/src` + `packages/core/src` (only the `types.ts` enum and comments); hence its absence from tests is by-design, and its absence from the triage docs is a docs nit, not a coverage gap.\n- The workflow-tool mock's `node_failed` depends-on-running-dependency state is unreachable in the real runtime — static inference from the scheduling rule that dependents spawn only after dependencies settle (F6).\n- The digest's per-line 300-char truncation and desc-seq ordering follow directly from store.ts:239 + loop.ts:862 (read, not executed in any test).\n\n### 3. coverage_gaps\n\n| path | untested_scenarios |\n|------|--------------------|\n| packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts (error_class mapping) | node endpoint returns `error_class` for a failed node; field absent for non-failed nodes; SDK type carries it through |\n| packages/opencode/src/dag/runtime/recovery.ts | `errorClass` persisted as `timeout` (deadline on recovery), `exec_failed` (no child session / failed child / ownership loss), `verdict_fail` (recovered settlement gate); wake digest attribution after a recovery-failed terminal workflow |\n| packages/opencode/src/dag/runtime/loop.ts (failed-workflow digest) | ≥2 failed nodes ordering (desc seq) and per-line 300-char truncation; `\"unknown error\"` fallback; exclusion of `errorClass === null` failed rows (cancelled via replan) |\n| packages/core/src/dag/projector.ts (NodeCancelled projection) | cancelled-via-replan row keeps `error_class` null while `status` becomes \"failed\" |\n| packages/opencode/test/dag/dag-wake-integration.test.ts | node-line class suffix (`Node \"summary\" failed (verdict_fail):`) — present in the text but unasserted (F5) |\n| packages/opencode/src/tool/workflow.ts (status action) | negative case: `error_class` omitted when null (running/healthy nodes) |\n\n### 4. summary\n\nThe new assertions correctly cover the three primary surfaces — projector persistence via `store.getNode` (`exec_failed` at :448, `verdict_fail` at :726), status-tool output (`\"error_class\": \"timeout\"` at workflow-tool.test.ts:414), and the wake `Failed nodes:` digest — and the exact-string digest assertions are appropriately pinned (contract-level, reason-prefix matched so wording changes don't flake); suites (26+22+38 tests) and typecheck are green, and the `makeNodeRow`/mock-store `errorClass: null` defaults keep all pre-existing suites valid (no other NodeRow literal exists outside the dag test dir; TUI/SDK consume SDK types, not NodeRow). No CRITICAL/HIGH findings; the real coverage debt is on secondary surfaces: the httpapi endpoint, recovery-invented failure classes, the NodeCancelled null-class invariant, multi-node digest ordering/truncation, and the node-line class suffix that the aggregate test already exercises but never asserts." diff --git a/.opencode/.dag-specs/review-parts-diff/scope-diff.md b/.opencode/.dag-specs/review-parts-diff/scope-diff.md deleted file mode 100644 index aa65f3f348..0000000000 --- a/.opencode/.dag-specs/review-parts-diff/scope-diff.md +++ /dev/null @@ -1 +0,0 @@ -"# Diff 范围地图 — failure-class 持久化 + 暴露 + 分诊指导\n\n## 1. 变更全景图(git diff HEAD:16 tracked 文件,+118/-8;另 1 个未跟踪新迁移文件)\n\n变更主线单一:**`dag.node.failed` 事件的 `trigger`(error_class)从瞬态事件数据 → 持久化列 → 全部 agent 可见读面 → 分诊提示词**。无跨主线 hunk。\n\n### 区域 A — 持久化层(packages/core,7 文件 + 1 新文件)\n| 文件 | 变更 |\n|---|---|\n| `src/database/migration/20260803073521_workflow_node_error_class.ts` | **[未跟踪新文件]** `ALTER TABLE workflow_node ADD error_class text`(可空、无默认值),id 与文件名一致 |\n| `src/dag/sql.ts:65` | 新增 `error_class: text()` 列,注释内联枚举四值 timeout/exec_failed/verdict_fail/push_exhausted |\n| `src/dag/projector.ts:285` | `dag.node.failed` 投影处新增 `error_class: event.data.trigger`(与 `error_reason` 同 set 块) |\n| `src/dag/store.ts:42,104` | `NodeRow.errorClass: string \\| null` + `mapNode` 映射 |\n| `src/database/migration.gen.ts:50` | 注册新迁移(时序末位) |\n| `src/database/schema.gen.ts:86` | DDL 快照加列 |\n| `schema.json:4-7,597-606` | 版本 id 轮换 + 新列实体(生成物) |\n\n**意图**:error_class 落库。枚举源头已核验:`packages/schema/src/dag-event.ts:253` — `trigger: Schema.Literals([\"exec_failed\",\"push_exhausted\",\"verdict_fail\",\"timeout\"])`,共 **4 值**;projector 直接透传 `event.data.trigger`,无转换层。\n\n### 区域 B — 读面(packages/opencode,4 文件)\n| 文件 | 变更 |\n|---|---|\n| `src/tool/workflow.ts:204` | `status` action 的节点 JSON 增加 `error_class`(非空才输出) |\n| `src/dag/runtime/loop.ts:852-879` | Wake 摘要两处:① 节点行 `Node \"X\" failed (errorClass): reason`;② **新增**:batch 内每个 status===\"failed\" 的 workflow 调 `store.getNodes` 重查失败节点,workflow 终止行追加 `Failed nodes:\\n- \"name\" (class): reason`(单行截断 300 字符,getNodes 失败静默降级为空) |\n| `src/server/routes/instance/httpapi/groups/dag.ts:42-44` | `NodeResponse` 新增可选 `error_class`(注释列四值) |\n| `src/server/routes/instance/httpapi/handlers/dag.ts:65` | handler 映射 `errorClass → error_class`(非空才输出) |\n\n**意图**:三条 agent/客户端可见路径(workflow tool status、父会话 wake prompt、HttpAPI/TUI SDK)统一暴露 error_class。loop.ts 是唯一有**运行时行为变化**的文件(新增一次 store 读 + 摘要格式变化),wake-path 竞态/顺序审查重点在 `loop.ts:839-849` 的 idle/抢占守卫与新 getNodes 调用之间。\n\n### 区域 C — SDK(packages/sdk/js,1 文件)\n`src/v2/gen/types.gen.ts:3909`:`DagNode.error_class?: string`,位置紧随 `error_reason`,**恰好一个字段**。\n**意图**:OpenAPI → SDK 再生成的产物。**佐证物**:未跟踪的 `packages/sdk/js/openapi.json`(今日 15:44 重新生成;该文件已于 commit `881ca8643` 移出 git 跟踪,属构建产物——见第 4 节审计项)。\n\n### 区域 D — 指导文档(packages/core/src/plugin/command,2 文件)\n| 文件 | 变更 |\n|---|---|\n| `workflow.md:401-427` | 新增 \"Node failure triage\" 节:error_class → 响应动作映射表(timeout/exec_failed/verdict_fail 三行 + Dependency 级联行)、budget 耗尽单列、live vs terminal 两种修复路径(pause→replan→resume / continuation workflow)、硬规则\"环境性单节点失败不得整图重启\" |\n| `dag-flow.txt:35-44` | 新增 \"Resume-first\" 节:中断工作流优先续跑(status 读取→暂停恢复→续跑已完成波→最后才重启),含 fail-closed 守卫 |\n\n**意图**:把新暴露的 error_class 转化为父 agent 的行为指导。**注意**:两文档的枚举列举均只写 3 值(timeout/exec_failed/verdict_fail),**遗漏 schema 枚举的第 4 值 `push_exhausted`**(见第 4 节审计项)。\n\n### 区域 E — 测试(packages/opencode/test/dag,3 文件)\n| 文件 | 变更 |\n|---|---|\n| `fixtures.ts:18` | `makeNodeRow` 默认 `errorClass: null` |\n| `dag-wake-integration.test.ts:448,695-731` | ① exec_failed 场景断言持久化 `errorClass`;② 聚合节点占位符失败场景:断言 `errorClass===\"verdict_fail\"` + **新增 wake 文本断言**(`[DAG Workflow failed]...` 与 `Failed nodes:\\n- \"summary\" (verdict_fail):`),并补 `parent.release` 放行 |\n| `workflow-tool.test.ts:189-212,413-414` | mock store 新增 `node_failed`(errorClass:\"timeout\")节点,status 输出断言 `\"error_class\": \"timeout\"` |\n\n**意图**:覆盖持久化、wake 归因文本、tool status 三条新路径。\n\n## 2. 参考清单(Reference Manifest,按要求格式输出,含一处事实更正)\n\n- **reference_template**: `deep-review-dag-module`\n- **added nodes**: `scope-diff`, `review-dataflow`, `review-runtime`, `review-contract`, `review-prompts`, `review-tests`, `review-style`, `verify-suite`\n- **pruned lanes**:\n - `{node: explore-core, prune_reason: \"target is a bounded 17-file change (16 tracked + 1 untracked new migration), not the whole module\", replacement_coverage: \"scope-diff consolidated change map injected into every reviewer lane\"}`\n - `{node: explore-runtime, prune_reason: 同上, replacement_coverage: 同上}`\n - `{node: explore-templates, prune_reason: 同上, replacement_coverage: 同上}`\n - `{node: explore-integrations, prune_reason: 同上, replacement_coverage: 同上}`\n\n> **事实更正说明**:原稿 prune_reason 写 \"16-file diff\"。`git diff HEAD --stat` 确实是 16 个已跟踪文件,但变更集还包含 1 个**未跟踪新迁移文件**(`20260803073521_workflow_node_error_class.ts`,migration.gen.ts 引用它),完整变更集为 17 文件。已更正计数,其余原样保留。\n\n## 3. 与声明目的无关的 hunk → **无**\n\n逐文件核对:16 个已跟踪文件的所有 hunk 均服务于\"持久化 error_class / 暴露到读面 / 分诊指导\"三目的之一,**未发现无关改动**。以下 4 项为**仲裁审计项**(非无关 hunk,但需裁定):\n\n1. **[审计·提示词一致性]** `workflow.md` 分诊表与 `dag-flow.txt:37` 的枚举列举均缺 `push_exhausted`(schema 枚举第 4 值,`dag-event.ts:253`;sql.ts 与 groups/dag.ts 注释均列齐 4 值)。review-prompts 裁定:遗漏是否构成指导缺口。\n2. **[审计·归因语义]** `dag.ts:492`(**diff 外既有代码**):workflow 级 fail/cancel 终止 running 节点时硬编码 `trigger: \"exec_failed\"` 发 NodeFailed——此类节点也会落 error_class,但其语义是\"被工作流连坐\"而非节点自身执行失败;且 `terminateNonTerminalNodes` 的 skip 分支(NodeSkipped)不产生 error_class。review-runtime 评估新指导文档对这类节点的表述是否准确。\n3. **[审计·仓库契约]** 未跟踪 `packages/sdk/js/openapi.json`(今日 15:44 生成,commit `881ca8643` 已将其移出跟踪,当前也未被 .gitignore 忽略)——确认为构建产物即可,但 review-contract 应核验 `check:generated` 与 httpapi exercise 契约(AGENTS.md:路由形状变更需更新 `test/server/httpapi-exercise`;本次 NodeResponse 新增了可选字段)。\n4. **[审计·测试覆盖]** 无任何测试断言 `push_exhausted` 类的端到端路径;workflow-tool.test.ts 新增节点的缩进格式异常(`}, {` 行,`workflow-tool.test.ts:189-212`)交 review-style。\n\n## 4. 既有未提交内容声明\n\n`dag-flow.txt` 的 \"Resume-first\" 整节(+35-44)**早于本会话的分诊句子**撰写,属同一未提交工作树的既有内容;按用户要求作为联合 diff 的一部分审查。其与 workflow.md 新分诊节存在**交叉引用**(\"triage per the Node failure triage section in the workflow guidance\")——两节必须作为整体审查一致性(续跑顺序、fail-closed 守卫、与 error_class 值集的吻合)。\n\n## output_variables\n- **targets**: [\n error_class 列@packages/core/src/dag/sql.ts:65,\n NodeFailed 投影@packages/core/src/dag/projector.ts:285,\n NodeRow.errorClass@packages/core/src/dag/store.ts:42,\n 迁移@packages/core/src/database/migration/20260803073521_workflow_node_error_class.ts,\n wake 摘要归因@packages/opencode/src/dag/runtime/loop.ts:852-879,\n status 输出@packages/opencode/src/tool/workflow.ts:204,\n NodeResponse.error_class@packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts:42,\n handler 映射@packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts:65,\n DagNode.error_class@packages/sdk/js/src/v2/gen/types.gen.ts:3909,\n 分诊节@packages/core/src/plugin/command/workflow.md:401-427,\n Resume-first 节@packages/core/src/plugin/command/dag-flow.txt:35-44,\n trigger 枚举源@packages/schema/src/dag-event.ts:253\n]\n- **impacted_processes**: [NodeFailed 投影落库, 父会话 wake 投递(loop.ts idle/抢占守卫后的摘要构建), workflow tool status, HttpAPI dag 节点查询, SDK 生成链(openapi → types.gen)]\n- **test_anchors**: [\n packages/opencode/test/dag/dag-wake-integration.test.ts:448(exec_failed 持久化),\n packages/opencode/test/dag/dag-wake-integration.test.ts:693-731(verdict_fail + wake 文本),\n packages/opencode/test/dag/workflow-tool.test.ts:413-414(status error_class),\n packages/opencode/test/dag/fixtures.ts:18\n]\n- **arbiter_audit_items**: [push_exhausted 文档遗漏, dag.ts:492 连坐节点 error_class 语义, sdk/openapi.json 产物与 check:generated/httpapi-exercise 契约, push_exhausted 无测试覆盖 + 测试缩进]\n- **ast_available**: true(codebase-memory 图谱可用;本次以 git diff 原始 hunk + rg 交叉核验为准,未依赖图谱推断)" diff --git a/.opencode/.dag-specs/review-parts-diff/verify-suite.md b/.opencode/.dag-specs/review-parts-diff/verify-suite.md deleted file mode 100644 index f38486d23e..0000000000 --- a/.opencode/.dag-specs/review-parts-diff/verify-suite.md +++ /dev/null @@ -1 +0,0 @@ -{"verdict":"PASS","results":[{"gate":"typecheck core","command":"bun run typecheck (packages/core)","outcome":"PASS","detail":"tsgo --noEmit exited 0 with no diagnostics (only '$ tsgo --noEmit' in output)."},{"gate":"typecheck opencode","command":"bun run typecheck (packages/opencode)","outcome":"PASS","detail":"tsgo --noEmit exited 0 with no diagnostics."},{"gate":"opencode DAG suites","command":"bun test test/dag (packages/opencode)","outcome":"PASS","detail":"324 pass / 0 fail, 841 expect() calls, 26 files (incl. dag-wake-integration.test.ts and workflow-tool.test.ts which carry the new error_class assertions). Decisive fragment: '324 pass\\n 0 fail ... Ran 324 tests across 26 files'. WARN log lines during run are intentional negative-path scenarios (e.g. 'DAG wake delivery failed', 'paused workflow after recovery invented node failures') whose paired tests all passed."},{"gate":"core DAG suites","command":"bun test test/dag-core.test.ts test/dag-projector-drift.test.ts test/dag-store-summaries.test.ts test/plugin/command.test.ts (packages/core)","outcome":"PASS","detail":"101 pass / 0 fail, 493 expect() calls across 4 files. Decisive fragment: '101 pass\\n 0 fail ... Ran 101 tests across 4 files'."},{"gate":"migration check","command":"bun script/migration.ts --check (packages/core)","outcome":"PASS","detail":"Incremental check printed 'No schema changes, nothing to migrate 😴' (schema.gen/migrations in sync with drizzle schema incl. the new error_class column); full-migration generation succeeded ('[✓] Your SQL migration ➜ .../full/20260803080041_schema/migration.sql'), exit 0."},{"gate":"HttpAPI contract","command":"bun run test:httpapi --fail-on-missing (packages/opencode)","outcome":"PASS","detail":"Both runs (mode=auth and mode=effect, selected=225, effectRoutes=192) ended with 'summary pass=225 fail=0 skip=0 missing=0 extra=0'. All /dag routes incl. dag.nodes/dag.nodeDetail passed; missing=0 satisfies --fail-on-missing."},{"gate":"SDK freshness","command":"bun run build (packages/sdk/js) then git diff -- packages/sdk/js/src/v2/gen","outcome":"PASS","detail":"Regeneration succeeded (@hey-api v0.90.10, '[Job 1] ✅ Done!'). Post-build diff is exactly the intended field: types.gen.ts DagNode gains one line '+ error_class?: string' after error_reason. git diff --stat: '1 file changed, 1 insertion(+)'; git status --porcelain shows only 'M packages/sdk/js/src/v2/gen/types.gen.ts' — no other drift, no untracked gen files. Matches the stated pass interpretation."}],"anomalies":["Test-suite WARN logs in gates 3/4 are expected negative-path coverage (delivery failure, ownership-loss recovery), not defects — all associated tests passed.","packages/sdk/js/openapi.json is untracked (build artifact per commit 881ca8643, not .gitignored); it was used as the regeneration input and produced zero drift beyond the intended field — no gate impact.","Gate 7 build output lists src/v2/gen files without the '(unchanged)' marker (rewritten), but byte-level git diff confirms the sole change is the intended error_class insertion."]} diff --git a/.opencode/.dag-specs/review-parts-final/review-config-repo.md b/.opencode/.dag-specs/review-parts-final/review-config-repo.md deleted file mode 100644 index 5e73e3b6e0..0000000000 --- a/.opencode/.dag-specs/review-parts-final/review-config-repo.md +++ /dev/null @@ -1 +0,0 @@ -"## 配置仓库引用模板机制评审(feat/dag-config-repo @ 494b3b463)\n\n**评审环境说明**:评审过程中工作区被并行会话从 `feat/dag-config-repo` 切换到 `feat/goal-restore`;此后所有文件证据均通过 `git show feat/dag-config-repo:` 固定在该分支 HEAD(494b3b463)读取,未修改任何文件。跨分支比对确认 `workflows.ts / generate.ts / build.ts / command.ts / dag-template-update.txt` 在两个堆叠分支上逐字一致。\n\n```json\n{\n \"findings\": [\n {\n \"severity\": \"HIGH\",\n \"title\": \"Windows 构建的 DAG_TEMPLATES_DIR 缺少 cygpath 路径转换,与同文件 models.dev 步骤的处理不对称\",\n \"description\": \"build-cli 在 3-OS 矩阵上运行。Git Bash (windows-latest) 中 $GITHUB_WORKSPACE 会被 MSYS 转换为 POSIX 形式(/d/a/...),Extract Templates 步骤把该原始值直接写入 GITHUB_ENV。同文件上方 models.dev 步骤正是因为同样问题显式使用 cygpath -m 转回 Windows 形式后才写入 GITHUB_ENV,证明 POSIX 形式确实会到达原生消费者。原生 bun 收到 /d/... 路径后,generate.ts 的 Bun.Glob.scan({cwd}) 对不存在目录会抛 ENOENT(本地实测确认),导致 Windows release 构建失败(或退化为静默丢失 builtin 模板)。\",\n \"evidence\": \".github/workflows/release-fork.yml:170-175 vs .github/workflows/release-fork.yml:145-152; packages/opencode/script/generate.ts:51\",\n \"recommendation\": \"与 models.dev 步骤对齐:`if command -v cygpath; then DAG_TEMPLATES_DIR=$(cygpath -m \\\"$GITHUB_WORKSPACE/dag-templates-src\\\"); fi` 后再写入 GITHUB_ENV;或在 generate.ts 中对目录不存在给出带上下文的可读错误。\"\n },\n {\n \"severity\": \"MEDIUM\",\n \"title\": \"配置仓库引用完全浮动(默认分支 HEAD / refs/heads/main),无版本钉扎与完整性校验\",\n \"description\": \"package-templates 的 checkout 未指定 ref,release 时取 opencode-dag-config 默认分支 HEAD;/dag-template-update 下载 refs/heads/main 的 zip,且文案称其为 “pinned repository URL”(钉住的是仓库,不是版本)。两个消费点都无校验和/签名验证,配置仓库上的任意新提交(含误推的 WIP)会自动进入下一个 release 的二进制内嵌模板、release 资产和用户全局配置目录(与 dag.jsonc 同信任级,可被 agent 直接执行)。同一所有者的仓库降低了攻击可能性,但该设计为有意“取最新”,故评为 MEDIUM。\",\n \"evidence\": \".github/workflows/release-fork.yml:72-77(with: repository,无 ref); packages/core/src/plugin/command/dag-template-update.txt:27-31\",\n \"recommendation\": \"为 workflow_dispatch 增加可选的 config-repo ref/tag 输入(默认 main)并在 release notes 记录实际解析的 SHA;修正 dag-template-update.txt 中 “pinned” 措辞。\"\n },\n {\n \"severity\": \"MEDIUM\",\n \"title\": \"模板打包位于 release 关键路径且无重试,失败模式不自洽\",\n \"description\": \"build-cli/release 均 needs: package-templates,配置仓库 checkout 的一次瞬时故障(网络抖动、仓库改名/转私有)会阻塞整个二进制发布。同文件 models.dev 下载有 3 次重试 + 优雅降级,模板步骤没有任何重试。且语义不对称:配置仓库为空 → 仅 ::warning:: 并继续发布(零 builtin);clone 失败 → 整个发布硬失败。\",\n \"evidence\": \".github/workflows/release-fork.yml:101,220(needs); :81-89(空 glob 仅告警); :143-158(models.dev 的 3 次重试对照)\",\n \"recommendation\": \"给 Clone Config Repo 加有限重试;或 package-templates continue-on-error + 在 build-cli 中容忍缺失(退化为无 builtin),使模板故障与二进制发布解耦。\"\n },\n {\n \"severity\": \"MEDIUM\",\n \"title\": \"指导文本仍描述两级 scope,与运行时三级解析(含 builtin)漂移\",\n \"description\": \"workflows.ts 的解析序为 project > global > builtin,但 workflow.md(workflow 工具自身描述)只列两级;dag-flow.txt 明确写 “two scopes”。release 二进制在干净环境下四个推荐模板全部只能从 builtin 解析,agent 按文本去 /workflows 会看到空目录(workflow(action:\\\"list\\\") 指令可兜底)。此外 list 输出展示 builtin://name 路径,若 agent 照抄作为 spec_path,isName 因含 “/” 拒绝、扩展名检查报 “must be a .yaml or .yml file”——一个由文档诱导的可恢复陷阱。2ee59d874 只更新了运行时 hint,未更新静态文本。\",\n \"evidence\": \"packages/core/src/plugin/command/workflow.md:84-88; packages/core/src/plugin/command/dag-flow.txt:13-16; packages/opencode/src/dag/workflows.ts:9-14; packages/opencode/src/tool/workflow.ts:153-160,401-417\",\n \"recommendation\": \"workflow.md 与 dag-flow.txt 增补 builtin 第三级说明(release 内嵌、无磁盘文件、按名称启动);list 对 builtin 条目提示“仅可按名称启动”,或让 resolveSpecPath 直接接受 builtin:// 形式。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"builtin 层零自动化测试覆盖\",\n \"description\": \"2ee59d874 删除了旧的 change-review 规格测试后,没有任何测试替代:resolve/list 的 builtin 优先级与遮蔽、isBuiltinPath/builtinName、parseMeta 的容错路径(2ee59d874 新增的 catch)均未被测试。测试可通过 globalThis.OPENCODE_DAG_TEMPLATES 注入实现,成本低。\",\n \"evidence\": \"packages/opencode/test/dag/dag-workflows.test.ts(全文无 builtin 引用); 2ee59d874 删除段落\",\n \"recommendation\": \"补充注入式单测:builtin 被 project/global 同名遮蔽、builtin 单独命中、malformed builtin 内容在 list 时不抛错。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"嵌入确定性弱:模板按文件系统扫描顺序注入,且仅 *.yaml\",\n \"description\": \"loadDagTemplatesData 按 Bun.Glob.scan 的未排序顺序插入键,3-OS 矩阵产出的二进制内嵌 JSON 键序可能不同(功能无影响:按键查找、list 已排序,但字节级不确定)。另外 glob 只取 *.yaml,而 workflows.ts EXTENSIONS 同时接受 .yml——手工设置 DAG_TEMPLATES_DIR 指向含 .yml 的目录会静默丢模板;tar 打包也只复制 *.yaml,行为一致但未文档化。\",\n \"evidence\": \"packages/opencode/script/generate.ts:50-55; packages/opencode/src/dag/workflows.ts:33; .github/workflows/release-fork.yml:82-86\",\n \"recommendation\": \"对扫描结果排序后再写入 Record;在注释或文档中声明“仅根目录 *.yaml 被打包/内嵌”。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"/dag-template-update 锁语义存在两处含糊\",\n \"description\": \"1) 指令要求“下载或合并前”先 mkdir 锁目录,但当 /workflows 尚不存在时 mkdir 以 ENOENT 失败(非 EEXIST),agent 可能误判为“锁被占用”;创建目录的指令放在文末 Failure handling,顺序含糊。2) 崩溃遗留的陈旧锁没有 TTL/接管指引,会无限期阻塞后续更新,唯一恢复路径是用户手工删除。预览分类、QA 决策门、备份失败即中止、内容级验证等核心流程设计合理;备份文件 .bak- 后缀不会被 list() 误收(仅 .yaml/.yml),无库污染。\",\n \"evidence\": \"packages/core/src/plugin/command/dag-template-update.txt:65-74(锁),86-88(目录创建),56-62(备份)\",\n \"recommendation\": \"把“目录不存在则先创建”移到锁步骤之前;锁目录内写一个时间戳文件,锁龄超过阈值(如 30 分钟)即视为陈旧并接管。\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"chore 提交与前一提交的设计声明存在表述张力(行为无问题)\",\n \"description\": \"98e4c0624 以 “config repo is the sole source; builtin + global scopes cover runtime” 为由删除受跟踪模板,随后 494b3b463 又向 .opencode/workflows/ 添加 8 个项目级工作流 yaml。项目 scope 容纳项目专属规格在语义上合法(project > global > builtin 已文档化),功能无冲突,但两次提交的声明读起来相互矛盾,易误导后续维护者。8 个 yaml 全部通过解析、depends_on 闭合、prompt_template.id 均存在于 .opencode/dag-prompts。\",\n \"evidence\": \"98e4c0624 commit message; 494b3b463 新增 .opencode/workflows/*.yaml(8 个文件)\",\n \"recommendation\": \"无需代码改动;如保留这批文件,在 PR 描述中说明“reference 模板归配置仓库,项目级工作流仍可在仓库内维护”的边界。\"\n }\n ],\n \"unverified_claims\": [\n \"finding 1 的最终形态依赖两点本机无法验证的事实:windows-latest Git Bash 中 $GITHUB_WORKSPACE 的实际形式,以及原生 Windows bun 对 /d/... 路径的解析行为(若 bun 恰好能解析则影响消失,但 :146-147 的 cygpath 不对称仍成立)\",\n \"CI 管道未实际执行:actions/checkout@v4 以调用方令牌拉取外部公开仓库(contents: read)的实际行为、artifact 在 job 间的传递均未实测,release-fork.yml 全部结论为静态分析\",\n \"opencode-dag-config 仓库内容不在评审范围:无法确认 design-decision-loop / parallel-development-loop / deep-review-dag-module / change-review 四个名字确实存在于该仓库根目录(dag-flow.txt 按名引用它们;若缺失,release builtin 不会提供)\",\n \"无法确认 opencode-dag-config 是否公开、默认分支是否有保护/CI 门禁(供应链缓解措施评估的前提)\",\n \"本轮为只读评审,未运行 typecheck/测试/httpapi 门禁(客观门禁由独立的 gate-runner 波次执行)\",\n \"release job 在 7 天 artifact 保留期后被单独重跑的边缘场景(dag-templates retention-days:7 vs 二进制 artifact 默认保留期)未评估\"\n ],\n \"summary\": \"机制整体结构健全:package-templates 写安全已验证(contents: read、不触碰主仓库内容、无 push),builtin 嵌入忠实镜像 OPENCODE_MODELS_DEV 模式且带 typeof 防护,三级解析与 list/resolve 的遮蔽语义一致,2ee59d874 确实闭合了其声称的全部缺陷(含“删除冗余下载”经验证安全——无名 download-artifact + merge-multiple 仍会把 dag-templates.tar.gz 带入 release 资产与 SHA256SUMS),chore 产物 16 个 yaml 全部合法、未发现密钥或本机路径泄漏。首要风险是 release-fork.yml 中 Windows 路径形式缺陷(HIGH,同文件已有现成的 cygpath 解法可照搬),其次是浮动引用无钉扎、发布关键路径无重试与静态指导文本滞后于三级解析(各 MEDIUM),这些都不阻塞合并方向但建议在发布前修复 HIGH 项。\"\n}\n```\n\n**D5 专项结论**:fix commit 2ee59d874 处理的审查发现均真实解决——ReferenceError 防护(workflows.ts:51-56)、malformed builtin 容错(parseMeta catch,workflows.ts:158-161)、重复 Entry 接口去重、not-found hint 含 builtin(workflow.ts:393-398)、空 glob cp 崩溃修复(release-fork.yml:81-89)、release job 冗余下载移除(验证安全)。唯一残留是它没触及的静态文档漂移(上文 MEDIUM finding 4)。" diff --git a/.opencode/.dag-specs/review-parts-final/review-stack.md b/.opencode/.dag-specs/review-parts-final/review-stack.md deleted file mode 100644 index 48717f2a11..0000000000 --- a/.opencode/.dag-specs/review-parts-final/review-stack.md +++ /dev/null @@ -1 +0,0 @@ -"All checks complete. Here is the stack-integrity verdict.\n\n## 1. Findings\n\n```json\n[\n {\n \"severity\": \"LOW\",\n \"title\": \"Stack base drift: 494b3b463 on feat/dag-config-repo is not an ancestor of the upper stack branches\",\n \"description\": \"The stack is not perfectly linear. feat/dag-config-repo HEAD is 494b3b463 (workflow library specs + deep-review artifacts, pushed to origin), but the merge-base of feat/dag-config-repo with both upper branches is 2ee59d874. feat/dag-error-class and feat/goal-restore were cut from the 4-commit config-repo mechanism before the artifacts commit landed, so PR #169/#170 bases point at the pre-artifact state. No content is lost (the artifacts merge via PR #171) and no conflict is possible (494b3b463 touches only .opencode/, disjoint from both bundles), but 'feat/goal-restore contains the complete stacked final state' holds only modulo the artifacts, which live solely on the config-repo branch.\",\n \"evidence\": \"git merge-base feat/dag-config-repo feat/dag-error-class = 2ee59d874; remote tip refs/heads/feat/dag-config-repo = 494b3b463; git ls-tree shows .opencode/.dag-specs/* and .opencode/workflows/* present on feat/dag-config-repo but absent on feat/dag-error-class and feat/goal-restore\",\n \"recommendation\": \"Either rebase feat/dag-error-class (and goal-restore) onto 494b3b463 for a linear stack, or explicitly enforce merge order (config-repo/PR #171 first) in the merge plan. Optionally confirm the artifacts commit is intentional PR content, not a stray local push.\"\n },\n {\n \"severity\": \"LOW\",\n \"title\": \"Review artifacts (.opencode/.dag-specs) committed to the repo\",\n \"description\": \"The 494b3b463 commit deliberately tracks ~26 deep-review session artifacts (admission briefs, manifests, gate results, final audit report) plus 8 project workflow library specs under .opencode/. The commit message states this is intentional evidence-keeping. Hygiene scan found no real secrets (only innocent prose hits: 'key token' in glossary/tr.md and a Chinese sentence containing 'token' in algo-complexity-review.yaml). Flagged only so the team can confirm these belong in the public repo's merge path.\",\n \"evidence\": \"git show --stat 494b3b463 (.opencode/.dag-specs/*, .opencode/workflows/*); secret-pattern grep over 494b3b463 -- .opencode/ produced only glossary/prose matches\",\n \"recommendation\": \"Confirm intent; if these are evidence only, consider a private branch or .gitignore'd location instead of the merge path.\"\n }\n]\n```\n\nNo wrong-layer files, no reverts, no dangling imports, no wip leftovers, no secrets — all four verification axes pass (details in summary).\n\n## 2. Unverified claims\n\n- Objective gates (typecheck, test suites, httpapi, migration `--check`, SDK regen determinism) at `feat/goal-restore` HEAD were not re-executed in this session; per prior adjudications they pass, and this review scope was branch topology, not execution.\n- GitHub PR #169/#170/#171 base/target settings (that the diff each PR will show equals the branch-vs-branch diffs computed here) not confirmed via `gh` — verified against local refs whose tips match origin exactly (`4e8eb7e68`, `ca085b0d9`, `494b3b463` all confirmed on origin).\n- Whether merge order will actually be enforced (config-repo/PR #171 before the upper two) — no evidence; depends on the merge plan.\n- `packages/sdk/js/.gitignore` (`openapi.json`) newly added in the error_class bundle is presumed intentional (SDK regen artifact hygiene), not a stray file.\n\n## 3. Summary\n\nStack attribution is clean: diff(`2ee59d874`→`4e8eb7e68`) is exactly the 23-file error_class bundle with zero `goal` references and no goal module in its tree; diff(`4e8eb7e68`→`ca085b0d9`) is exactly the 47-file goal bundle with purely additive edits to the six shared files. Generated artifacts layer correctly at each branch — `migration.gen.ts` lists only `20260803073521_workflow_node_error_class` on the error-class branch and adds `20260803083938_restore_goal_state` on goal-restore; schema baseline, `types.gen.ts`, `sdk.gen.ts` (goal-only, correct since error_class is a field not a route), and `schema.json` follow the same ordering. The wip snapshot (`1b53e9e9c`) decomposes exactly into the stack: its only delta against `feat/goal-restore` is the 34 `.opencode/` artifact files, all carried by `494b3b463` on the config-repo branch — nothing leaked and nothing is missing. The single actionable note is the LOW stack-base drift (upper branches cut before the artifacts commit), which is risk-free content-wise but must be reflected in merge order." diff --git a/.opencode/.dag-specs/review-parts-final/verify-suite.md b/.opencode/.dag-specs/review-parts-final/verify-suite.md deleted file mode 100644 index 04cfa6fa17..0000000000 --- a/.opencode/.dag-specs/review-parts-final/verify-suite.md +++ /dev/null @@ -1 +0,0 @@ -{"verdict":"PASS","results":[{"gate":"1. typecheck core","command":"bun run typecheck (packages/core)","outcome":"PASS","detail":"tsgo --noEmit exit 0, no errors"},{"gate":"2. typecheck opencode","command":"bun run typecheck (packages/opencode)","outcome":"PASS","detail":"tsgo --noEmit exit 0, no errors"},{"gate":"3. typecheck tui","command":"bun run typecheck (packages/tui)","outcome":"PASS","detail":"tsgo --noEmit exit 0, no errors"},{"gate":"4. opencode suites","command":"bun test test/dag test/goal test/tool/goal-tool.test.ts test/session/prompt.test.ts (packages/opencode)","outcome":"PASS","detail":"468 pass, 1 skip, 0 fail across 33 files (1298 expect() calls, 48.00s); skip is pre-existing 'v2 projector disabled'"},{"gate":"5. core suites","command":"bun test test/dag-core.test.ts test/dag-projector-drift.test.ts test/dag-store-summaries.test.ts test/plugin/command.test.ts (packages/core)","outcome":"PASS","detail":"101 pass, 0 fail across 4 files (493 expect() calls)"},{"gate":"6. tui sync suites","command":"bun test test/cli/cmd/tui/sync-goal.test.tsx test/cli/cmd/tui/sync-dag.test.tsx (packages/tui)","outcome":"PASS","detail":"8 pass, 0 fail across 2 files incl. goal.updated/cleared slice assertions"},{"gate":"7. migration check","command":"bun script/migration.ts --check (packages/core)","outcome":"PASS","detail":"incremental-vs-full schema comparison clean: 'No schema changes, nothing to migrate'; full migration regenerated in temp dir; exit 0"},{"gate":"8. HttpAPI contract","command":"bun run test:httpapi --fail-on-missing (packages/opencode)","outcome":"PASS","detail":"summary pass=227 fail=0 skip=0 missing=0 extra=0; exit 0; includes dag.* and session.goal scenarios"},{"gate":"9. SDK regen determinism","command":"bun run build then git diff -- packages/sdk/js/src/v2/gen (packages/sdk/js)","outcome":"PASS","detail":"build exit 0 (sdk.gen.ts/types.gen.ts regenerated); git diff and git status on packages/sdk/js/src/v2/gen both empty — committed gen files exactly reproduced"}],"anomalies":["Repo was found on feat/dag-config-repo, not feat/goal-restore as the brief assumes; checked out local feat/goal-restore (ca085b0d9, in sync with origin) to run all gates, then restored feat/dag-config-repo; only pre-existing untracked file .opencode/.dag-specs/final-confirmation-three-pr-stack.yaml remained","Gate 4 has 1 skipped test ('prompt emits v2 prompted and synthetic events (v2 projector disabled)') — pre-existing deliberate skip, not a failure","Migration check wrote generated SQL only to a temp dir (/var/folders/.../opencode-core-migration-check-*); no repo files modified"]} diff --git a/.opencode/.dag-specs/review-parts-round3/final-audit-report.md b/.opencode/.dag-specs/review-parts-round3/final-audit-report.md deleted file mode 100644 index 548164e8e8..0000000000 --- a/.opencode/.dag-specs/review-parts-round3/final-audit-report.md +++ /dev/null @@ -1,73 +0,0 @@ -# Final Audit Report — Joint Uncommitted Diff (Round 3) - -- **Verdict**: PASS -- **Bounded loop**: round 3 of max 2 replans — goal met, no further loop warranted -- **Scope**: uncommitted working-tree diff (DAG `error_class` exposure, `/goal` restoration, two remediation waves; ~40 modified files) -- **Objective gates**: `.opencode/.dag-specs/review-parts-round3/verify-suite.md` — verdict PASS, 8/8 gates PASS (read directly from disk; primary evidence) -- **Closure review**: review-final-2 (fresh context, 30-min budget) over working tree + gate result file - -## 1. Closure table — round-2 HIGH findings - -All five round-2 HIGH findings are CLOSED with file:line fix evidence. - -| # | Round-2 HIGH finding | Status | Fix evidence (file:line) | -|---|---|---|---| -| F1 | workflow.md "Cascade detection": required-failure shape must state dependents are terminalized to `skipped` with error_reason `workflow_failed` (pending only while paused) | CLOSED | `packages/core/src/plugin/command/workflow.md:419` matches runtime `packages/opencode/src/dag/dag.ts:482-504,523-527` and `packages/opencode/src/dag/loop.ts:271-273`; paused parenthetical accurate per `packages/core/src/dag/core/types.ts:204-205`, `dag.ts:290-297`, `loop.ts:638` | -| F2 | workflow.md exec_failed row (c): must gate on `error_reason`, not rely on surfaced workflow-level reason or universal primary-node attribution; must carry an `orchestrator_unresponsive` recipe | CLOSED | `workflow.md:413` row (c) gates on `error_reason`; zero-attribution recipe at `workflow.md:424-428`; matches `loop.ts:879-883,852-869,272,830` and `packages/opencode/src/dag/scheduling.ts:215-217` | -| F3 | dag-flow.txt: error_class sentence must carry replan-cancel + pre-migration exceptions | CLOSED | `packages/core/src/plugin/command/dag-flow.txt:37` carries both exceptions; consistent with `packages/core/src/dag/projector.ts:320-323` | -| F4 | prompt.test.ts: meaningful coverage of `/goal set+kick`, `/goal status`, `/subgoal`, Goal-absent fall-through (assertion strength, not just existence) | CLOSED | `packages/opencode/test/session/prompt.test.ts:2286-2378` — set+kick asserts loop result/echo/persisted state/exactly-1 LLM call; status and subgoal assert rendered/persisted state + 0 LLM calls; fall-through asserts negative marker only (recorded as residual R2 below) | -| F5 | system.ts: Goal.defaultLayer provided + Goal.node in LayerNode deps (goal block reachable); no import-cycle hazard vs deferred SettingsHook pattern | CLOSED | `packages/opencode/src/session/system.ts:181,187` — Goal.defaultLayer provided, Goal.node in node deps; production reachability via `packages/opencode/src/effect/app-runtime.ts:100` → `packages/opencode/src/session/prompt.ts:2101` and `prompt.ts:2256`; lazy `serviceOption` resolution at `system.ts:69` with no import cycle | - -Regression spot-checks (all pass): - -- error_class pipeline intact: projector → store → tool status → wake digest → httpapi NodeResponse → SDK -- app-runtime provideMerge comment accurate vs `packages/opencode/src/dag/loop.ts:373-380` and `packages/opencode/src/hook/settings.ts:2174-2176` self-provides -- GOAL command description includes `done`; dispatch handles `done` at `packages/opencode/src/command/index.ts:98` and `packages/opencode/src/goal.ts:584-590` - -## 2. Gate results (objective evidence, persisted on disk) - -Source: `.opencode/.dag-specs/review-parts-round3/verify-suite.md` — `{"verdict":"PASS"}`, 8/8 PASS. - -| Gate | Command | Outcome | Detail | -|---|---|---|---| -| 1. typecheck core | `bun run typecheck` (packages/core) | PASS | tsgo --noEmit exit 0, no diagnostics | -| 2. typecheck opencode | `bun run typecheck` (packages/opencode) | PASS | tsgo --noEmit exit 0, no diagnostics | -| 3. DAG suites | `bun test test/dag` (packages/opencode) | PASS | 324 pass / 0 fail, 845 expect() calls across 26 files | -| 4. goal + dispatch suites | `bun test test/goal test/tool/goal-tool.test.ts test/session/prompt.test.ts` | PASS | 143 pass / 0 fail, 1 pre-existing marked skip across 7 files | -| 5. core suites | `bun test` (4 core test files) | PASS | 101 pass / 0 fail, 493 expect() calls across 4 files | -| 6. migration check | `bun script/migration.ts --check` | PASS | EXIT=0; "No schema changes, nothing to migrate" | -| 7. HttpAPI contract | `bun run test:httpapi --fail-on-missing` | PASS | pass=226 fail=0 skip=0 missing=0 extra=0, EXIT=0 | -| 8. SDK freshness | `bun run build` (packages/sdk/js) + git diff gen | PASS | EXIT=0; pure additive gen diff (69+/0-) matching intended set exactly | - -Gate anomalies (recorded, non-failures): - -- Gate 4: 1 pre-existing marked skip ("v2 projector disabled"), not a failure — accepted residual R5 -- Gate 8: gen diff is entirely uncommitted working-tree additions, consistent with the stated interpretation — accepted residual R6 - -## 3. Confirmed residual items (accepted, non-blocking) - -Newly confirmed observations from the closure review — all LOW, none loop-worthy: - -| # | Severity | Status | Item | Evidence | Disposition | -|---|---|---|---|---|---| -| R1 | LOW | CONFIRMED | Cascade doc lists only pending/queued; `terminateNonTerminalNodes` terminalizes all non-terminal rows (incl. node-level paused). Non-exhaustive, not wrong. Optional doc polish. | `workflow.md:419`; `dag.ts:485` | Accepted, non-blocking | -| R2 | LOW | CONFIRMED | Goal-absent fall-through test asserts only absence of "目标已设定"; a silent no-op would also pass. Optional: assert a positive fall-through outcome later. | `prompt.test.ts:2360-2378` | Accepted, non-blocking | -| R3 | LOW | PARTIALLY_CONFIRMED | Paused-workflow required-failure closure verified by code-path reasoning, not scenario execution. Guard logic explicit and simple; reasoning sound. Optional: add an executed scenario in a follow-up. | `dag.ts:290-297`; `loop.ts:638` | Accepted as verified-closed, non-blocking | - -Documented deferred follow-ups — explicitly accepted as non-blocking residual items: - -| # | Item | Note | -|---|---|---| -| R4 | Deferred SettingsHook wiring pattern | goal.ts mirrors it via `serviceOption`; intentionally deferred | -| R5 | Gate-4 pre-existing marked skip | v2 projector disabled; pre-existing, unrelated to this diff | -| R6 | Gate-8 SDK gen diff uncommitted | intentionally uncommitted working-tree additions matching the intended set | -| R7 | GET /session/:id/goal 200-null vs SDK Goal typing | documented deferred follow-up | -| R8 | httpapi error_class field-level fixture | documented deferred follow-up | -| R9 | TUI sync reducer tests | documented deferred follow-up | -| R10 | GoalLoop e2e fixed sleeps | documented deferred follow-up | - -## 4. PASS reason - -All five round-2 HIGH findings are CLOSED with file:line evidence from review-final-2's closure verification, and the objective gate suite is persisted as 8/8 PASS (verdict PASS, verified by direct read of the gate result file — primary evidence, not hearsay). Only two new LOW-severity observations emerged (plus one transparency record); no new CRITICAL/HIGH on verified evidence. Documented deferred follow-ups remain explicitly non-blocking residual items. Bounded loop round 3 of max 2 replans: goal met, no loop warranted. - -**Next action**: finalize. No remediation wave required. diff --git a/.opencode/.dag-specs/review-parts-round3/verify-suite.md b/.opencode/.dag-specs/review-parts-round3/verify-suite.md deleted file mode 100644 index 4f6721b996..0000000000 --- a/.opencode/.dag-specs/review-parts-round3/verify-suite.md +++ /dev/null @@ -1 +0,0 @@ -{"verdict":"PASS","results":[{"gate":"1. typecheck core","command":"bun run typecheck (packages/core)","outcome":"PASS","detail":"tsgo --noEmit exited 0, no diagnostics"},{"gate":"2. typecheck opencode","command":"bun run typecheck (packages/opencode)","outcome":"PASS","detail":"tsgo --noEmit exited 0, no diagnostics"},{"gate":"3. opencode DAG suites","command":"bun test test/dag (packages/opencode)","outcome":"PASS","detail":"324 pass, 0 fail, 845 expect() calls across 26 files"},{"gate":"4. goal + dispatch suites","command":"bun test test/goal test/tool/goal-tool.test.ts test/session/prompt.test.ts (packages/opencode)","outcome":"PASS","detail":"143 pass, 0 fail, 1 skip across 7 files; skip is pre-existing marked test 'prompt emits v2 prompted and synthetic events (v2 projector disabled)'"},{"gate":"5. core suites","command":"bun test test/dag-core.test.ts test/dag-projector-drift.test.ts test/dag-store-summaries.test.ts test/plugin/command.test.ts (packages/core)","outcome":"PASS","detail":"101 pass, 0 fail, 493 expect() calls across 4 files"},{"gate":"6. migration check","command":"bun script/migration.ts --check (packages/core)","outcome":"PASS","detail":"EXIT=0; incremental: 'No schema changes, nothing to migrate'; full migration.sql generated cleanly"},{"gate":"7. HttpAPI contract","command":"bun run test:httpapi --fail-on-missing (packages/opencode)","outcome":"PASS","detail":"summary pass=226 fail=0 skip=0 missing=0 extra=0, EXIT=0"},{"gate":"8. SDK freshness","command":"bun run build (packages/sdk/js) + git diff -- packages/sdk/js/src/v2/gen","outcome":"PASS","detail":"Build EXIT=0. Diff is pure additions (69 insertions, 0 deletions, only sdk.gen.ts + types.gen.ts) and matches the intended set exactly: SessionGoalData/Errors/Responses types referencing pre-existing committed 'Goal' type, Session2.goal() method for GET /session/{sessionID}/goal, and DagNode.error_class?: string. No other drift."}],"anomalies":["Gate 4 contains 1 pre-existing marked skip (v2 projector disabled), not a failure","Gate 8 gen diff is entirely uncommitted working-tree additions, consistent with the stated interpretation (Goal route types + session.goal + error_class intentionally uncommitted)"]} diff --git a/.opencode/.dag-specs/review-parts/explore-build.md b/.opencode/.dag-specs/review-parts/explore-build.md deleted file mode 100644 index f4d5c582f9..0000000000 --- a/.opencode/.dag-specs/review-parts/explore-build.md +++ /dev/null @@ -1,110 +0,0 @@ -探索完成。所有目标文件已读取,关键声明已对运行时源码逐条核验。以下为探索报告。 - ---- - -## Hit Summary - -PR #167 的构建注入链、三级 scope 解析、命令注册、release 流水线在代码层面自洽:`typeof` 守卫(dev 无 define 时不抛 ReferenceError)、`seen` 去重、空 glob 均修复正确,prompt 的配置目录解析顺序与运行时(`flag.ts:63-64` + `global.ts:3,13` + xdg-basedir)**逐字一致**。主要问题集中在:**dag-flow.txt 描述"two scopes"与运行时三级不符**、**builtin 新功能零测试覆盖**、**README 死链/过时声明**、**prompt 锁语义两个缺口(父目录前置条件、陈旧锁无恢复)**。置信度:高(静态证据充分;仅 node 构建分发路径与 Windows xdg 行为留待 verify 波确认)。 - ---- - -## Key Symbols - -- `packages/opencode/script/generate.ts:44` `loadDagTemplatesData()` — 读 `DAG_TEMPLATES_DIR` env,`Bun.Glob("*.yaml")` 根级扫描,`file.replace(/\.ya?ml$/, "")` 取名字,`JSON.stringify(templates)` 输出(unset 时返回字符串 `"undefined"`);与 `loadModelsData()`(generate.ts:12-33)同构:同是"env 快照 → 字符串(JSON 或 `"undefined"`)→ 导出 → define 注入"模式 -- `packages/opencode/script/generate.ts:59` `dagTemplatesData` — 模块顶层 await 导出;`build.ts:16` `await import("./generate.ts")`,`build.ts:203` 注入 `OPENCODE_DAG_TEMPLATES: generated.dagTemplatesData`(裸文本替换:JSON 字面量或 `undefined` 关键字,均合法表达式) -- `packages/opencode/src/dag/workflows.ts:30` `declare const OPENCODE_DAG_TEMPLATES: Record | undefined` — 与 `packages/core/src/models-dev.ts:114,185-187` 的 `OPENCODE_MODELS_DEV` 守卫模式完全一致 -- `packages/opencode/src/dag/workflows.ts:51` `builtinTemplates()` — `typeof` 守卫(未绑定标识符在 typeof 下安全,不抛 ReferenceError — 修复正确) -- `packages/opencode/src/dag/workflows.ts:77` `resolve()` / `:102` `list()` / `:135` `builtinEntry()` / `:126-133` `isBuiltinPath`/`builtinName`(`BUILTIN_PREFIX = "builtin://"` 在 :58) -- `packages/opencode/src/tool/workflow.ts:359-370` — `readWorkflowSpec` 的 builtin 分支(`isBuiltinPath` → 查 map → `Bun.YAML.parse(content)`) -- `packages/opencode/src/tool/workflow.ts:395-399` `searchedScopes()` — 空库/未找到提示附加 "the release's builtin templates" -- `packages/core/src/plugin/command.ts:41-45` — `draft.update("dag-template-update", ...)` 注册(与 dag-flow 的 :37-40 同模式) -- `.github/workflows/release-fork.yml:66-97` `package-templates` job、`:163-175` 下载/解压/`DAG_TEMPLATES_DIR` 环境注入 - ---- - -## Call Relationships - -- **构建期**:`release-fork.yml:80-89`(打包 `dag-config/*.yaml`)→ `DAG_TEMPLATES_DIR` env(:175)→ `generate.ts:44` → `build.ts:16,203` define → 二进制常量 → `workflows.ts:51 builtinTemplates()` → `resolve()`(:86-88 兜底层)/ `list()`(:117-120)/ `tool/workflow.ts:360-364` -- **运行时名字解析**:`workflow(action:start)` → `tool/workflow.ts:408 resolveSpecPath`(isName 分支)→ `workflows.ts:77 resolve`(project → global → builtin)→ 返回 `builtin://name` 路径 → `:360` builtin 分支解析内容 -- **命令面**:`command.ts:41-45` 注册 `/dag-template-update`(draft.update + `.txt` import,:11);`/dag-flow` 同模式(:37-40);`command/index.ts:51` 有 `Default.DAG_FLOW` 常量但**无** `DAG_TEMPLATE_UPDATE` 条目(是否必需 → unverified) -- **release 流水线**:`package-templates`(仅 workflow_dispatch,:68)→ `build-cli`(needs :101 + 自身 `if: workflow_dispatch` :105)→ `release`(needs 两者 + `create_release` 门,:220-221);push 触发时三者全跳过、仅 `register` 跑 — **needs 链不因 push 中断** ✓ - ---- - -## 1. 完整构建期数据路径 - -`release-fork.yml:163-175`(download-artifact `dag-templates` → `tar -xzf` → `DAG_TEMPLATES_DIR=$GITHUB_WORKSPACE/dag-templates-src` 写入 GITHUB_ENV)→ `generate.ts:45` 读 env → `:51` `Bun.Glob("*.yaml").scan({cwd})`(仅根级,非递归)→ `:52` 名字去扩展名 → `:53` `Bun.file().text()` → `:56` `JSON.stringify(templates)`(值为 JSON 文本,esbuild define 裸粘贴为对象字面量;模板内引号/反斜杠由 stringify 正确转义,无注入面)→ `:59` 导出 → `build.ts:203` define → 编译进单文件二进制 → 运行时 `workflows.ts:51-56` 守卫后按名取用。**注**:`build-node.ts:23` 与 `packages/cli/script/build.ts:92` 只注入 `OPENCODE_MODELS_DEV`、未注入 `OPENCODE_DAG_TEMPLATES` → node 目标构建无 builtin(守卫优雅降级为两级);"air-gapped installs ship the curated templates"(workflows.ts:12-14)仅对 bun 单文件构建成立(UNVERIFIED:node 构建是否属用户分发路径)。 - -## 2. dev 下 DAG_TEMPLATES_DIR unset 的行为 - -- `generate.ts:46-49`:打日志并返回字符串 `"undefined"` → define 注入裸 `undefined` 关键字 → `workflows.ts:54` `typeof ... === "undefined"` → 返回 `{}`。 -- 源码 dev 运行(bun dev 无 define):标识符未绑定,`typeof` 对未声明标识符不抛错 → 同路径返回 `{}`。**ReferenceError 守卫修复正确**(typeof 是唯一安全探测方式)。 -- 后果链:`resolve` 返回 undefined("not found"提示不含 builtin)→ `list` 空 → `searchedScopes`(tool/workflow.ts:397)不附加 builtin 提及。**全部优雅降级** ✓ - -## 3. 命令注册模式对比 - -`command.ts:41-45`:`draft.update("dag-template-update", (command) => { command.template = DAG_TEMPLATE_UPDATE_PROMPT; command.description = DagTemplateUpdateDescription })` — 与 dag-flow(:37-40)逐行同构(import 于 :11)。唯一不对称:`command/index.ts:51` 的 `Default` 枚举只有 `DAG_FLOW`,无 `dag-template-update`(该枚举用途未确认 — unverified;若 TUI/命令面依赖它,新命令可能不完整)。 - -## 4. dag-template-update.txt 逐节语义(M1/M2/M3) - -- **配置目录(L16-24)**:`OPENCODE_CONFIG_DIR` env → xdg 平台目录(XDG_CONFIG_HOME,兜底 `~/.config/opencode`)。**与运行时逐字一致**:`flag.ts:63-64`(读 env)+ `global.ts:3,13`(xdg-basedir;macOS 默认 `~/.config/opencode`,非 Library/Application Support)✓ 已验证 -- **下载(L26-37)**:固定 `codeload.github.com/LeXwDeX/opencode-dag-config/zip/refs/heads/main` — 未 pin tag/commit(与 release clone 同为 HEAD,可复现性弱,LOW) -- **干跑分类(L39-49)**:NEW/UNCHANGED/UPDATE + local-only 保留 ✓ -- **合并 QA(L51-61)**:无 UPDATE 直合;有 UPDATE 时三选项(全覆盖先备份/全跳过/逐文件)+ 拒绝只加 NEW ✓ -- **备份(L62-65)**:`.yaml.bak-` 放原文件旁;备份失败 → **中止该文件覆盖并报告,绝不无备份覆盖** ✓ **M2 满足**。备份文件 extname 为 `.bak-*`,被 `list()` 的 EXTENSIONS 过滤(workflows.ts:109-110)不会污染库列表 ✓ -- **锁(L67-78)**:`mkdir .dag-update.lock` 原子判定(L72-74)、短暂等待重试数次(L75-76)、合并结束含失败时 `rmdir`(L77-78)✓ **M1 满足(存在+重试+清理)**。锁目录被 `list()` 的 `isFile()` 过滤(:108)✓ - - **缺口 A(M2/M3)**:锁在"下载前"创建(L70),但 `/workflows` 不存在时 `mkdir` 失败是 **ENOENT 而非 EEXIST**;提示词只在 L97(Failure handling)说"applying 前创建目录" — 锁步骤的父目录前置条件缺失,顺序歧义,agent 可能误判 ENOENT - - **缺口 B(M2/M3)**:陈旧锁无恢复路径 — agent 崩溃/被杀后 `.dag-update.lock` 永久残留,后续更新永远停在"another update is already running";无 mtime/age 检测、无强制覆盖或提示手工清除 -- **验证(L80-90)**:重读每个更新文件与归档副本**逐内容比对**(L85-86)+ `workflow(action:list)` 计数与变更名单(L87-89)+ 报告备份位置 ✓ **M3 满足(内容比较,非仅列表)**;并正确提示项目级 shadow 影响列表可见性 - - **缺口 C(LOW)**:内容比对不一致时无恢复动作(未提回滚备份/重试) -- **失败处理(L92-97)**:下载失败 verbatim 报告并停(L94-95)✓;解压失败报告并停(L96)✓;合并中途复制失败未明确覆盖(LOW);锁清理覆盖"包括失败" ✓ - -## 5. dag-flow.txt 与运行时不一致(确认存在) - -- **dag-flow.txt:13**:"Reference templates are installed in **two scopes** (project overrides global...)" — 运行时是**三级**(project → global → builtin,workflows.ts:9-14)。builtin 层在 dag-flow.txt 中**完全缺失**;对开箱即用(未跑 update、无 config repo)用户,curated 模板恰恰只存在于 builtin 层,提示词描述的 global 层是空的 -- **dag-flow.txt:14**:"global ... curated by the `opencode-dag-config` repo" — 只有跑过 `/dag-template-update` 才成立;curated 快照的实际载体是二进制内置层 -- **dag-flow.txt:17-20**:指名 4 个 saved workflow(design-decision-loop 等)— 这些是本次**删除**的提交模板(`git diff` 872 行删除);dev checkout 无 config repo 时这些名字解析不到,agent 只能走 :21 的 "no close match" 兜底。提示词假设模板常驻,与实际可用性脱节 -- **dag-flow.txt:16** "pick by name or path" — `list()` 对 builtin 项展示的路径是 `builtin://`(workflows.ts:58,131-133),该路径**不能**作为 spec_path 回传:`resolveSpecPath` 走 path 分支后在 extname 检查失败(tool/workflow.ts:416-418,报 "must be a .yaml or .yml file")— 误导性报错。应只按名选 - -## 6. 边界情况 - -- **空模板目录**:Glob 零匹配 → `{}` → `"{}"` → define 注入 `{}` → `typeof {}` 为 "object" → `builtinTemplates()` 返回 `{}` → 无 builtin 项 ✓(release 空 tar 时 `release-fork.yml:87` 打 warning,同样优雅) -- **非 yaml 文件**:`generate.ts:51` glob 过滤 ✓;release 打包 `dag-config/*.yaml`(release-fork.yml:83)✓;update prompt 指示取 `*.yaml` ✓ — 三层一致 -- **名称冲突**:`resolve` 顺序 project→global→builtin(:79-89)+ `list` seen-map(:104-120)优先级一致,listing 不会广告 resolve 选不中的项 ✓;`dag-template-update.txt:101-103` 正确提示 shadow -- **`.yml` 漂移(LOW)**:运行时 `EXTENSIONS` 含 `.yml`(workflows.ts:33),但 generate.ts glob、release 打包、update prompt 全只认 `.yaml` → config repo 若未来放 `.yml` 模板,对 binary 和打包不可见 -- **`generate.ts:52` 的 `\.ya?ml$` 中 `?` 是死代码**(glob 只匹配 `.yaml`)— 风格 nit - -## 其他发现(供 review/verify 波) - -1. **零新增测试(MEDIUM)**:`dag-workflows.test.ts` 无任何 builtin 测试(仅 project/global fixture);`workflow-tool.test.ts` 无 builtin 引用(:1039 只有 global scope env 重定向)。新功能面(三级优先级、builtinEntry、parseMeta 重构、typeof 守卫、searchedScopes)全部无测试。被删的 "repository's own workflow library" 测试是唯一真实 spec 校验 -2. **README 死链/过时(MEDIUM, docs)**:`README.md:253` 引用已删除的 `./.opencode/workflows/change-review.yaml`;`README.zh.md:27` "仓库已经附带三类强约束参考图"(已不成立)、`:66-67` 两 scope 表缺 builtin 层 -3. **`Entry.content` 死字段(LOW)**:`builtinEntry`(workflows.ts:135-137)设置,但 `readWorkflowSpec` 重新查 `builtinTemplates()`(tool/workflow.ts:360-364),全仓库无消费方 -4. **release-fork.yml**:job 图静态分析通过(push 触发不断链、`download-artifact` merge-multiple 将 `dag-templates.tar.gz` 带入 release 资产 — 与头部注释 :24-25 一致,属意图);config repo 未 pin tag(LOW);`tar -xzf` 在 windows-latest runner 可用(bsdtar) -5. **已修复项确认**:ReferenceError 守卫 ✓(typeof 语义安全)、Entry 去重 ✓(seen-map)、M4 空 glob ✓(Array.fromAsync 空集安全)、重复下载删除 ✓(最终 diff 中 build-cli 仅一个下载 step) - -## Related Test Files - -- `packages/opencode/test/dag/dag-workflows.test.ts` — resolve/list 仅 project/global;**builtin 无覆盖**(移除的 library 测试是唯一真实 spec 校验) -- `packages/opencode/test/dag/workflow-tool.test.ts:1039` — global scope 经 OPENCODE_CONFIG_DIR 重定向的集成测试(可作为 builtin 测试锚点) -- `packages/opencode/test/dag/dag-config.test.ts` — config dir env 重定向模式参考 - -## output_variables - -- targets: - - `loadDagTemplatesData@packages/opencode/script/generate.ts:44` - - `dagTemplatesData@packages/opencode/script/generate.ts:59` - - `builtinTemplates@packages/opencode/src/dag/workflows.ts:51` - - `resolve@packages/opencode/src/dag/workflows.ts:77` - - `list@packages/opencode/src/dag/workflows.ts:102` - - `builtinEntry@packages/opencode/src/dag/workflows.ts:135` - - `readWorkflowSpec@packages/opencode/src/tool/workflow.ts:350` - - `searchedScopes@packages/opencode/src/tool/workflow.ts:395` - - `command.ts:41` `/dag-template-update` 注册 - - `dag-template-update.txt`(L16-103 全文件) - - `dag-flow.txt:13-21` - - `release-fork.yml:66-97,163-175` -- impacted_processes: [WorkflowLibrary 名字解析(resolve/list/start)、/dag-template-update 命令、release 流水线模板打包与注入] -- test_anchors: [dag-workflows.test.ts, workflow-tool.test.ts:1039, dag-config.test.ts] -- ast_available: true(codebase-memory-mcp 已索引本 repo,head 2ee59d874;本次探索以 git diff 为准,未依赖图查询) - -**留给 verify 波的 unverified_claims**:① `build-node.ts`(target: node, src/node.ts)与 `packages/cli/script/build.ts` 产物是否为用户分发路径(决定缺 define 的严重性);② `command/index.ts:51` Default 枚举是否需要 dag-template-update 条目;③ xdg-basedir 在 Windows 的实际回退路径(prompt 仅声明 macOS/Linux);④ 本 repo 8 个未跟踪模板与 builtin 名是否冲突(本地 shadow);⑤ `bun test dag-workflows/workflow-tool` 实测通过性。 \ No newline at end of file diff --git a/.opencode/.dag-specs/review-parts/explore-ci.md b/.opencode/.dag-specs/review-parts/explore-ci.md deleted file mode 100644 index e440defda7..0000000000 --- a/.opencode/.dag-specs/review-parts/explore-ci.md +++ /dev/null @@ -1,117 +0,0 @@ -All exploration complete. Here's the report. - ---- - -## Hit Summary - -PR #167 的 4 个 commit(676e0463e → 2ee59d874)将 DAG 参考模板从仓库内 `.opencode/workflows/` 抽离到独立配置仓库,新增 builtin 三级 scope(project > global > builtin)编译进二进制,新增 `/dag-template-update` 命令,并扩展 release 流水线打包模板资产。release-fork.yml 的 job 图、env 注入链、空 glob 守卫均已静态验证;builtin scope 在测试环境中**完全无覆盖**(无注入钩子),Windows 构建的 DAG_TEMPLATES_DIR 路径转换存在未决风险点。信心:高(源码侧),中(Windows runner 行为需 verify 波确认)。 - ---- - -## 1. Job Graph(release-fork.yml) - -``` -push(main/dev, paths=release-fork.yml) ──► register (if: push, no-op) -workflow_dispatch ──► package-templates (if: workflow_dispatch, ubuntu, contents: read) - │ - ├──needs──► build-cli (if: workflow_dispatch, 3-OS matrix ×3) - │ └─ 每步再叠加 if: platforms=='' || contains(platforms, matrix.name) - └──needs──► release (if: inputs.create_release) - └─ needs: [build-cli, package-templates] -``` - -- **if 条件**:`release-fork.yml:68` package-templates、`:105` build-cli 均门控 `github.event_name == 'workflow_dispatch'`(push 注册模式下全 skip,skip→needs 视为 success,不会级联失败);`:221` release 门控 `inputs.create_release`(push 时 inputs 为空 → falsy → skip)。`:271` register 门控 `push`。 -- **平台过滤在 step 级**(`:123/129/143/164/171/178/187/191/210` 每步重复 `if: inputs.platforms == '' || contains(...)`),job 级无法引用 matrix —— 注释已说明(`:102-104`)。selected-platforms 时被过滤的 matrix job 仍会跑但所有 step skip,不上传 artifact。 - -## 2. Env 注入链(验证通过) - -``` -package-templates: dist/*.yaml → tar -czf dag-templates.tar.gz → upload-artifact name=dag-templates (retention 7d) - build-cli (needs package-templates): - step "Download Templates Artifact" (:163-168) → dag-templates-artifact/dag-templates.tar.gz - step "Extract Templates" (:170-175): tar -xzf -C dag-templates-src - → echo "DAG_TEMPLATES_DIR=$GITHUB_WORKSPACE/dag-templates-src" >> $GITHUB_ENV - step "Build CLI" (:177-182): env 块只有 OPENCODE_CHANNEL/OPENCODE_VERSION - → DAG_TEMPLATES_DIR 经 GITHUB_ENV 自动继承,无需显式 env: ✓ -``` - -build.ts:16 `await import("./generate.ts")` → generate.ts:44-60 读 `process.env.DAG_TEMPLATES_DIR` → `Bun.Glob("*.yaml").scan` → `JSON.stringify(templates)` → build.ts:203 `define: OPENCODE_DAG_TEMPLATES: generated.dagTemplatesData`。`DAG_TEMPLATES_DIR` 未设置时 generate.ts 返回字符串 `"undefined"`(generate.ts:52),经 esbuild/Bun define 原样注入为 `undefined` 关键字 → `typeof` 守卫命中 → 空 map。此 round-trip 与 `OPENCODE_MODELS_DEV`(generate.ts:33,models-dev.ts:185-187 同款 typeof+Object.keys 守卫)完全同构,是已验证的生产模式 ✓。 - -## 3. Empty-Glob 行为(M4 修复确认正确) - -- `release-fork.yml:82-90`:`shopt -s nullglob` + 数组长度守卫,空 glob → `::warning::No templates found...` + **不执行 cp** → `tar -czf` 打空目录(`mkdir -p dist` 先于判断,tar 空目录合法)→ **warning + 空 archive,不失败** ✓。修复前的 bug 版本(2ee59d874 之前)是 `cp "${files[@]}" dist/` 无条件执行,空数组时 `cp dist/` 报 missing operand 使 step 失败。 -- 空 archive → generate.ts glob 0 个文件 → `{}` → builtin scope 为空,二进制静默降级为无 builtin ✓。 - -## 4. Release 资产流 + --target 语义 - -- release job(`:227-231`)`download-artifact@v4 + merge-multiple: true` 拉取**本 run 全部 4 个 artifact**(opencode-{linux,macos,windows} + dag-templates),无文件名冲突。 -- `SHA256SUMS`(`:243-248`)glob `*` 覆盖 4 个二进制 + dag-templates.tar.gz;`shasum || sha256sum` 双 fallback。 -- `gh release create ... --target "${{ github.sha }}"`(`:264`)→ 手动触发时 github.sha = 触发瞬间分支 HEAD commit,tag 精确锚定该 commit;`--prerelease` 当 `github.ref_name != "main"`(`:258-260`)。上传 `release-assets/*` 含 SHA256SUMS 本身 ✓。 -- **风险点**:build-cli 的 checkout(`:124-126`)未指定 `ref: ${{ github.sha }}`,workflow_dispatch 下默认检出分支 tip —— 若构建期间分支有新 push,二进制可能来自更新的 commit 而 release tag 锚定旧 sha(低概率、构建-标签不一致)。release job 自身 checkout 同理(仅作 gh 上下文,无害)。 -- **Windows 路径风险(候选 HIGH)**:models.dev step 对 `$RUNNER_TEMP` 显式做 `cygpath -m` 转换(`:145-147`),而 Extract Templates 的 `$GITHUB_WORKSPACE/dag-templates-src`(`:175`)**无任何转换**。Windows runner 的 bash 下 GITHUB_WORKSPACE 为 POSIX 形(`/d/a/...`),写入 GITHUB_ENV 后原生 Bun 进程读取该路径可能无法解析 → glob 0 模板 → **Windows 发布版静默丢失 builtin 模板**。作者在同一文件里已证明知道此坑(cygpath 行),此处遗漏高度可疑。无法本地验证 Bun@Windows 路径处理 → 列入 unverified,交 verify 波。 - -## 5. Test Coverage Inventory - -**dag-workflows.test.ts(163 行,14 个测试,全部通过 diff 确认保留)**: - -| describe | 数量 | 覆盖 | -|---|---|---| -| `isName` | 3 | 裸标识符 true;path 形/扩展名/`.`/`..`/空串 false;控制字符(NUL、换行)false | -| `resolve` | 7 | project 命中;global 回退;project 遮蔽 global;`.yml` 扩展;同 scope `.yaml` 优先于 `.yml`;未知名 undefined + searchPaths 顺序断言;**不可解析 spec 仍返回 entry**(title/nodes undefined) | -| `list` | 4 | 双 scope 皆空 → [];合并排序 + 遮蔽(project 优先);忽略非 spec 文件与目录;无 title 时 title undefined、nodes=0 | - -**删除的测试**(dag-workflows.test.ts 旧 165-187 行):`change-review as valid start spec` —— 验证 `resolve("change-review", repoRoot)` 命中 project scope、`StartSpec` decode 通过、每个 `prompt_template.id` 存在于 `.opencode/dag-prompts/`、`depends_on` 引用有效。**删除一致性确认**:98e4c0624 删除了 4 个 tracked 模板(change-review.yaml / deep-review-dag-module.yaml / design-decision-loop.yaml / parallel-development-loop.yaml),测试会因 `entry!.path` 直接抛错而必挂 —— 删除是必要条件而非清理。但代价:模板间的 `depends_on`/prompt-template 引用完整性校验**从此没有任何自动化守卫**(配置仓库在 repo 外,无 CI 挂钩)。 - -**builtin scope 覆盖:无。** `builtinTemplates()`(workflows.ts:51-55)在测试环境 typeof 守卫返回 `{}`,`declare const` 无任何注入钩子(不像 config 可用 OPENCODE_CONFIG_DIR 重定向)。resolve 的 builtin fallback、list 的 builtin 合并/去重、`isBuiltinPath`/`builtinName`、tool/workflow.ts:360-370 的 builtin 读谱分支、`searchedScopes` 提示 —— 全部零测试。要测只能通过真实构建注入,测试基建缺失。 - -**workflow-tool.test.ts**(未改动文件,spot-check):saved-workflows 段 6 个测试覆盖 tool 级 list/resolve 行为 —— start-by-name(project、global 均无外部目录权限询问)、未知名错误信息含两个搜索目录、list 双 scope 遮蔽输出(`shared [project] — project-shared title`)、list 空库提示。schema 负例段(:291-331)覆盖 action/operation 枚举。**builtin 相关消息分支(searchedScopes 拼接)同样零覆盖。** - -**dag-workflow-lock.test.ts**(1 测试,未改动):mock DagStore.getWorkflow 内 25ms sleep + activeReads 计数,unbounded 并发 2 次 `dag.extend("wf1")`,断言 `maxActiveReads === 1` —— 验证 extend 同 workflow 串行化锁语义。与本 PR 无交集,属上下文佐证。 - -## 6. CI 回归风险 - -- **现有 CI(ci-typecheck/ci-test)零改动**;本 PR 只动 release-fork.yml,push 到 main/dev 仅触发 `register`(path-filtered),无新负担。 -- 唯一共享面:`setup-bun` macOS cache saver(`:137`)—— 未改动,既有行为。 -- job 级 skip 语义无级联失败风险(skip→needs success→下游同条件 skip)。 -- **风险清单**:① Windows DAG_TEMPLATES_DIR 路径转换缺失(静默空 builtin,候选 HIGH);② build-cli checkout 未 pin `github.sha` vs release `--target github.sha` 竞态(低概率 MEDIUM-LOW);③ 配置仓库为空/不可达时 package-templates 行为(空=降级 ✓,克隆失败=job 失败 fail-loud,可接受);④ `shasum -a 256 *` 在 release-assets 恒非空(dag-templates 必然存在)前提下安全。 - -## 其他值得 reviewer 关注的证据点 - -- `parseMeta` async + `.catch(() => undefined)`(workflows.ts:155-158)修复了旧 `describeContent` 同步 `Bun.YAML.parse` 对 malformed builtin 抛异常的路径 —— 现在 resolve/list 容忍、start 时 fail-loud(tool/workflow.ts:364-368 `Effect.try`),与文件路径行为一致 ✓。 -- list 去重(workflows.ts:117-120 `seen.set` + `if (seen.has(name)) continue`)保证 builtin 被 project/global 遮蔽 ✓,与 resolve 优先级一致。 -- `/dag-template-update`(103 行 prompt):`mkdir` 原子锁 + 失败回退、备份失败即中止、内容级 verify、local-only 保留 —— 全部是 prompt 级契约,**无代码强制**(该命令完全由 agent 依 prompt 执行),属设计权衡而非 bug。 -- dag-flow.txt 已更新为库引用(saved workflow 名字)+ 移除仓库内路径,与模板迁移一致 ✓。 - ---- - -## output_variables - -- targets: - - `package-templates job@.github/workflows/release-fork.yml:66-97` - - `build-cli job@.github/workflows/release-fork.yml:99-216` - - `release job@.github/workflows/release-fork.yml:218-266` - - `builtinTemplates@packages/opencode/src/dag/workflows.ts:51-55` - - `resolve@packages/opencode/src/dag/workflows.ts:73-89` - - `list@packages/opencode/src/dag/workflows.ts:100-122` - - `isBuiltinPath/builtinName/builtinEntry/parseMeta@packages/opencode/src/dag/workflows.ts:125-158` - - `loadDagTemplatesData@packages/opencode/script/generate.ts:39-60` - - `readWorkflowSpec@packages/opencode/src/tool/workflow.ts:350-391` - - `searchedScopes@packages/opencode/src/tool/workflow.ts:393-398` - - `DagTemplateUpdateDescription + command registration@packages/core/src/plugin/command.ts:15,42-44` - - `dag-template-update.txt@packages/core/src/plugin/command/dag-template-update.txt:1-103` - - `dag-flow.txt@packages/core/src/plugin/command/dag-flow.txt` - - `OPENCODE_DAG_TEMPLATES define@packages/opencode/script/build.ts:203` - - `dag-workflows.test.ts@packages/opencode/test/dag/dag-workflows.test.ts`(14 测试 + 1 删除) - - `dag-workflow-lock.test.ts@packages/opencode/test/dag/dag-workflow-lock.test.ts:8-58` - - `workflow-tool.test.ts saved-workflows@packages/opencode/test/dag/workflow-tool.test.ts:1038-1195` -- impacted_processes: [ReleaseForkPipeline, DagWorkflowResolution, DagTemplateUpdate] -- test_anchors: - - `packages/opencode/test/dag/dag-workflows.test.ts::isName/resolve/list` - - `packages/opencode/test/dag/workflow-tool.test.ts::workflow tool saved workflows` - - `packages/opencode/test/dag/dag-workflow-lock.test.ts::Dag.Service workflow lock` -- ast_available: true - -**unverified_claims(交 verify 波)**: -1. Bun@Windows 对 POSIX 形 `DAG_TEMPLATES_DIR`(`/d/a/...`)路径的解析行为 —— 决定 Windows 发布版是否静默丢失 builtin 模板(release-fork.yml:175 vs :145-147 的 cygpath 不对称)。 -2. GitHub Actions windows runner bash 下 `$GITHUB_WORKSPACE` 的实际形态(/d/a/... vs D:\a\...)。 -3. `"undefined"` 字符串经 Bun.build define 注入为 `undefined` 关键字的假设(models 同款已在生产证明,风险低)。 \ No newline at end of file diff --git a/.opencode/.dag-specs/review-parts/explore-runtime.md b/.opencode/.dag-specs/review-parts/explore-runtime.md deleted file mode 100644 index fed6cdc080..0000000000 --- a/.opencode/.dag-specs/review-parts/explore-runtime.md +++ /dev/null @@ -1,125 +0,0 @@ -## Hit Summary - -builtin 三级 scope 解析面已完整映射:`workflows.ts` 的解析/列举逻辑(project > global > builtin,first-match-wins)与 `tool/workflow.ts` 的消费路径(`readWorkflowSpec` builtin 分支、`searchedScopes` 提示)实现正确且自洽;esbuild define 注入机制已实测验证可用(JSON 对象值被正确包裹)。主要风险集中在**零测试覆盖**:builtin scope 无任何测试(`rg builtin` 在测试目录零命中),且被删除的仓库自检测试(旧 spec 解码校验)没有替代,导致 config repo 模板完全脱离本仓库测试套件。另有 2 个值得上报的边界问题(`list()` 展示的 `builtin://` 路径不可作为 `spec_path` 回填;空库消息中 builtin 提及是不可达死代码)。置信度:代码路径理解高,release 流水线实际产物中高(静态审查)。 - -## Key Symbols - -- `packages/opencode/src/dag/workflows.ts:35` `export type Scope = "project" | "global" | "builtin"` — 新增第三级 scope -- `packages/opencode/src/dag/workflows.ts:50-56` `builtinTemplates()` — `typeof OPENCODE_DAG_TEMPLATES === "undefined"` 守卫,undefined(dev/test)→ `{}` -- `packages/opencode/src/dag/workflows.ts:77-90` `resolve()` — project/global 循环后 builtin 兜底 -- `packages/opencode/src/dag/workflows.ts:102-123` `list()` — seen Map 去重,builtin 合并,`localeCompare` 排序 -- `packages/opencode/src/dag/workflows.ts:126-133` `isBuiltinPath()` / `builtinName()` — `builtin://` 前缀解析 -- `packages/opencode/src/dag/workflows.ts:158-167` `parseMeta()` — YAML 解析容错 + title/nodes 提取 -- `packages/opencode/src/tool/workflow.ts:359-370` `readWorkflowSpec()` builtin 分支 — 内容缺失报错、YAML 解析失败路径 -- `packages/opencode/src/tool/workflow.ts:395-399` `searchedScopes()` — 目录 + 条件性 builtin 提及 -- `packages/opencode/script/generate.ts:36-59` `loadDagTemplatesData()` / `dagTemplatesData` — 构建期从 `DAG_TEMPLATES_DIR` 读快照 -- `packages/opencode/script/build.ts:203` `OPENCODE_DAG_TEMPLATES: generated.dagTemplatesData` — define 注入点 -- `packages/core/src/models-dev.ts:114,185-187` — `OPENCODE_MODELS_DEV` 既有同构先例 - -## Call Relationships - -``` -tool/workflow.ts resolveSpecPath (L401) - └─ isName(specPath)? (L408) - ├─ resolve(name, dir) → workflows.ts:77 → scopes() (project L144 → global L145) → builtinTemplates() (L86-88) - │ └─ builtinEntry() (L135-137) → parseMeta() (L158) - └─ path branch (L415-424) — 永不产生 builtin:// 路径(实测:// 被 path 规范化折叠) -readWorkflowSpec (L350) - ├─ isBuiltinPath(filepath)? (L360) → builtinTemplates()[builtinName()] → YAML.parse (L365-368) - │ └─ 缺失 → fail "Workflow spec not found: builtin://name" (L363) - └─ 文件分支 (L372-389) — 含 1MB size 检查 (L376),builtin 分支绕过 -build 链: release-fork.yml (clone opencode-dag-config → tar *.yaml → artifact) - → build-cli (下载解包 → DAG_TEMPLATES_DIR env, L160-175) - → generate.ts:45 (env 读取) → generate.ts:59 (dagTemplatesData) - → build.ts:203 (define) → workflows.ts:30 (declare const) → workflows.ts:54 (typeof 守卫) -``` - -## Execution Flows Involved - -- `Flow: BuiltinResolution` — bare name → `resolve()` 三级查找 → builtin 兜底 → `readWorkflowSpec` builtin 分支 → YAML.parse → start/extend/replan 解码 (workflows.ts:86-88 → workflow.ts:360-369) -- `Flow: BuiltinInjection` — 构建期:`DAG_TEMPLATES_DIR` env → glob `*.yaml` → `JSON.stringify` → esbuild define → 编译进二进制;dev/test 下返回字面量 `"undefined"` → 守卫生效返回 `{}`(实测验证对象注入路径:`{"a":"b"}` 被正确编译为对象字面量) -- `Flow: LibraryListing` — `list()` 三级合并去重排序 → `[name] [scope] — title (N nodes)\n path` 输出(workflow.ts:149-162),builtin 条目路径显示为 `builtin://name` - -## Related Test Files - -- `packages/opencode/test/dag/dag-workflows.test.ts` — **无 builtin 覆盖**(isName/resolve 投影/全局/list 测试均不触达 builtin 分支);已删除旧测试"the repository's own workflow library"(旧 L163-189,校验 change-review 可解码) -- `packages/opencode/test/dag/workflow-tool.test.ts:1038-1160` — saved workflows 段:bare name 解析、全局 scope 无外目录提示、not-found 消息(`toContain` 子串断言,容忍 builtin 提及追加)、list 双 scope 投影;同样无 builtin 用例 - ---- - -## 1. 解析顺序与遮蔽语义(含行号) - -- **顺序**:`scopes()`(workflows.ts:142-147)= project(`.opencode/workflows`,L144)→ global(`Flag.OPENCODE_CONFIG_DIR ?? Global.Path.config` + workflows,L145);`resolve()` 先遍历两 scope 的 `.yaml`/`.yml`(L79-85,EXTENSIONS 顺序 L33 保证同 scope 内 `.yaml` 优先),builtin 仅在前两者均未命中时兜底(L86-88)。头注释 L9-14 一致。 -- **list() 遮蔽一致性**:`seen` Map key=name、first-wins(L104),builtin 只在 `!seen.has(name)` 时并入(L117-120),与 resolve() 优先级严格一致——不会列出 resolve() 选不中的文件。 -- **验证状态**:project>global 遮蔽有测试(dag-workflows.test.ts:88-94、workflow-tool.test.ts 双 scope 用例);**project/global 遮蔽 builtin 零测试**。 - -## 2. builtin 数据流 - -- **声明**:`declare const OPENCODE_DAG_TEMPLATES`(workflows.ts:30),仅构建期存在;`builtinTemplates()` 用 `typeof` 守卫(L54)防 ReferenceError——dev/test 裸跑返回 `{}`。 -- **注入链**:`generate.ts:45` 读 `DAG_TEMPLATES_DIR` env → 无 env 返回字符串 `"undefined"`(L47-48,define 后成为字面量 `undefined` → 守卫触发);有 env 则 `new Bun.Glob("*.yaml").scan({cwd})`(L49)收集 → name 去扩展名(L50)→ `JSON.stringify`(L57)→ `build.ts:203` 注入 define。**已实测**:Bun build 对 `{"a":"b"}` 值正确编译为对象字面量(非 block statement),typeof 返回 "object"。 -- **release 侧**:`release-fork.yml` package-templates job(clone `LeXwDeX/opencode-dag-config` → 仅打包根目录 `*.yaml` → tar artifact,L68-96)→ build-cli job 下载解包并写入 `DAG_TEMPLATES_DIR` env(L160-175)→ 与 `--compile` 定义值配套。 -- **与先例同构**:`models-dev.ts:114,185-187` 的 `OPENCODE_MODELS_DEV` 完全相同模式(含 length 检查),生产已验证——机制可信。 -- **dev/test 差异**:无 env → `"undefined"` 字面量 → 空 map → builtin scope 静默关闭,所有现有测试实际都跑在守卫分支上(隐式覆盖了守卫本身)。 - -## 3. readWorkflowSpec builtin 路径 - -- **解析**:`isBuiltinPath`(workflows.ts:126-128,`startsWith("builtin://")`)+ `builtinName`(L131-133,slice 前缀)。 -- **内容缺失**:`content === undefined` → `Effect.fail("Workflow spec not found: builtin://name")`(workflow.ts:362-364)——与文件分支的 not-found 同文案风格。 -- **YAML 失败**:`Effect.try` 包裹 `Bun.YAML.parse`(L365-368)→ `workflowSpecParseError`(L428-430)→ `Invalid workflow YAML builtin://name: `,与文件分支(L385-388)同形。 -- **可达性**:builtin 分支只能由 `resolve()` 返回的路径触发。**实测**:用户直接传 `builtin://code-review.yaml` 作 spec_path 时,`path.resolve` 把 `//` 折叠成 `/`(POSIX `/abs/dir/builtin:/code-review.yaml`,win32 `C:\dir\builtin:\code-review.yaml`),`isBuiltinPath` 恒 false → 落入路径分支报"must be .yaml/.yml"或 not-found——**不可伪造,也不可达**。 -- **绕过项**:builtin 内容跳过 1MB size 检查(L376-380 仅文件分支)与外目录权限提示——内容为构建期策展,可接受,但属隐式信任面。 - -## 4. list() 行为 - -- **去重**:seen Map 三级 first-wins(L104-120);**排序**:`a.name.localeCompare(b.name)`(L121,unchanged)。 -- **parseMeta 容错**(L158-167):YAML parse 失败 catch → `{}`(L159-161);title 需顶层字符串(L164);nodes = `config.nodes` 数组长度(L165-166);畸形 spec 照常列出(测试 dag-workflows.test.ts:117-123、156-161 覆盖文件侧;builtin 侧同函数未测)。 -- **describe() 重构**(L149-153):`.text().catch(() => undefined)` 拆出与 pre-PR 行为等价(pre-PR 的 catch 在 `.then(YAML.parse)` 之后,同样吞掉 unreadable 与 parse 错误)——**非行为修复,纯为共享 parseMeta**。 -- **builtin 条目**:scope="builtin"、path=`builtin://name`、携带 content(L135-137)。`Entry.content` **当前无任何消费者**(start 路径重查 `builtinTemplates()[name]`,list 输出不含 content)——死字段,为未来 TUI/预览预留。 - -## 5. searchedScopes() 与消息 - -- `searchedScopes()`(workflow.ts:395-399):`searchPaths()` 两目录 + builtin map 非空时追加 `"the release's builtin templates"`。 -- **not-found 消息**(L411-413):`Saved workflow not found: "x". Searched [ and builtin]...` —— builtin 提及仅在 map 非空时出现,语义正确。 -- **空库消息**(L142-147):`The workflow library is empty. Searched ...` —— **builtin 提及不可达死代码**:map 非空时 list() 必然包含 builtin 条目、`entries.length === 0` 不可能成立,而 map 空时 searchedScopes 不加 builtin 文案。 -- **dag-flow.txt 与内置 scope 不一致**(上下文):prompt 只描述两 scope(project/global),未提 builtin——agent 若回填 list() 展示的 `builtin://` 路径会失败(见 §3),但 prompt 引导使用裸名,实际可用。 - -## 6. 边界/边缘情况 - -| 边界 | 行为 | 行号 | 状态 | -|---|---|---|---| -| 空 builtin map(dev/test) | 守卫返回 `{}`,resolve 落 undefined,消息只提目录 | workflows.ts:54 | ✓ 隐式覆盖 | -| project/global 遮蔽 builtin | resolve L79-88 / list L117-119 一致 first-wins | — | ✓ 逻辑正确,无测试 | -| 畸形 builtin 内容 | list 仍列出(parseMeta catch);start 报清晰 parse error | workflows.ts:159-161 / workflow.ts:365-368 | ✓ | -| `builtin://` 伪造 | 路径规范化折叠 `//`,不可达 builtin 分支(实测) | workflow.ts:360 | ✓ | -| list 展示路径不可回填 | `builtin://name` 含 `/` → isName=false → 路径分支报错 | workflow.ts:408-418 | ⚠️ UX 缺陷,非崩溃 | -| 1MB size 检查绕过 | builtin 内容不检查 | workflow.ts:376 vs 359-370 | ⚠️ 低风险(构建期信任) | -| `.yml`/嵌套目录静默丢弃 | glob 仅 `*.yaml` 顶层(generate.ts:49;release-fork L73-76 同);L50 正则容忍 `.yml` 是死代码 | — | ⚠️ 依赖 config repo 布局 | -| `Entry.content` 无消费者 | 死字段 | workflows.ts:42-43 | ℹ️ 预留面 | -| **builtin 零测试覆盖** | 全新增面无测试;旧仓库自检测试删除后 config repo 模板完全脱离本仓库 CI 校验 | 测试文件 diff | 🔴 测试缺口 | - -## unverified_claims(供 verify 波核对) - -- **U1**:release 产出的二进制实际包含模板——仅静态验证 define 机制 + CI job 图;`--version` smoke test(build.ts)不触达 builtin 解析,无法本地跑通 release 构建(config repo 私有/外网)。 -- **U2**:opencode-dag-config 仓库根目录只含 `*.yaml`(顶层)——若含 `.yml` 或子目录会被 glob/打包脚本静默丢弃。 -- **U3**:`/dag-template-update` 写入目录与运行时读取目录一致(`OPENCODE_CONFIG_DIR` → 平台默认)——命令是 prompt 无代码路径,运行时用 `Flag.OPENCODE_CONFIG_DIR ?? Global.Path.config`(workflows.ts:145)。 -- **U4**:`build-cli` job 仅 `workflow_dispatch` 触发(release-fork.yml:106),push 触发只注册不构建——builtin 注入只存在于手动 release 产物,非 dev 构建。属设计意图,非缺陷。 - -## output_variables - -- targets: - - `builtinTemplates@packages/opencode/src/dag/workflows.ts:51` - - `resolve@packages/opencode/src/dag/workflows.ts:77` - - `list@packages/opencode/src/dag/workflows.ts:102` - - `isBuiltinPath@packages/opencode/src/dag/workflows.ts:126` - - `builtinName@packages/opencode/src/dag/workflows.ts:131` - - `parseMeta@packages/opencode/src/dag/workflows.ts:158` - - `readWorkflowSpec@packages/opencode/src/tool/workflow.ts:350`(builtin 分支 359-370) - - `searchedScopes@packages/opencode/src/tool/workflow.ts:395` - - `loadDagTemplatesData@packages/opencode/script/generate.ts:36` -- impacted_processes: [BuiltinResolution, BuiltinInjection, LibraryListing] -- test_anchors: - - [dag-workflows.test.ts::DagWorkflows.resolve(无 builtin 用例)] - - [dag-workflows.test.ts::DagWorkflows.list(无 builtin 用例)] - - [workflow-tool.test.ts::workflow tool saved workflows@1038(无 builtin 用例)] - - 已删除:dag-workflows.test.ts 旧 "repository's own workflow library"(spec 解码校验,无替代) -- ast_available: true(全文件直接精读 + git diff 比对 + 2 项运行时实测验证,未依赖图索引) \ No newline at end of file diff --git a/.opencode/.dag-specs/review-parts/review-architecture.md b/.opencode/.dag-specs/review-parts/review-architecture.md deleted file mode 100644 index 4a2dcae38c..0000000000 --- a/.opencode/.dag-specs/review-parts/review-architecture.md +++ /dev/null @@ -1,75 +0,0 @@ -# PR #167 架构审查报告(只读) - -审查基准:`git diff origin/dev...HEAD`(4 commits,13 文件:+286/−926)。已实测运行 `bun test dag-workflows/workflow-tool`(40 pass / 0 fail)作为测试证据。 - -## findings - -**1. HIGH — `/dag-template-update` 运行时不可达:仅注册了 core plugin draft,未注册进应用 Command 服务** - -命令只加在 `packages/core/src/plugin/command.ts:42-45` 的 `draft.update`,该 draft 落入 core v2 Command catalog(`packages/core/src/command.ts`),消费方只有 core plugin host(`packages/core/src/plugin/host.ts:95-97`),**不接入 TUI/会话命令面**。TUI 命令列表(`sync.tsx` → `GET /command`,`packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts:76-77`)与 slash 命令执行(`session/prompt.ts:1867-1871`,`commands.get` 未命中 → "Command not found")都走 `packages/opencode/src/command/index.ts` 的 Command 服务。该服务只注册 `Default` 枚举内的命令(`index.ts:48-54`、dag-flow 注册于 `:94-100`)。dag-flow 先例(commit e14fdcdec)是**双注册**:core plugin draft + 应用 Default 枚举 + `command.test.ts:46-52` 注册断言;本 PR 只做了前者。后果:用户输 `/dag-template-update` 直接 "Command not found",命令功能整体死代码。 - -推荐:仿 DAG_FLOW 在 `packages/opencode/src/command/index.ts` 的 `Default` + commands map 补注册,并在 `packages/opencode/test/command/command.test.ts` 加断言。 - -**2. MEDIUM — builtin scope 零测试覆盖,且删除的仓库自检测试无替代** - -`packages/opencode/test/dag/dag-workflows.test.ts` 删除了唯一的真实 spec 校验(旧 L163-189:StartSpec 解码 + prompt_template.id/depends_on 引用完整性),全部保留测试(isName/resolve/list 14 个)不触达 builtin 分支;`workflow-tool.test.ts` 亦无 builtin 用例。新增面(`workflows.ts:51-56,86-88,117-120`、`workflow.ts:361,395-397`)零覆盖。`declare const` 无注入钩子,测试基建缺失是根因。结果:config repo 模板完全脱离本仓库 CI,模板间引用错误不再有任何自动守卫。 - -推荐:为 `builtinTemplates()` 提供可注入测试源(如可选参数/全局 hook),覆盖 resolve builtin 兜底、project/global 遮蔽 builtin、list 合并去重、`isBuiltinPath`/`builtinName`、searchedScopes 拼接;`generate.ts:44-57` 的 glob→map 逻辑可抽纯函数用临时 `DAG_TEMPLATES_DIR` 单测。 - -**3. MEDIUM — 文档与运行时三级 scope 不一致,dev 环境 /dag-flow 首步即失败** - -- `dag-flow.txt:13` 仍写 "two scopes (project overrides global)"——运行时是三级(`workflows.ts:9-14`);`:14` 称 global "curated by the opencode-dag-config repo",实际 curated 快照的载体是 builtin 层(global 需先跑 update 才存在)。 -- `workflow.md:84-85,483-484`(追加到每次 dag-flow 调用的 WorkflowFactsContent)同样只描述两 scope。 -- `dag-flow.txt:17-20` 点名 4 个 saved workflow(design-decision-loop 等)——dev checkout(无 builtin、无 config repo)下解析不到,agent 按提示第一步就 "Saved workflow not found: ..."(`workflow.ts:412`),主流程在 dev 与 release 体验割裂。 -- README 死链/过时:`README.md:253` 指向已删除的 `change-review.yaml`;`README.md:30`、`README.zh.md:27` "仓库附带三类参考图"已不成立;`README.zh.md:66-67` 两 scope 表缺 builtin 层。 - -推荐:三处文档统一为三级描述并注明 builtin 仅存在于 release 构建;README 死链改为指向配置仓库。 - -**4. MEDIUM — Windows runner `DAG_TEMPLATES_DIR` 路径未做 cygpath 转换(同文件不对称)** - -`release-fork.yml:175` 写 `DAG_TEMPLATES_DIR=$GITHUB_WORKSPACE/dag-templates-src`,无任何转换;同 job 的 models.dev step(`:145-147`)对 `$RUNNER_TEMP` 显式做了 `cygpath -m`——作者在同一文件证明知道此坑。若 Windows bash 下 `GITHUB_WORKSPACE` 呈 MSYS 形(`/d/a/...`)或 Bun 无法解析混合分隔符路径,glob 零匹配 → **Windows 发布版静默丢失 builtin 模板**(generate.ts 空集不报错,`workflows.ts:51-56` 优雅降级为两级)。 - -推荐:与 models.dev 一致加 `cygpath -m`(保留 POSIX 与 Windows 双形态兼容写法),并在 build 步骤后加一步生成物校验(如 `bun -e` 检查二进制含模板名),防静默降级。 - -**5. MEDIUM — `/dag-template-update` prompt 锁语义两个缺口(纯 prompt 契约,无代码兜底)** - -- 父目录前置缺失:锁创建 `mkdir /workflows/.dag-update.lock`(`dag-template-update.txt:72-74`)在 `/workflows` 不存在时失败是 **ENOENT 而非 EEXIST**;"创建目录"指令在 L97 Failure handling 才出现,且措辞是 "before applying",锁在下载前——agent 可能把 ENOENT 误判为"有并发更新"。 -- 陈旧锁无恢复:agent 崩溃/被杀后 `.dag-update.lock` 永久残留,`L75-76` 只说"wait and retry, then stop"——此后该命令永远不可用,无 mtime/age 检测、无强制覆盖、无提示手工清除。 - -推荐:锁步骤前明确先 `mkdir -p /workflows`;增加"若锁已存在,检查其 mtime 超时(如 >30min)则视为陈旧、报告并提示删除后重试"。 - -**6. LOW — `workflows.ts:16` 头注释残留 "same two-level scope"** - -三级已落地但注释未更新(:9-14 已改为三级,:16 与之一句之隔自相矛盾)。推荐改为 "same multi-level scope" 或移除。 - -**7. LOW — 空库消息的 builtin 提及是不可达死代码** - -`tool/workflow.ts:145` 的 empty-library 分支:builtin map 非空时 `list()` 必含 builtin 条目(`workflows.ts:117-120`)→ `entries.length === 0` 不成立;map 空时 `searchedScopes` 不追加 builtin 文案。分支只在"builtin 存在"时命中,文案永不出现。 - -**8. LOW — `list()` 展示的 `builtin://name` 路径不可回填 `spec_path`,与 dag-flow.txt 指引冲突** - -`dag-flow.txt:16` 说 "pick by name or path";builtin 条目路径(`workflows.ts:58,131-133`)经 `resolveSpecPath` 路径分支(`tool/workflow.ts:415-424`)会被 `path.resolve` 折叠 `//` 后报 "must be a .yaml or .yml file"。用户按文档回填即报误导性错误(正确用法是裸名,但文档未说清)。 - -**9. LOW — `.yml` 漂移:三层只认 `.yaml`,运行时认 `.yml`,`generate.ts:52` 的 `?` 是死代码** - -运行时 `EXTENSIONS` 含 `.yml`(`workflows.ts:33`),但 generate.ts glob(`generate.ts:51`)、release 打包(`release-fork.yml:83`)、update prompt 全只匹配 `.yaml`;`/\.ya?ml$/` 中的 `?` 无输入可达。config repo 若放入 `.yml` 会被静默丢弃——布局契约全靠隐式约定,建议文档化或在 glob 统一。 - -**10. LOW — build-cli checkout 未 pin `ref: ${{ github.sha }}`,release tag 锚定旧 sha 竞态** - -`release-fork.yml:122-126` checkout 默认分支 tip;`release` job `--target "${{ github.sha }}"`(`:264`)锚定触发瞬间的 commit。构建期间若分支有 push,二进制与 tag 指向不同 commit。低概率但存在;成本极低(加一行 `ref`)。 - -**11. LOW — `Entry.content` 死字段** - -`workflows.ts:42-43,135-137` 设置 content,全仓库无消费者(start 路径重查 `builtinTemplates()`,`tool/workflow.ts:361`;list 输出不含)。注释标注为预留,可接受,但建议注明驱动场景(TUI 预览)避免误以为已接通。 - -## unverified_claims - -- Windows runner bash 下 `$GITHUB_WORKSPACE` 的实际形态(`D:\a\...` vs `/d/a/...`)及 Bun 对混合分隔符路径的解析行为——决定 finding 4 是否实际触发;本地无法复现 windows-latest 环境。 -- opencode-dag-config 仓库根目录只含顶层 `*.yaml`(无 `.yml`/子目录)——glob、release 打包、update prompt 三层假设成立与否无法本地验证。 -- release 产物二进制确实包含模板(define round-trip 端到端)——仅静态验证 `build.ts:203` + `generate.ts:44-57`,与 `OPENCODE_MODELS_DEV` 先例同构,风险低。 -- core v2 Command catalog(plugin draft 落点)是否被任何 TUI/会话表面消费——静态追踪显示无(`command.list` 路由与 session 均走应用 Command 服务),未做运行时验证。 -- `"undefined"` 字符串经 Bun.build define 注入为 `undefined` 关键字语义——models 生产先例已证明,风险低。 - -## summary - -总体架构方向正确:三级 scope(project > global > builtin)的解析/遮蔽/去重语义在代码层自洽,define 注入复用了 `OPENCODE_MODELS_DEV` 成熟先例(typeof 守卫、`"undefined"` 字面量降级均正确),release 双渠道(资产 + 内嵌)职责清晰,`package-templates` 与 build-cli 的耦合度合理,已修复项(ReferenceError 守卫、Entry 去重、空 glob、重复下载)均验证为正确修复而非回退。阻塞性问题只有一个但致命:`/dag-template-update` 命令只注册了 core plugin draft 一处,未按 dag-flow 先例接入应用 Command 服务,运行时不可达——发布前必须补注册。次要面(builtin 零测试、文档三级不一致、Windows 路径转换、锁语义缺口)应在合并前或紧随其后处理,其中 finding 4 若成立会导致 Windows 发布版静默丢失该 PR 的核心价值。 \ No newline at end of file diff --git a/.opencode/.dag-specs/review-parts/review-logic.md b/.opencode/.dag-specs/review-parts/review-logic.md deleted file mode 100644 index 17bddc0065..0000000000 --- a/.opencode/.dag-specs/review-parts/review-logic.md +++ /dev/null @@ -1,79 +0,0 @@ -审查完成。以下为逻辑正确性审查报告。 - -## 1. findings - -### CRITICAL -无。 - -### HIGH - -**H1. Windows 发布构建的 `DAG_TEMPLATES_DIR` 路径未做 cygpath 转换(与同文件 models 步骤不对称)** -- description: Extract Templates 把 `$GITHUB_WORKSPACE/dag-templates-src` 原样写入 GITHUB_ENV;而同一文件 15 行之上的 models 步骤对 `$RUNNER_TEMP` 路径显式做了 `cygpath -m` 转换——作者已知 Windows runner 的 bash 下路径形态陷阱,此处遗漏。若 bash 中 `GITHUB_WORKSPACE` 为 MSYS/POSIX 形(`/d/a/...`),原生 Bun 进程无法解析该 cwd:本地实测 `Bun.Glob.scan({cwd: 不存在目录})` 抛 ENOENT(fail-loud,build 失败);若 scan 恰好返回空则 builtin 静默缺失。无法本地验证 Windows 侧行为(见 U1/U2)。 -- evidence: `.github/workflows/release-fork.yml:175` vs `.github/workflows/release-fork.yml:145-147` -- recommendation: 与 models 步骤同构,写入 GITHUB_ENV 前 `cygpath -m`(cygpath 存在时):`echo "DAG_TEMPLATES_DIR=$(cygpath -m "$GITHUB_WORKSPACE/dag-templates-src")" >> "$GITHUB_ENV"`。 - -**H2. builtin scope 零测试覆盖,且被删的仓库自检无替代** -- description: 新增面全部无测试:`builtinTemplates()` 守卫、resolve builtin 兜底(workflows.ts:86-88)、list builtin 合并(117-120)、`isBuiltinPath`/`builtinName`、`readWorkflowSpec` builtin 分支(workflow.ts:359-370)、`searchedScopes` 拼接。全仓 test 目录 `rg builtin` 零命中。删除的 "repository's own workflow library" 测试是唯一对真实 spec 做 `StartSpec` 解码 + prompt_template/depends_on 引用完整性校验的守卫;删除后 config repo 模板完全脱离本仓库任何 CI 校验(该仓库无 CI 挂钩)。删除本身必要(4 个模板已移出),但无替代校验。 -- evidence: `packages/opencode/test/dag/dag-workflows.test.ts`(diff 删除 describe 块);`packages/opencode/src/dag/workflows.ts:51-56,86-88,117-120` -- recommendation: 为 builtinTemplates/resolve/list 补单测(测试环境无法注入 define——可把 builtin map 改为可注入来源,或在测试内直接断言守卫分支 + 用 `Object.defineProperty(globalThis, ...)` 模拟注入);同时考虑在 opencode-dag-config 仓库加模板解码 CI。 - -### MEDIUM - -**M3. `list()` 展示的 `builtin://name` 路径不可回填为 spec_path(工具契约断裂)** -- description: project/global 条目展示的路径可直接回传;builtin 条目路径 `builtin://name`(无扩展名)回传时 `isName` 因含 `/` 为 false → 落入 path 分支 → `path.resolve` 折叠 `//`、extname 为空 → 报误导性错误 "must be a .yaml or .yml file"。结合 dag-flow.txt:16 的 "pick by name or path",agent 按 list 输出回填必失败。 -- evidence: `packages/opencode/src/dag/workflows.ts:136`(path 生成);`packages/opencode/src/tool/workflow.ts:408`(isName 拒绝)、`415-418`(extname 检查);`packages/core/src/plugin/command/dag-flow.txt:16` -- recommendation: 在 resolveSpecPath 的 path 分支前特判 `builtin://` 前缀直接走 builtin 解析;或在 list 输出中为 builtin 条目注明 "start by name only"。 - -**M4. dag-flow.txt 描述 "two scopes" 与运行时三级不符;4 个指名模板在 dev checkout 下不可解析** -- description: 运行时是 project → global → builtin 三级(workflows.ts:9-14),提示词只字未提 builtin——而对开箱即用用户,curated 模板恰恰只存在于 builtin 层,global 层为空。design-decision-loop / parallel-development-loop / deep-review-dag-module / change-review 四个名字对应本 PR 删除的模板(diff 872 行删除),dev checkout(无 builtin 注入、未跑 update)下全部解析失败。"run list first" 是兜底,但指名具体模板会误导。 -- evidence: `packages/core/src/plugin/command/dag-flow.txt:13-19`;`packages/opencode/src/dag/workflows.ts:9-14,35` -- recommendation: 更新为三级描述;对 4 个模板名标注"需已安装(builtin/已跑 update)"。 - -**M5. /dag-template-update 锁语义两个缺口:父目录 ENOENT 歧义 + 陈旧锁无恢复** -- description: (a) `mkdir /workflows/.dag-update.lock` 在 `/workflows` 不存在时失败是 ENOENT 而非 EEXIST,而提示词的唯一诊断是"fails because the directory exists → another update is in progress"(L72-74),agent 可能误报并发;"create it before applying"(L97)位置在锁章节之后,且锁章节说 "before downloading or merging"(L70)与段落线性顺序(下载→干跑→合并→锁)矛盾。(b) agent 崩溃/被杀后锁目录永久残留,后续更新永远停在"already running",无 mtime/age 检测、无强制清理或手工清除指引。 -- evidence: `packages/core/src/plugin/command/dag-template-update.txt:67-78,97` -- recommendation: 明确"先 `mkdir -p /workflows` 再取锁";增加陈旧锁恢复路径(检测锁目录 mtime 超阈值视为陈旧,或提示用户手工 rmdir 后再试)。 - -**M6. README 死链与过时声明(模板删除的连带损伤)** -- description: README.md:227 链接已删除的 `./.opencode/workflows/change-review.yaml`(404);README.md:30 / README.zh.md:27 "仓库附带三类参考图"已不成立;README.zh.md:66-67 两级 scope 表缺 builtin 层。 -- evidence: `README.md:227,30`;`README.zh.md:27,66-67` -- recommendation: 随本 PR 同步更新(本 PR 未触碰 README,属遗漏)。 - -### LOW - -**L7. `.ya?ml$` 的 `?` 是死代码;`.yml` 被三层一致丢弃** -- description: glob 只匹配 `*.yaml`,正则中 `?` 永不生效;运行时 EXTENSIONS 含 `.yml`(workflows.ts:33),但 generate.ts glob、release 打包(release-fork.yml:83)、update 提示三层都只认 `.yaml`——config repo 若放 `.yml` 模板会被静默丢弃。 -- evidence: `packages/opencode/script/generate.ts:51-52`;`.github/workflows/release-fork.yml:83` -- recommendation: 要么统一支持 `.yml`,要么去掉 `?` 并把"仅 .yaml"写成显式约束。 - -**L8. 空库消息的 builtin 提及是不可达死代码** -- description: builtinTemplates 非空 → list() 必有 ≥1 条目 → `entries.length === 0` 分支不可能执行;map 空 → searchedScopes 不追加 builtin 文案。两条件互斥,builtin 文案永不出现于空库消息。not-found 消息中的 builtin 提及(map 非空但该名缺失)可达且正确。 -- evidence: `packages/opencode/src/tool/workflow.ts:145` vs `395-399`;`packages/opencode/src/dag/workflows.ts:117-120` -- recommendation: 无害,但应移除或重构消息构造,避免维护者误读。 - -**L9. `Entry.content` 死字段** -- description: builtinEntry 写入 content(workflows.ts:135-137),但 readWorkflowSpec 重新查 `builtinTemplates()`(workflow.ts:360-364),全仓无 `.content` 消费者。 -- evidence: `packages/opencode/src/dag/workflows.ts:42-43,135-137`;全仓 grep 无消费者 -- recommendation: 为 TUI/预览预留可保留,建议加注释说明意图。 - -**L10. builtin 内容绕过 1MB size 检查** -- description: size 检查仅文件分支(workflow.ts:376),builtin 分支(359-370)跳过——内容为构建期策展,风险可接受,属隐式信任面。 -- evidence: `packages/opencode/src/tool/workflow.ts:376` vs `359-370` -- recommendation: 可接受,建议注释注明设计意图。 - -**L11. opencode-dag-config 未 pin tag/commit** -- description: update 命令下载 `refs/heads/main`(L31)、release clone 同取 HEAD(release-fork.yml:74-77)——同一 URL 两处 HEAD,可复现性弱,模板变更不可追踪。 -- evidence: `packages/core/src/plugin/command/dag-template-update.txt:31`;`.github/workflows/release-fork.yml:74-77` -- recommendation: 考虑 pin 到 tag;属设计权衡。 - -## 2. unverified_claims - -- U1: windows-latest runner 的 bash 中 `$GITHUB_WORKSPACE` 的实际形态(POSIX `/d/a/...` 还是原生 `D:\a\...`)——决定 H1 是否触发,本地不可验证 -- U2: Bun@Windows 对 MSYS/POSIX 形 cwd 的 `Glob.scan` 行为(本地仅在 macOS 实测:缺失 cwd 抛 ENOENT) -- U3: release 流水线端到端产物实际包含 builtin 模板(package-templates → build-cli → 二进制)——仅静态分析,config repo 私有/无法本地跑通 -- U4: opencode-dag-config 仓库根目录仅含 `*.yaml`(若含 `.yml`/子目录会被 glob 与打包三层一致静默丢弃,见 L7) -- U5: Windows 上 xdg-basedir 对 `Global.Path.config` 的实际回退(提示词仅声明 macOS/Linux 行为,运行时代码静态一致但 Windows 分支未验证) - -## 3. summary - -核心逻辑正确:resolve 三级回退嵌套(scope 外循环 × extension 内循环)、list 去重/排序与 resolve 优先级严格一致、readWorkflowSpec builtin 分支(缺失报错/坏 YAML → `workflowSpecParseError`)与文件分支同形、typeof 守卫在 dev/release 双环境语义正确、generate.ts 路径拼接与 JSON.stringify 无注入面、release-fork.yml 的 nullglob 守卫与 needs 链(push 触发不中断)均确认无误;40 项相关测试与 typecheck 全绿,无 CRITICAL。已修复项(ReferenceError 守卫、Entry 去重、M4 空 glob、重复下载删除)确认修复正确无回退,且探索波声称的 `command/index.ts` Default 枚举缺口经查该文件不存在,已证伪。主要风险集中在 H1(Windows 路径形态,同文件有 cygpath 先例)、H2(builtin 零测试 + 被删自检无替代)与 M3/M4/M5(builtin 路径不可回填、提示词两级表述、锁的 ENOENT/陈旧锁缺口);建议合并前至少处理 H1、H2 与 M3,其余为文档与打磨项。 \ No newline at end of file diff --git a/.opencode/.dag-specs/review-parts/review-robustness.md b/.opencode/.dag-specs/review-parts/review-robustness.md deleted file mode 100644 index 6bd1cea978..0000000000 --- a/.opencode/.dag-specs/review-parts/review-robustness.md +++ /dev/null @@ -1,50 +0,0 @@ -所有关键证据已收集并验证(测试 14/14 通过、resolve 调用面确认、命令注册链确认、draft.update 创建语义确认)。以下是运行时健壮性审查报告。 - ---- - -# PR #167 运行时健壮性审查 - -## 1. Findings - -| # | Severity | Title | Description | Evidence | Recommendation | -|---|----------|-------|-------------|----------|----------------| -| F1 | **HIGH** (unverified 行为) | Windows 构建 `DAG_TEMPLATES_DIR` 缺 cygpath 转换,1/3 平台发布版可能静默无内置模板 | `Extract Templates` 将 `$GITHUB_WORKSPACE/dag-templates-src` 裸写入 GITHUB_ENV;同一文件 22 行前的 models.dev step 对 `$RUNNER_TEMP` 显式 `cygpath -m`(说明作者已知 MSYS/原生路径混用坑),此步却无转换。若 Windows runner 下 Bun 无法解析该路径 → glob 0 模板 → 构建成功、发布照发、builtin 静默为空,仅 build 日志一行 "0 templates" | `.github/workflows/release-fork.yml:175` vs `:145-147` | 与 models.dev 同款防御:`DAG_TEMPLATES_DIR="$(cygpath -m "$GITHUB_WORKSPACE/dag-templates-src")"`,或改用原生 `${{ github.workspace }}` 上下文 | -| F2 | MEDIUM | builtin 注入缺失仅静默降级,无任何显式失败门禁 | env 存在但 glob 0(Windows 路径 bug、config repo 无 yaml、tar 空)→ `JSON.stringify({})` → 二进制无 builtin,release 照常成功。`generate.ts:55` 日志不参与 CI 判定。审查准则 3 的答案是:**静默降级,无显式报错路径**(与 models.dev `:158` warning 同构,属团队既有模式) | `script/generate.ts:51-56`、`release-fork.yml:87` | 在 package-templates job 或 build-cli 增加显式校验 step:模板数 > 0 否则 fail-loud(空 config repo 时发布中止而非静默发无 builtin 版) | -| F3 | MEDIUM | `/dag-template-update` 孤儿锁无恢复路径 | 锁用 `mkdir .dag-update.lock` 原子判定,但 prompt 无 mtime/age 检测、无强制清除、无"锁陈旧则接管"路径。agent 进程崩溃/被杀后锁永久残留,后续更新永远停在"another update is already running",恢复只能靠用户手动 rmdir(提示词未指导) | `dag-template-update.txt:67-78` | 补充锁龄检测(如超过 N 分钟视为陈旧,允许接管)或明确指导用户删除锁目录的恢复步骤 | -| F4 | MEDIUM | 锁的父目录前置条件缺失:全新机器 ENOENT 误判 | 锁路径为 `/workflows/.dag-update.lock`,目录创建指令(`:97`)位于 Failure handling 段,晚于锁步骤(`:70`"before downloading or merging")。全新机器 `/workflows` 不存在时 `mkdir` 报 ENOENT 而非 EEXIST,agent 可能误判"另一更新进行中"而停止 | `dag-template-update.txt:70,72,97` | 明确顺序:先 `mkdir -p /workflows` 再取锁;并将 ENOENT 与 EEXIST 的判别写进提示词 | -| F5 | MEDIUM | builtin 新功能零测试覆盖,模板契约校验随旧测试删除而消失 | `builtinTemplates()`、resolve builtin 兜底、list builtin 合并、`isBuiltinPath/builtinName`、`readWorkflowSpec` builtin 分支、`searchedScopes` 提示全部无测试(测试环境守卫返回 `{}`,无注入钩子)。旧 "repository's own workflow library" 测试(模板 `depends_on`/`prompt_template.id` 完整性校验)删除后,config repo 模板与本仓库 CI 完全脱钩 | `test/dag/dag-workflows.test.ts`(14 测全过但无 builtin 用例)、`dag-workflows.test.ts:161-187` 删除块 | 在 config repo 加独立 CI 校验 spec 结构;本仓库可加 `DAG_TEMPLATES_DIR` 指向 fixture 的构建期注入测试(generate.ts 可直接单测) | -| F6 | MEDIUM | dag-flow.txt 与运行时三级 scope 及实际模板名漂移 | prompt 只描述 two scopes(`:13`)、称 global "curated by the opencode-dag-config repo"(`:14`)、指名 4 个**本 PR 已删除**的提交模板(`:17-20`)。开箱即用用户(未跑 update、无全局目录)的模板只在 builtin 层,但 prompt 引导的名字与二进制实际内容(构建时从 config repo HEAD 动态取)**零同步校验**——config repo 改名即解析不到 | `dag-flow.txt:13-20` | 补 builtin 层描述;删除/弱化固定模板名(改为"按 list() 输出选择");构建时校验 dag-flow.txt 提及名 ⊆ builtin 名或删名 | -| F7 | MEDIUM | 供应链 pin 缺失:release 与 update 均取 config repo HEAD | `actions/checkout` 无 `ref`(release-fork.yml:73-77)、codeload URL 固定 `refs/heads/main`(dag-template-update.txt:32)。任何推送到 config repo 的提交(包括恶意或意外)立即进入所有新发布二进制,且内置模板信任级声明弱(workflows.ts:12-14 仅说 "curated") | `release-fork.yml:76`、`dag-template-update.txt:32`、`workflows.ts:12-14` | 至少 pin tag/commit 并在 workflow 注明;信任边界文档(builtin 与 dag.jsonc 同级)显式写入说明 | -| F8 | LOW | 备份文件 `.bak-*` 永久累积 | 每次 overwrite 生成一个带时间戳备份,无清理策略、无数量上限。已被 `list()` 的 EXTENSIONS 过滤不污染库列表,但长期更新磁盘持续膨胀 | `dag-template-update.txt:62-65` | 补充保留策略(如保留最近 N 份)或提示用户清理 | -| F9 | LOW | 锁重试次数未具体化 | "wait briefly and retry a few times"(`:75`)由 agent 自由裁量,无明确次数/间隔,行为不可复现 | `dag-template-update.txt:75-76` | 给出具体值(如 3 次 × 2 秒) | -| F10 | LOW | list 展示的 `builtin://name` 路径不可回填且报误导性错误 | 用户把列表路径回填 spec_path → `path.resolve` 折叠 `//` → 非 builtin 分支 → 报 "must be a .yaml or .yml file"(:416-417),而非提示用裸名 | `workflows.ts:58,131-133`、`tool/workflow.ts:415-418` | 路径分支对 `builtin://` 前缀给出"请用裸名"提示 | -| F11 | LOW | builtin 内容绕过 1MB 大小检查 | `MAX_WORKFLOW_SPEC_BYTES` 检查仅文件分支(:376-380),builtin 分支(:360-370)直接解析。构建期信任可接受,但 config repo 若被塞大文件 → 二进制膨胀 + 无防护 | `tool/workflow.ts:360-370` vs `:376-380` | 构建期 generate.ts 对模板体积设上限即可 | -| F12 | LOW | 空库消息中 builtin 提及为死代码 | `searchedScopes` 在 builtin map 非空时附加 builtin 文案,但 map 非空时 `list()` 必然含 builtin 条目 → `entries.length === 0` 永不成立 | `tool/workflow.ts:142-147,395-399` | 删除条件或接受为无害冗余 | -| F13 | LOW | build-cli checkout 未 pin `github.sha` 而 release `--target` 锚定 sha | workflow_dispatch 下默认检出分支 tip,构建期间新 push → 二进制与 tag 锚点不一致(低概率) | `release-fork.yml:124-126` vs `:264` | checkout 加 `ref: ${{ github.sha }}` | -| F14 | LOW | `.yml` 三处不一致:运行时支持、构建/打包/更新只认 `.yaml` | `EXTENSIONS` 含 `.yml`(workflows.ts:33),但 generate.ts glob(:51)、release 打包(:83)、update prompt 全只取 `*.yaml`;`\\.ya?ml$` 的 `?` 是死代码 | `workflows.ts:33`、`generate.ts:51-52` | 统一为 `.yaml` 或补齐 `.yml` | - -**已修复项确认**(无回退):ReferenceError 守卫正确(`typeof` 语义,workflows.ts:54);Entry 去重正确(seen-map,:104-120);M4 空 glob 正确(`shopt -s nullglob` + 数组守卫,release-fork.yml:82-88);重复下载 step 已删。**路径穿越检查**:`resolve()` 唯一调用方经 `isName`(拦截 `/`、`\`、extname、控制字符、`.` 开头)→ `path.join` 无穿越面;builtin map key 来自构建期 glob 文件名(非递归)→ 安全;`builtinName()` 仅作 map 索引与错误文案,无 fs 操作 → 安全。**parseMeta 容错**:恶意 YAML 只影响 title/nodes 元数据显示(类型守卫,:162-165),start 路径 fail-loud(workflow.ts:364-368)→ 满足"恶意 YAML 只影响元数据解析"。**下载/解压失败**:prompt 强制原文报错且停(:94-96),内容级 verify 兜底(:80-90)→ 满足准则 5。**失败传播**:package-templates 任一失败(clone/tar/upload)→ build-cli、release 全部 skip → run failed、无 release 产出 → fail-loud 中止正确。 - -## 2. Unverified Claims - -1. **Bun@Windows 对 `D:\a\...` 风格 `DAG_TEMPLATES_DIR` 的 `Bun.Glob.scan({cwd})` 解析行为** —— 决定 F1 实际影响;无本地环境可验证 -2. **GitHub Actions windows runner 的 bash 中 `$GITHUB_WORKSPACE` 实际形态**(`D:\a\...` 原样 vs MSYS 转换)—— F1 前提 -3. **opencode-dag-config 仓库根目录布局**:顶层 `*.yaml` 假设、模板名集合、与 dag-flow.txt 所列 4 个名字(design-decision-loop 等)是否匹配 —— F6 实害 -4. **node 构建产物(build-node.ts:23 未注入 define)是否属用户分发路径** —— 若 npm 分发则这些用户无 builtin -5. **release 二进制实际包含 builtin 模板** —— 无法本地构建验证(config repo 私有/外网),仅静态确认 define 链 -6. **xdg-basedir 在 Windows 的实际回退路径**(dag-template-update.txt:22-24 仅声明 macOS/Linux)—— update 命令写入位置与运行时读取位置(workflows.ts:145)一致性 - -## 3. Failure Scenarios - -| Scenario | Impact | Likelihood | -|----------|--------|------------| -| Windows runner `DAG_TEMPLATES_DIR` 不可解析 → 3 平台中 windows 版静默无内置模板,发布照常成功 | Windows 用户开箱即用无 curated 模板,无任何失败信号 | MEDIUM(cygpath 不对称是强信号,但 Bun@Windows 行为未证) | -| config repo 克隆失败 / tar 失败 / artifact 上传失败 | 整条 needs 链(build-cli + release)skip,run 失败、无 release 产出 —— 失败传播正确、fail-loud | LOW(网络/瞬时故障,手动重试即可) | -| config repo 空或无 `*.yaml` | warning + 空 tar → 全平台二进制无 builtin,release 照发(仅日志可见) | LOW(作者自控仓库) | -| `/dag-template-update` 执行中 agent 进程崩溃 → 孤儿锁 | 所有后续更新永久阻塞,需用户手动 rmdir 恢复;无提示指导 | MEDIUM(长任务中断概率非零) | -| 全新机器执行 `/dag-template-update` | 锁 mkdir ENOENT 被误判为并发冲突而中止更新 | MEDIUM(prompt 顺序歧义) | -| config repo 模板改名/损坏 YAML | 损坏 → list 容错、start 报清晰错误(✓);改名 → dag-flow.txt 引导的名字解析不到,agent 走兜底多耗一轮 | MEDIUM(跨仓库契约无门禁) | - -## 4. Summary - -代码层健壮性整体良好:运行时三级解析/去重/容错/路径安全全部验证通过,release 失败传播是 fail-loud 中止(正确行为),已修复的 4 项确认无回退。主要风险集中在**边界静默性**:Windows 路径转换缺失(F1,修复成本一行)与 builtin 注入缺失无 CI 门禁(F2)会让发布版无声降级;`/dag-template-update` 作为纯 prompt 命令缺乏孤儿锁恢复与目录前置处理(F3/F4),崩溃恢复体验脆弱。测试缺口(F5)与 dag-flow.txt 模板名漂移(F6)是中等风险,需 config repo 侧 CI 或构建期校验补齐。无 CRITICAL 项,建议 F1/F2 修复后合并。 \ No newline at end of file diff --git a/.opencode/.dag-specs/review-parts/review-style.md b/.opencode/.dag-specs/review-parts/review-style.md deleted file mode 100644 index 96cb91c804..0000000000 --- a/.opencode/.dag-specs/review-parts/review-style.md +++ /dev/null @@ -1,41 +0,0 @@ -## 风格与惯例审查报告(PR #167) - -审查范围:`git diff origin/dev...HEAD`(4 commits)全部 TS/TSX 与 YAML 变更。已核实:40 个 dag 相关测试全部通过(`bun test dag-workflows workflow-tool`,0 fail);验证了 explore 波的关键声明。 - -### 1. findings - -| # | severity | 标题 | 说明 | evidence | 建议 | -|---|----------|------|------|----------|------| -| 1 | **HIGH** | `DAG_TEMPLATES_DIR` Windows 路径未转换,同文件内 cygpath 先例被遗漏 | build-cli 的 Extract Templates 步骤把 POSIX 形 `$GITHUB_WORKSPACE/dag-templates-src` 直接写入 `GITHUB_ENV`;同一文件 30 行前 models.dev 步骤对 `$RUNNER_TEMP` 显式做了 `cygpath -m` 转换(作者已证明知道此坑)。Windows runner 的 bash 下 `GITHUB_WORKSPACE` 为 `/d/a/...` 形态,原生 Bun 进程读取该路径若失败 → glob 0 个模板 → **Windows 发布版静默丢失全部 builtin 模板**(降级为两级 scope,无任何报错) | `.github/workflows/release-fork.yml:175` vs `:146-147` | 与 models.dev 步骤对齐,写 env 前对路径做 `cygpath -m` 转换(非 Windows 下幂等) | -| 2 | **MEDIUM** | `parseMeta` 假 async + 冗余 Promise 链包裹同步解析 | `Bun.YAML.parse` 是同步操作,却被 `Promise.resolve(text).then((value) => Bun.YAML.parse(value)).catch(() => undefined)` 包进 Promise 链(pre-PR 的 `.text().then(parse)` 形状遗留物)。违反 AGENTS.md「同步解析、校验应保持同步」精神,并强制 `builtinEntry`/`resolve`/`list` 无谓地携带 async/await。全 src 无此包裹同步解析的先例(`Promise.resolve` 在 src 中仅用于 sync-callback 桥接) | `packages/opencode/src/dag/workflows.ts:158-161` | parseMeta 改为同步函数(`Effect.try` 或 `Promise.resolve().then(...).catch(...)` 单链),builtinEntry 随之同步,调用点去掉 `await` | -| 3 | **MEDIUM** | `/dag-flow` 提示词停留在"两 scope",与本次 PR 的核心新增(builtin 第三级)脱节 | dag-flow.txt 本次 PR 内被修改(11 行 diff),但 :13 仍写"installed in **two scopes**";全局/项目顺序反列(L14-15 global 在前,实际解析 project 优先);builtin 层完全缺失——而**全新安装(未跑 update)时 curated 模板恰恰只存在于 builtin 层**。:16 "pick by name or path" 也有误导:`list()` 对 builtin 项展示的 `builtin://name` 路径含 `/` → `isName` 为 false → 落入 path 分支报 "must be a .yaml or .yml file"(workflow.ts:416-418),路径回填必然失败 | `packages/core/src/plugin/command/dag-flow.txt:13-16` | 补第三级 scope 说明(builtin 编译进二进制);路径选择措辞改为"按名选择",删除对 builtin 路径回填的暗示 | -| 4 | **MEDIUM** | builtin 全新增面零测试覆盖,且唯一真实 spec 校验测试被删除 | `resolve` 的 builtin 兜底、`list` 的 builtin 合并去重、`isBuiltinPath`/`builtinName`、`readWorkflowSpec` builtin 分支、`searchedScopes` 提示——全部无测试。测试环境 `typeof` 守卫恒返回 `{}`,无任何注入钩子。被删的 "repository's own workflow library" 测试是唯一的 `depends_on`/`prompt_template.id` 完整性守卫,删除后 config repo 模板完全脱离本仓库 CI | `packages/opencode/test/dag/dag-workflows.test.ts`(删除段);`workflows.ts:86-88,117-120` | 为 builtinTemplates 增加可注入的测试钩子(如通过 env/参数重载),补 resolve 兜底、list 遮蔽合并、searchedScopes 拼接的最小用例 | -| 5 | **LOW** | 过期注释:header 仍称"same two-level scope" | 本次 PR 把查找顺序改为三级(并更新了上方 bullet 列表),但 `Mirrors config.ts: same two-level scope` 未同步 | `packages/opencode/src/dag/workflows.ts:16-17` | 改为 three-level 或删除该句 | -| 6 | **LOW** | `Entry.content` 死字段 | `builtinEntry`(:135-137)设置 content,但 `readWorkflowSpec` 重新查询 `builtinTemplates()`(workflow.ts:361),全仓库无任何消费者;`list()` 输出也不含它 | `packages/opencode/src/dag/workflows.ts:43,135-137` | 要么让 readWorkflowSpec 消费 entry.content,要么删字段(或加注释明确为预留面) | -| 7 | **LOW** | README 死链/过时声明(PR 删文件未同步文档) | README.md:253、README.zh.md:227 指向已删除的 `.opencode/workflows/change-review.yaml`(404);README.zh.md:27 "仓库已经附带三类强约束参考图" 已不成立;:66-67 scope 表缺 builtin 层 | `README.md:253`、`README.zh.md:27,66-67,227` | 更新为 config repo 引用 + 三级 scope 表;删除死链接 | -| 8 | **LOW** | 锁语义两个 prompt 级缺口 | ① `mkdir /workflows/.dag-update.lock`:首次运行时父目录不存在 → ENOENT,提示词将其归因为"目录已存在=他人持锁",agent 会误判;② 陈旧锁无恢复路径(崩溃后 `.dag-update.lock` 永久残留,无 age 检测/手工清除提示) | `packages/core/src/plugin/command/dag-template-update.txt:67-78,97` | 锁步骤前明确"先创建 workflows 目录";补充陈旧锁的处理指引 | -| 9 | **LOW** | "pinned repository URL" 措辞不实 | 提示词称 URL 为 "pinned/fixed",但 `codeload.../zip/refs/heads/main` 未 pin tag/commit(release 流水线同),可复现性弱 | `dag-template-update.txt:28-29`;`release-fork.yml:83` | 措辞改为"固定分支 URL"或真的 pin commit | -| 10 | **NIT** | `\.ya?ml$` 中的 `?` 是死代码 | glob 只匹配 `*.yaml`(generate.ts:51,release 打包、update prompt 亦只认 `.yaml`),正则暗示 `.yml` 支持并不存在——三层 `.yml` 漂移是既有事实 | `packages/opencode/script/generate.ts:52` | 去掉 `?` 或统一 `.yml` 支持 | -| 11 | **NIT** | 私有 helper 重复显式返回类型标注 | `describe` 与 `parseMeta` 各自标注相同的内联返回类型 `Promise<{ title?: string; nodes?: number }>`;repo 风格倾向依赖推断 | `workflows.ts:150,158` | 删除显式标注(或提取共享 type alias) | - -**核查通过项**(非 finding): -- 命名一致性:`builtinTemplates`/`builtinEntry`/`builtinName`/`isBuiltinPath`/`BUILTIN_PREFIX` 前缀统一 ✓;`DAG_TEMPLATE_UPDATE_PROMPT`/`DagTemplateUpdateDescription` 与 dag-flow 同构 ✓ -- 导出模式:`export * as DagWorkflows`(workflows.ts:20)沿用既有 module shape;新 helper 全部 namespace-private 不导出 ✓ -- 类型纪律:导出函数显式返回类型、私有函数推断,边界正确;无 `any` ✓ -- 控制流:全部早退、无 else、const 优先 ✓;`searchedScopes` 对 `searchPaths()` 返回的新数组 push 安全 ✓ -- Effect 纪律:builtin 分支的 `Effect.fail(new Error(...))` 与文件分支(workflow.ts:374,377,412)逐字同形,遵循文件内既有惯例 ✓ -- `Array.fromAsync` 为仓库既有模式(5+ 处使用)✓;动态 import 无新增需求 ✓ -- dag-template-update.txt 结构(`##` 分节 + 加粗术语 + 失败处理段)与 review.txt/initialize.txt 房风一致,比 dag-flow.txt 的数字列表更贴近主流结构 ✓ -- 已修复项确认:typeof 守卫(workflows.ts:54)、seen 去重(:117-120)、空 glob 守卫(release-fork.yml:84-89)均修复正确;测试全部通过 ✓ - -### 2. unverified_claims - -- **U1**:Bun@Windows 对 POSIX 形 `DAG_TEMPLATES_DIR`(`/d/a/...`)路径的解析行为——决定 finding #1 是否真致 Windows 版静默丢模板(本地无法验证 Windows runner,需 CI 实测或 Windows 环境确认) -- **U2**:release 产出的二进制实际包含 builtin 模板——define 注入链已静态验证与 `OPENCODE_MODELS_DEV` 同构(生产先例),但本 PR 产物未做 `--version`/运行时 smoke 验证 -- **U3**:opencode-dag-config 仓库根目录只含 `*.yaml`(顶层)——若含 `.yml` 或子目录会被 glob/打包/提示词三层静默丢弃 -- **U4**:`/dag-template-update` 的写入目录(prompt 描述)与运行时读取目录(workflows.ts:145 `Flag.OPENCODE_CONFIG_DIR ?? Global.Path.config`)逐字一致——已对 flag.ts/global.ts 静态核对,但未实测 XDG_CONFIG_HOME 全路径矩阵 -- **已消解**:explore 波声称的 `command/index.ts:51` Default 枚举缺 `DAG_TEMPLATE_UPDATE` 条目——该文件不存在(`packages/core/src/plugin/command/index.ts` IO error),`Default.DAG_FLOW` 全仓库零命中,此声明为误报,不构成 finding - -### 3. summary - -整体风格纪律良好:命名前缀统一、导出模式与模块形状合规、早退/const/推断类型纪律一致、Effect 错误处理与文件内既有惯例同构,dag-template-update.txt 的 `##` 分节结构与仓库 prompt 房风吻合;40 个 dag 测试全部通过。主要问题在**一致性**而非风格本身:dag-flow.txt 未同步 builtin 第三级(其 "two scopes" 描述在全新安装下直接误导 agent)、`parseMeta` 的假 async 是 pre-PR 形状的遗留物、README 死链未随模板删除更新。唯一 HIGH 是 release-fork.yml 的 Windows 路径转换遗漏——同一文件内已有 cygpath 先例,属可低成本修复的静默失败风险;builtin 零测试覆盖与 config repo 脱离 CI 校验是测试面最大缺口。 \ No newline at end of file diff --git a/.opencode/.dag-specs/review-parts/review-testability.md b/.opencode/.dag-specs/review-parts/review-testability.md deleted file mode 100644 index 12e53ad980..0000000000 --- a/.opencode/.dag-specs/review-parts/review-testability.md +++ /dev/null @@ -1,42 +0,0 @@ -# PR #167 测试与覆盖审查报告 - -**测试执行证据**(全部在 `packages/opencode` 内): -- `bun test test/dag/dag-workflows.test.ts` → **14 pass / 0 fail** ✓(34 expect 调用) -- `bun test test/dag/workflow-tool.test.ts` → **26 pass / 0 fail** ✓(80 expect 调用) -- `bun run typecheck`(tsgo --noEmit)→ **通过,零错误** ✓ -- 额外:本地实测执行 release-fork.yml 的 nullglob 打包片段(空 glob → warning + rc=0 + 合法空 tar,M4 修复行为确认) - -## 1. findings - -| # | severity | title | description | evidence | recommendation | -|---|----------|-------|-------------|----------|----------------| -| F1 | **MEDIUM** | 删除仓库自检测试后,模板完整性校验失去全部回归保护 | 被删测试(旧 L163-189)是唯一锚点:`resolve("change-review")` → StartSpec decode → 每个 `prompt_template.id` 存在 → `depends_on` 引用有效。删除是**必要**的(98e4c0624 删除 4 个模板后旧测试会因 `entry!.path` 抛错),但**无替代**:config 仓库在 repo 外且无 CI 挂钩,"air-gapped installs ship the curated templates"(workflows.ts:12-14)的声明没有任何自动化校验 | `git diff` dag-workflows.test.ts 旧 L163-189(仅删除,无新增);`rg builtin packages/opencode/test` 零命中 | 在两个仓库之一补锚点:config repo 加 CI(对每个 `*.yaml` 跑 StartSpec decode + prompt-template 引用校验),或本 repo 用 fixture spec 跑一次真实 decode 冒烟 | -| F2 | **MEDIUM** | builtin 三级 scope 全分支零测试覆盖——无注入钩子 | `builtinTemplates()`(workflows.ts:51-56)typeof 守卫在测试环境恒返回 `{}`,`declare const` 运行期无法注入。resolve builtin 兜底(workflows.ts:86-88)、list builtin 合并/遮蔽(workflows.ts:117-120)、`readWorkflowSpec` builtin 分支(workflow.ts:359-370,含缺失 fail 与 YAML 错误两路径)、`searchedScopes` builtin 提及(workflow.ts:395-399)在 40 个测试中全部不可达。project>global 遮蔽有测试(dag-workflows.test.ts:88-94),第三级的优先级契约零断言。**部分缓解**:14+26 个测试全部经守卫分支运行,隐式证明了无 ReferenceError | dag-workflows.test.ts:48-162 无任何 builtin 用例;workflow-tool.test.ts:1038-1184 无 builtin 引用 | 抽取注入面:将 `builtinTemplates()` 改为可注入/可 mock(或单测纯函数 `isBuiltinPath`/`builtinName`/`builtinEntry` + 带 stub map 的 resolve/list 用例),中成本关闭主要缺口 | -| F3 | **LOW/MEDIUM** | generate.ts 的 DAG_TEMPLATES_DIR 加载零测试,且存在测试约束 | `loadDagTemplatesData`(generate.ts:44-57)两个分支(env 未设→`"undefined"` 字面量;env 设→glob `*.yaml`→JSON)均无测试(`rg DAG_TEMPLATES_DIR|dagTemplatesData` 在 test/ 零命中)。关键路径"`"undefined"` 字面量 → define → typeof 守卫"(dev 运行依赖它)无回归保护。约束:import generate.ts 会触发顶层 `fetch(models.dev/api.json)`(generate.ts:24-26),测试需同时设 `MODELS_DEV_API_JSON` + `DAG_TEMPLATES_DIR` + 动态 import——可行但脆弱,解释了缺失 | generate.ts:44-59;`rg -n "DAG_TEMPLATES_DIR" packages --include='*.test.ts'` → 0 | 补一个双 env + 动态 import 的往返测试;或至少在 review 记录中显式接受该风险 | -| F4 | **LOW** | release-fork.yml bash 逻辑:nullglob 守卫**本地实测验证**,GITHUB_ENV 注入链有 Windows 转换缺失风险 | nullglob 片段(release-fork.yml:82-90)本地实测:有文件 → cp 执行 rc=0;空 glob → warning + 不执行 cp + tar 空目录 rc=0(M4 修复确认无 missing-operand 失败)。但 `DAG_TEMPLATES_DIR=$GITHUB_WORKSPACE/dag-templates-src`(release-fork.yml:172-175)无 `cygpath -m` 转换,而同文件 models.dev 步骤(release-fork.yml:145-147)显式转换——windows runner bash 下 GITHUB_WORKSPACE 为 POSIX 形 `/d/a/...`,原生 Bun 进程可能无法解析 → **Windows 发布版静默丢失 builtin 模板**(glob 空不报错)。本机无 Windows 无法验证 | release-fork.yml:82-90(实测)/ 172-175(静态);本地 bash 复现输出 rc=0 无报错 | 与 models.dev 同款加 `cygpath -m` 转换(或用 `${{ github.workspace }}` 原生形变量);最低限度在 release 后 smoke 断言二进制内置模板数 | -| F5 | **LOW** | M1 并发锁:现有锁测试与 `/dag-template-update` 的锁无关,update 锁结构性不可测 | dag-workflow-lock.test.ts:8-58 覆盖的是 `Dag.Service.extend` 同 workflow 串行化(mock DagStore 25ms sleep + `maxActiveReads===1`),是运行时锁,本 PR 未动。update 命令的 `.dag-update.lock`(dag-template-update.txt:67-78)是 **prompt 级契约**——无代码执行它,测试环境没有任何自动化手段验证(mkdir 原子性、重试、清理、陈旧锁无恢复均为 agent 行为) | dag-workflow-lock.test.ts 全文(未变更文件);dag-template-update.txt:67-78 | 接受现状并显式声明:该锁只有靠 agent 按 prompt 执行 + 人工 review;或未来把锁逻辑下沉为可测代码 | -| F6 | **LOW** | 测试纪律:符合"测真实实现"要求,无逻辑复制 | resolve/list 测试用真实 tmpdir fs fixture + 真实函数(无复制查找逻辑);workflow-tool 用真实 tool execute + 真实文件;锁测试用 `Layer.mock`(AGENTS.md 认可模式)。fixture 辅助函数(spec/savedSpec)是输入构造,非实现副本 | dag-workflows.test.ts:13-30, 71-162;workflow-tool.test.ts:1058-1059 | 无动作 | -| F7 | **LOW** | generate.ts:52 `\.ya?ml$` 的 `?` 是死代码;`.yml` 在打包链被静默丢弃 | glob 仅 `*.yaml`(generate.ts:51),`?` 分支不可达;release 打包 `dag-config/*.yaml`(release-fork.yml:83)与 update prompt 同。而运行时 `EXTENSIONS` 含 `.yml`(workflows.ts:33)——config repo 若放 `.yml`,对二进制与打包均不可见,依赖 repo 布局约定(U2) | generate.ts:51-52 | 三处统一为 `.ya?ml` 或显式文档化"config repo 只接受 .yaml" | -| F8 | **LOW** | builtin 内容绕过 1MB size 检查 | 文件分支有 size 检查(workflow.ts:376-380),builtin 分支无(workflow.ts:360-369)——构建期策展内容,风险低但属隐式信任面 | workflow.ts:359-370 vs 376-380 | 可接受,记录即可 | - -## 2. unverified_claims - -- **U1**:Windows runner 上 POSIX 形 `DAG_TEMPLATES_DIR`(`/d/a/...`)在原生 Bun 进程能否解析——若不能,Windows 发布版静默丢失 builtin 模板(release-fork.yml:172-175 vs :145-147 的 cygpath 不对称)。无 Windows 环境,无法本地验证。 -- **U2**:opencode-dag-config 仓库根目录只含 `*.yaml`(顶层)——若有 `.yml` 或子目录会被打包与注入链静默丢弃(仓库私有,无法查看)。 -- **U3**:`generate.ts` 返回的 `"undefined"` 字符串经 Bun.build define 注入后成为 `undefined` 关键字、触发 typeof 守卫——按 `OPENCODE_MODELS_DEV` 同构模式推断(生产已验证该机制),本地未跑 release 构建。 -- **U4**:release 产出的二进制实际包含模板——仅静态验证 define 注入链 + CI job 图(config repo 私有,本地无法复现 release 构建)。 -- **U5**:`/dag-template-update` 提示词行为(下载/合并/备份/锁/验证)无法自动化验证——纯 prompt 契约,只能靠 agent 执行后人工审计。 - -## 3. coverage_gaps - -| path | untested_scenarios[] | -|------|---------------------| -| `packages/opencode/src/dag/workflows.ts` | resolve 的 builtin 兜底命中(L86-88);list 的 builtin 条目合并/排序/被 project 与 global 遮蔽(L117-120);`builtinTemplates()` 守卫的"有值"正分支(L51-56,测试只走空分支);`isBuiltinPath`/`builtinName`(L126-133);`builtinEntry`/`parseMeta` 经 builtin 路径(L135-137, 158-167) | -| `packages/opencode/src/tool/workflow.ts` | `readWorkflowSpec` builtin 分支两条失败路径:内容缺失 fail(L362-364)、YAML 解析错误(L365-368);`searchedScopes` 追加 "the release's builtin templates" 的分支(L395-399,当前测试 env 下恒不触发) | -| `packages/opencode/script/generate.ts` | `loadDagTemplatesData` 两分支(L44-57):env 未设返回 `"undefined"`;env 设时 glob 收集 → name 去扩展名 → JSON 序列化往返 | -| `.github/workflows/release-fork.yml` | package-templates 空 glob 路径(本地 bash 实测过,非 CI 实测);Extract Templates 的 GITHUB_ENV 注入在 windows runner 的行为(U1) | -| 已删除测试的覆盖面(无替代锚点) | StartSpec decode 有效性;`prompt_template.id` 引用存在性;`depends_on` 完整性——模板迁到 config repo 后**任何地方**都没有自动化校验 | - -## 4. summary - -测试状态完全符合预期(14+26 pass、typecheck 干净),且测试纪律合规(测真实实现、Layer.mock 模式、无逻辑复制);nullglob 空 glob 修复经本地实测确认行为正确。主要问题是 F1/F2:第三级 builtin scope 是新行为契约却零测试覆盖,且删除仓库自检测试后模板完整性校验在两侧仓库都没有锚点——这是 PR 最大的测试缺口,建议优先补注入面测试 + config repo CI。release 流水线(F4/U1/U4)只能静态审查 + 依赖 Windows runner 与 config repo 的后续实证,属已知不可本地验证面,其中 Windows 路径转换缺失是最值得在合并前修复的低成本风险点。 \ No newline at end of file diff --git a/.opencode/batch-a-implement-manifest.md b/.opencode/batch-a-implement-manifest.md deleted file mode 100644 index d1cfc19fb8..0000000000 --- a/.opencode/batch-a-implement-manifest.md +++ /dev/null @@ -1,57 +0,0 @@ -# Batch A Implement — 图编排 Manifest - -## reference_template -`parallel-development-loop`(global,13 节点)——保护脊柱保留: -audit-module-wave(模块波本地审查,PASS|LOOP|BLOCKED)→ wire-modules(单一集成/提交所有者)→ simulate-wired-system(reasoner 纯逻辑推演)→ verify-wired-system(确定性验证)→ 三路 fresh review → arbitrate-final-review(唯一终审)→ finalize-delivery(仅 PASS 条件放行)。 - -## 任务注入 -`.scratch/batch-a/issues/01-08`(09 为 dev→main 收束票,CI 全绿后在图外执行)。 -规格依据:`.opencode/grill-batch-a/`(CONTEXT.md + ADR-0001~0004 + node-lifecycle-transitions.md v2,Round 2 doc 审核 PASS 冻结版,基线 dev@5330b15a9)。 - -## 展开(expand) -参考图的 develop-core / develop-adapters / develop-tests 三个泛模块槽替换为 8 个票据实现节点: - -| 节点 | 票据 | 依赖 | -|---|---|---| -| impl-q1 | 01 裁决旗清旗 | freeze-contract | -| impl-q2 | 02 送达门控 re-time | freeze-contract | -| impl-s5 | 05 锁一行超时 | freeze-contract | -| impl-flaky-stdout | 06 stdout 污染族 | freeze-contract | -| impl-flaky-ws | 08 workspace 计时 | freeze-contract | -| impl-q3 | 03 事件+guard 前移 | impl-q1(projector 写集串行) | -| impl-sdk | 04 SDK 再生 | impl-q3 | -| impl-flaky-share | 07 ShareNext 计时 | impl-flaky-stdout(同文件串行) | - -## 剪裁(prune_decisions) -| node | prune_reason | replacement_coverage | -|---|---|---| -| develop-core | 泛槽与已审计的票据分解不匹配 | 8 个票据节点按审计后写集分工,含测试切片(各票 TDD 自带) | -| develop-adapters | 同上 | 同上(03/04 覆盖 schema/SDK 适配面) | -| develop-tests | 测试切片并入各票 TDD | 每票先写失败测试再实现;review-tests 终审覆盖矩阵 | -| freeze-design | 设计已在图外冻结(两轮 doc 审核 PASS) | 改为 freeze-contract:只核验票据写集互斥并产出结构化契约,不重做设计 | - -## 写集互斥表 -- q1:projector 折叠侧 + dag 测试(清旗族) -- q2:runtime/loop.ts re-time 发起点 + 门控测试 -- q3:schema 事件定义 + EventManifest + dag.ts 命令路径 + projector handler + 测试 -- sdk:packages/sdk/js 生成物 + 消费者类型对齐 -- s5:dag.ts withWorkflowLock 包装层(唯一区域)+ 测试 -- flaky-stdout:test/cli/run + src/share/share-next.ts + 相关 fixture -- flaky-share:test/share 计时部分(06 已合入其依赖报告) -- flaky-ws:workspace sync 测试(+ 根因所需最小 src,须记录) -已知同文件异区:s5 与 q3 同 dag.ts(区域互斥:锁包装层 vs 命令路径);q1 与 q3 同 projector(已串行)。 - -## Git 纪律 -- 所有 impl 节点禁止任何 git 操作(add/commit/stash/branch/push) -- wire-modules 是唯一提交所有者(typecheck + 套件 + lint 全绿后单 commit) -- PR 由父会话在终审 PASS 后开(分支 feat/batch-a → dev) - -## 审查门禁义务 -arbitrate-final-review 必须审计本 manifest:每个 prune 有 prune_reason + replacement_coverage,缺任一禁止 PASS(fail-closed)。 - -## 续作记录(Continuation) -原图 dag_024a09546ffevmMvS3M6v0I1Av 于 audit PASS、wire-modules 提交 17f10f0ce 之后 terminal failed——两个 spawn 期配置错误:impl-q3({{freeze-contract}} 非直接依赖,已由 impl-q3b 替换并完成)与 verify-wired-system(replan 片段遗漏 input: repo)。 -终态不可逆 → 按续作合约起新图 batch-a-continue: -- reused_nodes:freeze-contract、impl-q1/q2/q3b/s5/flaky-stdout/flaky-ws/flaky-share/sdk、audit-module-wave(PASS)、wire-modules(提交 17f10f0ce)——全部完成且经审计,不重跑 -- 续跑尾部:verify-wired(修复 input 绑定)→ simulate-wired + 三路 review → arbitrate-final → finalize -- 尾部节点一律从真实仓库状态(git show HEAD + 票据 + grill 文档)取证,不注入可能为空的旧输出(fail-closed) diff --git a/.opencode/dag-prompts/arch-gate.md b/.opencode/dag-prompts/arch-gate.md deleted file mode 100644 index 3c45efc98d..0000000000 --- a/.opencode/dag-prompts/arch-gate.md +++ /dev/null @@ -1,46 +0,0 @@ -# Role: Architecture Gate (read-only) - -You are the architecture gatekeeper. Validate whether the design/spec provided in the Context section below conforms to this project's architecture constraints, BEFORE implementation starts. Never modify any file. - -## Evidence Sources (both carry equal authority) - -- Documentation: AGENTS.md architecture invariants, design docs, module contracts. -- Foundation code: existing interface signatures, layer composition, test-anchored behavior. - -Do not trust only the evidence handed to you — independently search the repository for the constraints that govern the touched domain. Missing input evidence does not mean no constraint exists. - -## Review Dimensions (a hit requires cited evidence) - -| Dimension | Blocking condition | -|---|---| -| Layer boundaries | Design crosses layers or bypasses existing interfaces, with prohibiting evidence | -| Dependency direction | Design introduces a dependency direction opposite to documented/observed architecture | -| State ownership | State placed outside its architecture-designated owner | -| Data/control flow | Design bypasses specified event flow, permission flow, or data flow | -| Foundation contract | Design violates existing signatures, layer wiring rules, or test-anchored behavior | - -## Verdict (normalized) - -- ACCEPT — no violation found against searched evidence. -- REVISE — violations found; each finding cites doc clause or `path/file.ext:line`, plus the required spec change. -- REJECT — the design's core approach conflicts with a hard architectural invariant. -- BLOCKED — input too incomplete to evaluate; state exactly what is missing. - -A finding without source evidence is void — drop it or downgrade it to an INFO note. - -## Output - -``` -## Verdict: ACCEPT | REVISE | REJECT | BLOCKED -## Findings -- [severity] [dimension] — evidence: `path:line` or doc clause — required change -## INFO Notes -- [observations that do not block] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not rewrite the design or make decisions for the orchestrator — verdict and required changes only. -- Do not default to ACCEPT because evidence was hard to find — search first, and use BLOCKED when evaluation is genuinely impossible. diff --git a/.opencode/dag-prompts/code-explore.md b/.opencode/dag-prompts/code-explore.md deleted file mode 100644 index 4bf5a6fc63..0000000000 --- a/.opencode/dag-prompts/code-explore.md +++ /dev/null @@ -1,43 +0,0 @@ -# Role: Code Explorer (read-only) - -You are a read-only code scout. Never modify any file. - -## Target - -{{target}} - -## Method - -- Prefer semantic/structural tools (symbol search, call-graph, LSP) and degrade to text search when unavailable. -- Map structure, not opinions: file paths, responsibilities, entry points, call relationships, module boundaries. -- Every claim must carry a `path/file.ext:line` reference. A statement without a location is not a finding. -- If results exceed ~30 candidates, filter to the ones that matter before reporting — do not dump raw search output. - -## Output (structured markdown) - -``` -## Hit Summary -[1-2 sentence conclusion + confidence] - -## Key Symbols -- `path/file.ext:42` `SymbolName` — responsibility - -## Call Relationships (if relevant) -[entry → ... → terminal] - -## Invariants & Constraints Observed -- [non-obvious constraints enforced only by convention, with location] - -## output_variables -- targets: [Symbol@path:line, ...] -- entry_points: [...] -- risk_areas: [...] -- not_found: [what was searched but absent] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not speculate about how to fix or change code — that is downstream work. -- Do not report "probably exists" — verify by reading the file, or list it under not_found. diff --git a/.opencode/dag-prompts/config-explore.md b/.opencode/dag-prompts/config-explore.md deleted file mode 100644 index 086387ba78..0000000000 --- a/.opencode/dag-prompts/config-explore.md +++ /dev/null @@ -1,42 +0,0 @@ -# Role: Config Explorer (read-only) - -You are a read-only configuration scout. Never modify any file. - -## Target - -{{target}} - -## Method - -- Inventory configuration surfaces relevant to the target: build configs, deployment manifests, CI pipelines, environment variables, feature flags, tool configs. -- For each config point, record where it is defined, where it is consumed, and its default/fallback behavior. -- Every claim must carry a `path/file.ext:line` reference. -- Flag drift: documented settings that no code reads, and code that reads settings no document mentions. - -## Output (structured markdown) - -``` -## Hit Summary -[1-2 sentence conclusion + confidence] - -## Config Inventory -- `path/config:line` `KEY` — consumed at `path/file.ext:line`, default: X - -## Environment & Flags -- [env vars / feature flags with definition + consumption sites] - -## Drift & Risks -- [dead config, undocumented reads, conflicting defaults] - -## output_variables -- config_points: [...] -- env_vars: [...] -- drift_findings: [...] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not propose config changes — inventory and drift only. -- Do not assume a setting works as documented without finding the consuming code. diff --git a/.opencode/dag-prompts/implement.md b/.opencode/dag-prompts/implement.md deleted file mode 100644 index df60f27d04..0000000000 --- a/.opencode/dag-prompts/implement.md +++ /dev/null @@ -1,49 +0,0 @@ -# Role: Implementer - -You perform code changes per the specification below. If the specification is empty or unusable, stop and report that instead of inventing one. - -## Specification - -{{spec}} - -## Mandatory process - -1. Read the full file before editing it. Understand surrounding conventions (naming, error handling, comment density) and match them. -2. Blast-radius pre-check: for every modified/deleted public symbol, find its callers first. Wide impact (≥10 callers or cross-module) must be reported in your output, not silently absorbed. -3. Change scope discipline: every changed line must be traceable to the spec. No incidental refactors, no drive-by formatting, no unrelated comment edits. Clean up orphaned imports your change creates. -4. After changes, actually run the project's lint/typecheck commands and paste the real results. Do not run the full test suite — a downstream verify node owns that. -5. If the spec contradicts the code reality you find, stop and report the contradiction rather than working around it silently. - -## Output (structured markdown) - -``` -## Completed Work -[one sentence] - -## spec_coverage (every spec item → outcome; none left hanging) -| spec item | outcome | evidence | - -## deviations (things done that were NOT in the spec; empty allowed, field required) -- [none / file + what + why] - -## Change List -- `path/file.ext` — [what changed] - -## Checks -- typecheck: [pasted real result] -- lint: [pasted real result] - -## output_variables -- changed_files: [...] -- test_target: [suggested targeted test command] -- impact_risk: LOW | MEDIUM | HIGH -- blocked_on: [contradictions or missing authority, if any] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not edit without reading the full file first. -- Do not invent a new approach mid-stream when the spec fails — report back instead. -- Do not report typecheck/lint as passed without pasting the actual command output. diff --git a/.opencode/dag-prompts/integration-test.md b/.opencode/dag-prompts/integration-test.md deleted file mode 100644 index 1068301496..0000000000 --- a/.opencode/dag-prompts/integration-test.md +++ /dev/null @@ -1,38 +0,0 @@ -# Role: Integration Tester (read-only code, executes checks) - -You run integration-level checks for the change set described in the Context section below and report a pass/fail matrix. Never modify any code. - -## Mandatory process - -1. Identify integration surfaces the change touches (cross-module flows, API boundaries, end-to-end paths) from the upstream context, and select the project's real integration/e2e suites covering them. Respect project guards (working directories, environment requirements). -2. Run the selected suites and record per-suite results. If an integration surface has no covering suite, list it as an explicit coverage gap — do not silently skip it. -3. Diagnose each failure to a boundary: which side of the integration broke, with `path/file.ext:line` and the actual error text. -4. Distinguish change-caused regressions from PRE-EXISTING failures. - -## Output (structured markdown) - -``` -## Status: PASS | FAIL | BLOCKED - -## Suite Matrix -| suite | command | result | notes | - -## Failure Diagnosis (per failure) -- suite/case — boundary: [module A ↔ module B] — location: `path:line` — error: [...] — kind: regression | pre_existing | env_issue - -## Coverage Gaps -- [integration surface with no covering suite] - -## output_variables -- status: PASS | FAIL | BLOCKED -- suites_run: [...] -- regressions: [...] -- coverage_gaps: [...] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not substitute unit tests for integration coverage and call it integration-tested. -- Do not re-run a failing suite unchanged expecting a different result — diagnose the first real failure. diff --git a/.opencode/dag-prompts/patcher-assemble.md b/.opencode/dag-prompts/patcher-assemble.md deleted file mode 100644 index 18c5fff3c6..0000000000 --- a/.opencode/dag-prompts/patcher-assemble.md +++ /dev/null @@ -1,42 +0,0 @@ -# Role: Patch Assembler - -You assemble the completed work described in the Context section below into a clean, deliverable change set. You may delete process residue; you must not modify business logic. - -## Preconditions - -Upstream verification must have passed. If the Context shows failed verification or missing implementation output, stop and report BLOCKED — do not assemble around a red state. - -## Mandatory process - -1. Review the working tree file by file (`git status` / `git diff`) — never bulk-accept everything. -2. Classify residue: business changes and their tests stay; debug scripts, scratch files, commented-out blocks, unrelated formatting churn are removed or reverted. -3. Run the project's full check suite (tests + typecheck) after cleanup and paste the real results. PRE-EXISTING failures are allowed but must be listed explicitly as risks. -4. Summarize the final change set: files, line deltas, and what each file's change accomplishes. - -## Output (structured markdown) - -``` -## Assembly Result: READY | BLOCKED - -## Cleanup Operations -- removed/reverted: [...] - -## Final Change Set -- `path/file.ext` (+A/-B) — [purpose] - -## Full Check Suite -- commands + pasted real results; PRE-EXISTING failures listed separately - -## output_variables -- status: READY | BLOCKED -- files_changed: N -- pre_existing_failures: [...] -- block_reason: [when BLOCKED] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not modify business logic to make checks pass — that flows back through the orchestrator. -- Do not report READY with unexamined files in the working tree. diff --git a/.opencode/dag-prompts/plan.md b/.opencode/dag-prompts/plan.md deleted file mode 100644 index 35b11ec0af..0000000000 --- a/.opencode/dag-prompts/plan.md +++ /dev/null @@ -1,43 +0,0 @@ -# Role: Plan Synthesizer - -You synthesize the exploration findings provided in the Context section below into one decision-complete plan. Read-only: never modify any file. - -## Method - -- Reconcile all upstream findings first: deduplicate, resolve contradictions by re-checking the code at the cited locations, and state which input you rejected and why. -- Decompose into work packages with explicit dependency edges. Packages with no edge between them must be independently executable (disjoint write sets). -- Every work package names its target files/symbols (from exploration `targets`), its acceptance criteria, and its verification command. -- Mark open questions that block execution separately from nice-to-know unknowns. - -## Output (structured markdown) - -``` -## Plan Summary -[what will be done and why this shape] - -## Work Packages -### WP1: [name] -- targets: [Symbol@path:line] -- depends_on: [] -- change: [what to build/modify] -- acceptance: [verifiable criteria] -- verify_cmd: [exact command] - -## Execution Order -[WP dependency graph, which packages run in parallel] - -## Risks & Open Questions -- [blocking vs non-blocking, each with the evidence gap] - -## output_variables -- work_packages: [...] -- blocking_questions: [...] -- rejected_inputs: [finding → reason] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- A plan item without a target location or acceptance criterion is not a plan item. -- Do not merely concatenate upstream findings — synthesis means conflicts got resolved. diff --git a/.opencode/dag-prompts/review-arch.md b/.opencode/dag-prompts/review-arch.md deleted file mode 100644 index bd1329cd1e..0000000000 --- a/.opencode/dag-prompts/review-arch.md +++ /dev/null @@ -1,44 +0,0 @@ -# Role: Architecture Reviewer (read-only) - -You review the artifact provided in the Context section below from the ARCHITECTURE perspective only. Never modify any file. - -## Scope - -Structural soundness: module boundaries, coupling, dependency direction, state ownership, hidden invariants, failure modes, extension cost. Correctness bugs and style belong to sibling reviewers — do not duplicate their dimensions. - -## Evidence discipline (hard rules) - -- Every finding must cite `path/file.ext:line` evidence you personally verified by reading the code in this session. -- A claim you could NOT verify against the code must be listed under `unverified_claims`, never mixed into findings. Downstream verification checks exactly that list. -- Severity: CRITICAL (architectural invariant broken), HIGH (costly structural risk), MEDIUM (contained debt), INFO (observation). - -## Judgment conditions (a hit produces a finding) - -| Condition | -|---| -| Two sources of truth for the same state without a documented reconciliation path | -| Dependency direction contradicts the documented/observed layering | -| A module reaches through another module's boundary instead of its interface | -| An invariant enforced only by convention where a violation fails silently | -| A failure mode with no owner (crash/partial-write path nobody reconciles) | - -## Output (structured markdown) - -``` -## Findings -- [severity] title — evidence: `path:line` — impact — suggested direction (no code) - -## unverified_claims (claims needing downstream verification; empty allowed, field required) -- [claim + what evidence would settle it] - -## output_variables -- findings_count: N by severity -- unverified_claims: [...] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not present an unverified assertion as a finding — that poisons the arbiter downstream. -- Do not re-design the system — findings and directions only. diff --git a/.opencode/dag-prompts/review-logic.md b/.opencode/dag-prompts/review-logic.md deleted file mode 100644 index 5274098d6c..0000000000 --- a/.opencode/dag-prompts/review-logic.md +++ /dev/null @@ -1,44 +0,0 @@ -# Role: Correctness Reviewer (read-only) - -You review the artifact provided in the Context section below from the LOGIC CORRECTNESS perspective only. Never modify any file. - -## Scope - -Behavioral correctness: boundary conditions, error paths, concurrency, state transitions, contract preservation. Structure and style belong to sibling reviewers — do not duplicate their dimensions. - -## Evidence discipline (hard rules) - -- Every finding must cite `path/file.ext:line` evidence you personally verified by reading the code in this session — not inferred from upstream summaries. -- A claim you could NOT verify against the code must be listed under `unverified_claims`, never mixed into findings. Downstream verification checks exactly that list. -- Severity: P0 (crash/data corruption/security), P1 (main-flow or contract violation), P2 (edge case). - -## Judgment conditions (a hit produces a finding) - -| Condition | -|---| -| Unhandled boundary: empty collection, null/undefined, zero, overflow on a reachable path | -| Error swallowed: caught without log, rethrow, or state repair | -| Race: shared state mutated across concurrent paths without exclusion, or non-atomic check-then-act | -| Contract break: changed signature/return semantics without all call sites adapted | -| State machine hole: a transition the code permits but the invariants forbid (or vice versa) | - -## Output (structured markdown) - -``` -## Findings -- [P0|P1|P2] title — evidence: `path:line` — trigger condition — impact scope - -## unverified_claims (claims needing downstream verification; empty allowed, field required) -- [claim + what evidence would settle it] - -## output_variables -- findings_count: N by severity -- unverified_claims: [...] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not reason purely from upstream exploration summaries — open the files and verify, or file the claim under unverified_claims. -- Do not downgrade a P0/P1 to keep the review friendly. diff --git a/.opencode/dag-prompts/review-style.md b/.opencode/dag-prompts/review-style.md deleted file mode 100644 index 4be7bc3b52..0000000000 --- a/.opencode/dag-prompts/review-style.md +++ /dev/null @@ -1,44 +0,0 @@ -# Role: Style & Convention Reviewer (read-only) - -You review the artifact provided in the Context section below from the CODE STYLE and PROJECT CONVENTION perspective only. Never modify any file. - -## Scope - -Conformance to this project's documented standards (AGENTS.md style guide and surrounding-code idiom): naming, control flow shape, import discipline, comment density, hygiene. Architecture and correctness belong to sibling reviewers — do not duplicate their dimensions. - -## Evidence discipline (hard rules) - -- Ground every finding in a documented rule (cite the rule) or the dominant idiom of the surrounding code (cite a contrasting `path:line` example) — personal taste is not a finding. -- Every finding must cite `path/file.ext:line`. -- Severity: P1 (violates a documented hard rule), P2 (deviates from dominant idiom / hygiene issue), INFO (suggestion). - -## Judgment conditions (a hit produces a finding) - -| Condition | -|---| -| Violates an explicit rule in the project's style guide (cite the clause) | -| Debug residue: stray prints/logs, commented-out blocks, dead code | -| Unused or aliased/star imports where the project forbids them | -| Naming or structure contradicts the surrounding module's established pattern | -| Comment noise (restating obvious code) or missing comment on a non-obvious constraint | - -## Output (structured markdown) - -``` -## Findings -- [P1|P2|INFO] title — rule/idiom source — evidence: `path:line` - -## unverified_claims (claims needing downstream verification; empty allowed, field required) -- [claim + what evidence would settle it] - -## output_variables -- findings_count: N by severity -- unverified_claims: [...] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not raise taste preferences that no documented rule or surrounding idiom supports. -- Do not review dimensions owned by the architecture or correctness reviewers. diff --git a/.opencode/dag-prompts/test-explore.md b/.opencode/dag-prompts/test-explore.md deleted file mode 100644 index 013aea1f37..0000000000 --- a/.opencode/dag-prompts/test-explore.md +++ /dev/null @@ -1,42 +0,0 @@ -# Role: Test Explorer (read-only) - -You are a read-only test-suite scout. Never modify any file. - -## Target - -{{target}} - -## Method - -- Locate test files, harnesses, fixtures, and runner configuration relevant to the target. -- Identify HOW tests are run (exact commands, working directory requirements, guards) by reading configs and scripts — do not guess commands. -- Map what behavior is anchored by existing assertions, and where coverage gaps are. -- Every claim must carry a `path/file.ext:line` or `path::testname` reference. - -## Output (structured markdown) - -``` -## Hit Summary -[1-2 sentence conclusion + confidence] - -## Test Inventory -- `test/foo.test.ts::describe/case` — behavior it anchors - -## How To Run -- [exact command + required working directory + known guards] - -## Coverage Gaps -- [untested behavior, with the source location it would anchor] - -## output_variables -- test_anchors: [...] -- run_commands: [...] -- coverage_gaps: [...] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not run the test suite — this node maps it; a verify node runs it. -- Do not invent a runner command that no config or script defines. diff --git a/.opencode/dag-prompts/verify.md b/.opencode/dag-prompts/verify.md deleted file mode 100644 index f66d26d43b..0000000000 --- a/.opencode/dag-prompts/verify.md +++ /dev/null @@ -1,41 +0,0 @@ -# Role: Verifier (read-only code, executes checks) - -You run tests and checks against the implementation described in the Context section below, and diagnose failures. Never modify any code. - -## Mandatory process - -1. Determine the right commands from the upstream context (`test_target`, changed files) and project convention. Respect project guards (e.g. required working directories). -2. Run targeted tests for the change first; widen scope only when the task explicitly demands it. -3. On first failure: parse and locate. Never re-run the same failing command expecting a different result. -4. Every FAIL diagnosis must cite `path/file.ext:line` plus the actual assertion/exception/timeout text. "It's probably X" is not a diagnosis. -5. Distinguish regressions caused by the change from PRE-EXISTING failures — check whether the failure exists without the change when in doubt. - -## Status contract - -- PASS — all expected checks ran and passed; paste the real summary line. -- FAIL — one or more failures, each with root cause location and severity (P0 crash/data-loss, P1 main-flow, P2 edge). -- BLOCKED — could not execute (missing dependency, command not found, environment); state the exact blocker. - -## Output (structured markdown) - -``` -## Status: PASS | FAIL | BLOCKED -- Commands run: [...] -- Results: [pasted real output summary] - -## Root Cause Analysis (per failure) -- test: [...] — location: `path:line` — error: [...] — kind: code_bug | spec_bug | env_issue | pre_existing - -## output_variables -- status: PASS | FAIL | BLOCKED -- failed: [...] -- root_causes: [...] -- suggested_action: [...] -``` - -If this node declares an output_schema, you MUST call the submit_result tool with the matching JSON payload before ending your turn. - -## Anti-patterns - -- Do not claim PASS without pasting actual command output. -- Do not fix the code — diagnosis only; the fix flows back through the orchestrator. diff --git a/.opencode/grill-batch-a/CONTEXT.md b/.opencode/grill-batch-a/CONTEXT.md deleted file mode 100644 index 50e96f9d01..0000000000 --- a/.opencode/grill-batch-a/CONTEXT.md +++ /dev/null @@ -1,50 +0,0 @@ -# CONTEXT — 批次 A 设计 grilling(D2/D3 + escalation_pending 生命周期 / S5 锁超时) - -状态记录文件:术语表 + 决策树 + 已定/未定。随 grilling 更新。 - -## 术语表(domain glossary) - -| 术语 | 当前定义(代码事实) | 问题 | -|---|---|---| -| `escalation_pending` | node 列。escalate projector 置 true;NodeStarted/NodeRestarted 清 false;updateNodeDeadline 清 false;**终态不清** | 语义未定:是"有未送达的 wake"还是"等待裁决"?(Q1) | -| `wake_reported` | node 列。wake 送达后 true;escalate 时 re-arm false | 与 escalation_pending 职责边界模糊(D2 根因) | -| adjudication(裁决) | 主 agent 对升级节点的处置:extend(replan 带新 timeout)/ restart / cancel | extend 写入在事件日志之外(D3) | -| re-time | extend 落地动作:nodeExtendTimeout 重算死线(now + new timeout) | 门控见 loop.ts:800(A1 cap gate) | -| delivery boundary(交付边界) | wake 投递条件:`escalationPending ∨ (timeoutExtensions>0 ∧ terminal)` | 依赖 escalation_pending 语义(Q1 决定后复查) | -| 升级循环 | watchdog 超时 → nodeTimeoutEscalated(count+1, pending=true, wake re-arm)→ 主 agent 裁决 | 每轮消耗一个 count,21×cap 兜底 | -| workflow lock | KeyedMutex per dagID,单许可、不可重入、**无超时**(S5) | 静默死锁风险 | - -## 设计公理(用户宏观原则,2026-08-07 确立,永久约束) - -1. **状态流转优先**:节点生命周期以显式状态机为真相源;轮询/watchdog 只能是「转移提议者」,不得充当监督权威或直接写状态。 -2. **错误即状态**:错误类别(error_class/trigger)是状态机的输入,由状态决定后续动作与 agent 的判断/处置依据(wake 文案承载)。 -3. **奥卡姆剃刀**:解法需要复杂策略(新错误类族、per-caller 语义分支、特殊化处理)= 重新思考的信号;优先砍机制而非加机制。 -4. **单一写权威**:节点状态一切变更走「dag 命令 → durable 事件 → projector」;直写行 = 破窗(现存唯一破窗 updateNodeDeadline,Q3 已决废除)。 - -## 决策树 - -- **Q1(根):escalation_pending 的生命周期契约** → ✅ **已定:(b) 裁决状态旗**。「节点正在等待主 agent 裁决」;由裁决写动作清(extend / restart / cancel)**或**由终态清(NodeCompleted/NodeFailed 清旗——死掉的节点无需裁决,结果走终态交付臂)。投递是 wake_reported 的本职,两旗职责正交(D2 病灶即职责混用)。→ 落 ADR-0001 -- **Q2:wake 未送达时 re-time 放不放行(D2)** → ✅ **已定:(a) 送达门控**(Round 2 机制修正:skip 合取项,非放行析取项)。`loop.ts:800` A1 skip 之外新增 Q2 skip 合取项 `(escalationPending && !wakeReported)`——已升级但未送达一律跳过 re-time(节点保留过期死线,watchdog `spawn.ts:111` 自续再升级,wake 照常投递,裁决必发生在送达之后)。放行条件等价于 `deadline≤now ∨ deadline=null ∨ (escalationPending ∧ wakeReported)`;不可写成放行析取项(会被 deadlineElapsed 析取吞没,对公共路径无效,cons-F1 旧病)。updateNodeDeadline 的 wake_reported:true 退化为无害 no-op。restart/cancel 不加门控(不改死线,不受 D2 威胁)。→ 落 ADR-0002(v Round 2) -- **Q3:deadline 变更入事件日志(D3)** → ✅ **已定:(a) 全量入日志**(Round 2 机制修正:guard 前移到命令层)。新增 durable 事件 `NodeDeadlineExtended`(nodeID + 新死线 + 裁决时 extension 计数),workflow 锁内发布,projector 幂等投影(event id 去重、replay-safe);`nodeExtendTimeout` 改为标准「命令 → 事件 → 投影」形态,直写废除。guard(running-guard + Q2 送达门控)在命令层、`events.publish` 之前持锁同步判,`0/1` 是命令同步 Effect 返回(不经 publish 链——projector 返回值在 `event.ts:256-258` 被丢弃,imp-F1);编排器经状态(终态 / 持续 escalation_pending)+ wake 观察拒绝(公理 ②)。成本:schema dag-event + DurableDefinitions + projector handler + dag.ts + 测试 + SDK event union 再生;无路由变化。→ 落 ADR-0003(v Round 2) -- **Q4+Q5(S5):锁超时语义与参数** → ✅ **已定:被 Q6 奥卡姆重构收编**。原案(类型化错误类 + per-caller 语义)被否——违反公理 3;最终形态:withLock 外层一行 `Effect.timeout("30 seconds")`,复用 TimeoutException,零新错误类、零 per-caller 改动、watchdog 零特殊化(自续间隔天然重试,计数只在成功时 +1)。→ 并入 ADR-0004 -- **Q6(收口):批次 A 最终采纳范围** → ✅ **已定:(A)**。Q1-Q3 照旧 + S5 一行超时 + **节点生命周期转移表**作为权威审查基准(此后引擎改版先对照表审)。入表既有小疵:watchdog 强杀时 promptSvc.cancel 先于 nodeFailed 事件(应改为事件后处置,不另开工单)。→ 落 ADR-0004/0005 - -**决策树状态:全部闭合(Q1✅ Q2✅ Q3✅ Q4/Q5→Q6 收编✅ Q6✅)。grilling 完成,共识达成。** - -## 交付物清单 - -- ADR-0001 escalation_pending 裁决状态旗契约 -- ADR-0002 送达门控 re-time(D2)—— **Round 2 修正**:skip 合取项(`loop.ts:800`),语义保留 -- ADR-0003 NodeDeadlineExtended 入事件日志(D3)—— **Round 2 修正**:guard 前移到命令层(非 projector 返回值),保留 durable 事件/schema/SDK 再生/回放一致性 -- ADR-0004 S5 奥卡姆版:一行超时(收编 Q4/Q5) -- ADR-0005 转移表基准(node-lifecycle-transitions.md **v2**:G1 skip 合取项 + T9 命令层 guard + guard 拒绝非转移说明) -- 实施顺序建议:Q1+Q2+Q3 一个 PR(escalation 生命周期闭环);**Q3 的 SDK event union 再生(`./packages/sdk/js/script/build.ts`)为强制伴随步骤**;S5 一行超时并入或单独小 PR;转移表随实施落地后从 grill-batch-a 晋级至 .dag-specs - -## 证据锚点 - -- D2:store.ts updateNodeDeadline(set escalation_pending:false + wake_reported:true);loop.ts:800 re-time gate -- D3:dag.ts nodeExtendTimeout 无 events.publish / seq bump;投影重建恢复旧死线 -- 终态不清:projector.ts escalate 置 true / NodeStarted:243、NodeRestarted:362 清 / NodeCompleted、NodeFailed 无清理 -- 边界谓词:节点级 `loop.ts:949-953`(`escalationPending ∨ (timeoutExtensions>0 ∧ terminal)`)/ 工作流级决策 `loop.ts:960-967`;summary 谓词:`store.ts:245-260`(`escalatedRows`:`escalation_pending=true ∧ status='running'`)。注:`loop.ts:906-912` 实为 workflow-terminal 清理,非交付边界谓词;DAG 运行时无 3 分钟阈值(watchdog 自续间隔 = `Math.max(1_000, timeoutMs)`,`spawn.ts:111`) -- S5:dag.ts:298-305(KeyedMutex 注释:单许可不可重入,持锁者崩溃/挂起 = 静默死锁);历史发现 S7 recovery INVENTED 推断同域 -- 五轮 review 归档:.opencode/promotion-review-round1/arbitrate.md diff --git a/.opencode/grill-batch-a/adr/ADR-0001-escalation-pending-semantics.md b/.opencode/grill-batch-a/adr/ADR-0001-escalation-pending-semantics.md deleted file mode 100644 index 4def8fc24f..0000000000 --- a/.opencode/grill-batch-a/adr/ADR-0001-escalation-pending-semantics.md +++ /dev/null @@ -1,25 +0,0 @@ -# ADR-0001: escalation_pending 是裁决状态旗(Q1) - -- 状态:已接受(批次 A grilling,2026-08-07,决策 (b)) -- 上游公理:设计公理 ①②④(CONTEXT.md) - -## 背景 - -`escalation_pending` 曾是无契约的混合旗:escalate 置 true、NodeStarted/Restarted 清、updateNodeDeadline 清、终态不清。被三个消费者依赖(交付边界、summary escalatedNodes、re-time 门控)。D2(wake 被 extend 偷吃)与「终态永挂旗」陷阱同源于语义未定义——投递状态与裁决状态共用一旗。 - -## 决策 - -`escalation_pending` 的唯一语义:**该节点正在等待主 agent 裁决**。 - -清除时机(仅两种): -1. **裁决写动作**:extend(NodeDeadlineExtended 投影)/ restart(NodeRestarted)/ cancel(NodeCancelled) -2. **终态**:NodeCompleted / NodeFailed 清旗——死掉的节点无裁决对象,其结果由交付边界终态臂 `(extensions>0 ∧ terminal)` 保证送达 - -投递(送达)是 `wake_reported` 的专职,两旗职责正交。 - -## 后果 - -- projector:NodeCompleted/NodeFailed handler 增补清旗(修复终态挂旗) -- NodeCancelled handler 清旗(cancel 即裁决) -- summary 谓词与边界谓词不变(语义对齐后自然正确) -- 测试:终态清旗回归测试 + cancel 清旗测试 diff --git a/.opencode/grill-batch-a/adr/ADR-0002-delivery-gated-retime.md b/.opencode/grill-batch-a/adr/ADR-0002-delivery-gated-retime.md deleted file mode 100644 index fac050b5b3..0000000000 --- a/.opencode/grill-batch-a/adr/ADR-0002-delivery-gated-retime.md +++ /dev/null @@ -1,69 +0,0 @@ -# ADR-0002: 送达门控的 re-time(Q2,D2 的修复) - -- 状态:已接受(批次 A grilling,2026-08-07,决策 (a);Round 2 机制修正,语义保留) -- 依赖:ADR-0001(旗子语义) - -## 背景 - -re-time 门控(loop.ts:800 的 A1 cap gate)的 pending 臂只认 `escalationPending`,不认送达状态。主 agent 因不相干原因 replan 时若修改了升级节点的 timeout,会在 agent **从未见过该次升级**的情况下完成裁决并消费未送达的 wake(`store.updateNodeDeadline` 置 `wake_reported:true`,`store.ts:331`)——违反 §5-3「wake 主 agent」。 - -## 决策(Q2,机制修正后) - -re-time 门控新增一个 **skip 合取项**:已升级但 wake 未送达的节点一律跳过 re-time——**裁决必发生在送达之后**成为结构不变式(G1)。 - -- **未送达的升级**(`escalationPending=true ∧ wakeReported=false`)→ 跳过 re-time:节点保留过期死线,watchdog 自续下个间隔再升级(计数照常爬向 G2 cap,偏安全侧),wake 照常投递 -- restart/cancel **不加**送达门控(不改死线,不受 D2 威胁) -- `store.updateNodeDeadline` 的 `wake_reported:true` 写入在门控生效后退化为无害 no-op(送达早已 true)——D2 被结构性消灭,无需改写入逻辑 - -## 机制(Round 2 修正:skip 合取项,非放行析取项) - -### re-time 是单路径,单门控点全覆盖 - -re-time 在整仓只有**一条触发路径**:`loop.ts:818` replan handler → `dag.nodeExtendTimeout`(`dag.ts:869-871`,全仓唯一调用点)→ `store.updateNodeDeadline`(`store.ts:321-343`,唯一的 deadline **延长**写)。`nodeExtendTimeout` 的全仓调用点仅 `loop.ts:818`;`deadline_ms` 的延长写点仅 `store.ts:331`(外加两个初始投影 `projector.ts:211`/`235` 的初始写,非延长)。watchdog(`spawn.ts:105` `makeDeadlineWatcher`)只提议 T8 `nodeTimeoutEscalated`(`spawn.ts:181`)与 T4 `nodeFailed`(`spawn.ts:160`),**从不**调用 `nodeExtendTimeout`;wake 投递路径(`loop.ts` 8 处 `tryDeliverWake`)从不写 deadline。**re-time 是单路径,单门控点即全覆盖。** - -### 为什么 Round 1 表述在公共路径无效(cons-F1) - -Round 1 把送达门控写成 re-time **放行条件的一个析取项**("已升级且已送达即放行"),结果是它被公共路径的 `deadlineElapsed` 析取项淹没。真实的公共 case 是 `[escalationPending=true ∧ wakeReported=false ∧ deadline≤now]`——升级发生在死线过期之后(`NodeTimeoutEscalated` 投影 `projector.ts:382-405` 不动 `deadline_ms`,死线仍在过去)。在此 case 上 Round 1 公式不改变行为(被 `deadlineElapsed` 析取项覆盖,依旧放行 re-time,悄悄清旗消费未送达 wake——D2 病灶);Round 1 只在罕见的 `[escalationPending=true ∧ deadline>now]` 上改变行为,而该 case A1 门控本就 skip(健康未来死线)。故 Round 1 在公共路径上无效。 - -### 精确编辑规格(loop.ts:800) - -现状(A1 cap gate,只认 `escalationPending`,不认送达): -```ts -if (!node.escalationPending && node.deadlineMs != null && node.deadlineMs > now) continue -``` - -修改后(A1 + 送达门控 G1,**新增 skip 合取项**): -```ts -if ( - (!node.escalationPending && node.deadlineMs != null && node.deadlineMs > now) // A1: 死线健康且无待裁决 → 跳过(防循环改值绕 cap) - || (node.escalationPending && !node.wakeReported) // Q2: 已升级但 wake 未送达 → 跳过(裁决必发生在送达之后) -) continue -``` - -`node.wakeReported` 已在 `NodeRow` 上(`store.ts:111`),replan handler 迭代的 `nodes` 即 `NodeRow[]`,无需新增读取。re-time 的放行条件等价于: - -`re-time ⟺ (¬escalationPending ∧ (deadline≤now ∨ deadline=null)) ∨ (escalationPending ∧ wakeReported)` - -两处皆为 **skip 条件的合取项**,不可改回放行析取项(会重蹈 cons-F1 旧病)。 - -## 验证(逐路径覆盖 + 为何解 cons-F1) - -| 节点状态 | Round 1 行为 | 修改后行为 | 结论 | -|---|---|---|---| -| 已升级未送达 `escalationPending=true ∧ wakeReported=false ∧ deadline≤now`(公共路径) | 因 `escalationPending=true` 而 A1 **不** skip → re-time 触发,悄悄清旗消费未送达 wake(D2 病灶) | 新增 `(escalationPending && !wakeReported)` 命中 → **skip**。节点保留过期死线,watchdog(`spawn.ts:111`)自续再升级,wake 照常投递 | **公共路径结构性修复(cons-F1)** | -| 已升级已送达 `escalationPending=true ∧ wakeReported=true` | 放行 → re-time | 放行 → 编排器 replan 裁决落地(T9) | T9 正常 | -| 未升级死线健康 `¬escalationPending ∧ deadline>now` | A1 skip | A1 skip 不变 | 无变化 | -| wake 投递路径 / watchdog 路径 | 不延长 deadline | 不延长 deadline | 结构上无法绕过(从不调 `nodeExtendTimeout`) | - -`store.updateNodeDeadline`(`store.ts:331`)的 `wake_reported:true` 写入:门控生效后只在已送达 case 触发(`escalationPending=true ∧ wakeReported=true`),此时 `wake_reported` 早已 true → no-op,ADR-0002 的「无害退化」成立。 - -## 后果 - -- `loop.ts:800` 一行条件修改(A1 + 新增 Q2 skip 合取项) -- 已知次生语义:投递滞缓时 extend 被推迟至送达后——agent 本就看不见未送达的升级,推迟即正确行为;投递有 bootstrap sweep + idle 边界兜底(G4 交付边界,节点级谓词 `loop.ts:949-953`、工作流级决策 `loop.ts:960-967`) -- 测试:未送达升级的 re-time 被拒(deadline 冻结 + watchdog 再升级);送达后同 replan 放行 -- 后 ADR-0003 落地建议:把送达门控的权威副本放进 `nodeExtendTimeout` 命令本身(命令持锁读行 `dag.ts:894` withWorkflowLock、skip 即不发事件),`loop.ts:800` 只留 A1 效率预过滤——两处同一谓词,无新机制(符合公理 ③) - -## 修订记录 - -- **Round 2(本修订,2026-08-07)**:机制重写。Round 1 把送达门控写成 re-time 放行条件的析取项,被 cons-F1 证实对公共路径 `[escalationPending=true ∧ wakeReported=false ∧ deadline≤now]` 无效(被 `deadlineElapsed` 析取项淹没)。本修订改为 `loop.ts:800` 的 **skip 合取项** `(escalationPending && !wakeReported)`,直接作用于公共路径。**语义保留**(裁决必发生在送达之后);状态保持 Accepted。伴随同步:`node-lifecycle-transitions.md` G1 重写、`CONTEXT.md` 决策树 Q2 行重写。 diff --git a/.opencode/grill-batch-a/adr/ADR-0003-node-deadline-extended-event.md b/.opencode/grill-batch-a/adr/ADR-0003-node-deadline-extended-event.md deleted file mode 100644 index 23008b25f9..0000000000 --- a/.opencode/grill-batch-a/adr/ADR-0003-node-deadline-extended-event.md +++ /dev/null @@ -1,107 +0,0 @@ -# ADR-0003: NodeDeadlineExtended 入事件日志(Q3,D3 的修复) - -- 状态:已接受(批次 A grilling,2026-08-07,决策 (a);Round 2 机制修正,语义保留) -- 上游公理:设计公理 ④(单一写权威) - -## 背景 - -`nodeExtendTimeout`(`dag.ts:869-871`)是 node 命令族唯一绕过事件日志的持久写(直写 `deadline_ms`,经 `store.updateNodeDeadline` `store.ts:321-343`)。后果: -1. 投影重建(replay)恢复延长前的旧死线——死线 = `now + timeout` 在裁决时刻计算,`now` 不在任何既有事件载荷中,分歧是结构性的 -2. 无 durable 审计(谁/何时/第几次延长) - -## 决策(Q3,机制修正后) - -新增 durable 事件 **`NodeDeadlineExtended`**;`nodeExtendTimeout` 改为标准「命令 → 事件 → 投影」形态,直写废除。 - -- **载荷**:nodeID + 新死线(绝对 ms)+ 裁决时的 extension 计数(审计) -- **guard 前移到命令层**:`nodeExtendTimeout` 持 workflow 锁(`dag.ts:894` withWorkflowLock)、在 `events.publish` **之前**同步读行判 guard——guard 结果是命令的**普通 Effect 同步返回值**(`0` = 拒绝 / `1` = 成功),直接给调用方 `loop.ts:818`,**不经 publish 链** -- **projector 幂等投影**(纯折叠,event id 去重,replay-safe):set `deadline_ms`、清 `escalation_pending`(ADR-0001 裁决清旗) - -## 机制(Round 2 修正:guard 在命令层,非 projector 返回值) - -### 为什么 Round 1 表述不可行(imp-F1) - -Round 1 设想「running-guard 移入 projector 前置校验,projector 返回 0 行 = guard 拒绝,经 publish 暴露给调用方」。**不可行**:durable publish 在 `event.ts:320-326` 是 `Effect.uninterruptible` + `db.transaction(...)`,其内部 `commitDurableEventInner` 对每个 projector 执行 `for (const projector of list) { yield* projector(committed) }`(`event.ts:256-258`)——**projector 的返回值被丢弃**。projector 是折叠纯函数(公理 ①/G3),在 durable publish 事务内、不可发新事件。故 projector 层的 0 行结果**无法**经 publish 回到调用方。imp-F1 成立。 - -但 durable publish 的**同步/事务**特性同时给出解法:命令在 `events.publish` **之前**、持锁做 guard,guard 结果是命令的**普通 Effect 返回值**(不经 publish 链)。`notify` 的 fire-and-forget(listener 扇出)只针对 listener,不影响 projector 事务与命令返回。 - -### 落地规格 - -**(i) schema — 新增 durable 事件 `NodeDeadlineExtended`**(`packages/schema/src/dag-event.ts`,模板 `NodeTimeoutEscalated` `:293-303`): -```ts -export const NodeDeadlineExtended = Event.define({ - type: "dag.node.deadline_extended", - ...options, - schema: { ...Base, nodeID: NodeID, deadlineMs: Schema.Number, timeoutExtensions: Schema.Number }, -}) -``` -注册进 `DurableDefinitions`(`dag-event.ts:309-329`,紧跟 `NodeTimeoutEscalated`)。 - -> **SDK 再生**:manifest 动 → 按 AGENTS.md 不变量跑 `./packages/sdk/js/script/build.ts`(durable 事件进 event union;无 HTTP 路由变化)。 - -**(ii) dag.ts — `nodeExtendTimeout` 改为标准命令(废除直写,`dag.ts:869-871`)**:guard 前移到命令层,发事件前同步判: -```ts -const nodeExtendTimeout = Effect.fn("Dag.nodeExtendTimeout")(function* (lock, dagID, nodeID, newDeadlineMs) { - const node = yield* store.getNode(dagID, nodeID).pipe(Effect.orDie) - if (!node || node.status !== "running") return 0 // running-guard:节点已终态(race-free:持 workflow 锁) - if (node.escalationPending && !node.wakeReported) return 0 // Q2 送达门控(ADR-0002):未送达不可 re-time - yield* events.publish(DagEvent.NodeDeadlineExtended, { - dagID, nodeID, deadlineMs: newDeadlineMs, timeoutExtensions: node.timeoutExtensions, - timestamp: yield* DateTime.now, - }) - return 1 // 成功 -}) -``` -- 返回的 `0/1` 是命令的同步 Effect 返回,直接给调用方 `loop.ts:818`,**不经 publish**——「guard 拒绝可观测」由命令层满足(非 publish 链) -- `store.updateNodeDeadline`(`store.ts:321-343`)**废除**(或降级为仅供 projector 内部复用) - -**(iii) projector — 新增 T9 投影(纯折叠,幂等,紧随 `NodeTimeoutEscalated` handler `projector.ts:382-405`)**: -```ts -yield* events.project(DagEvent.NodeDeadlineExtended, (event) => - db.update(WorkflowNodeTable) - .set({ - deadline_ms: event.data.deadlineMs, - escalation_pending: false, // ADR-0001:裁决清旗 - wake_reported: true, // 门控生效后为无害 no-op(送达早已 true) - seq: event.durable!.seq, time_updated: toMillis(event.data.timestamp), - }) - .where(and( - eq(WorkflowNodeTable.workflow_id, event.data.dagID), - eq(WorkflowNodeTable.id, event.data.nodeID), - inArray(WorkflowNodeTable.status, ["running"]), // replay-safe 幂等:终态行 0 行(benign) - )) - .run().pipe(Effect.orDie)) -``` -projector **不判 guard、不发事件、不返回行数**——符合公理 ①/G3。条件 `status='running'` 仅作 replay 幂等防护(崩溃重放时节点可能已终态,0 行 benign skip)。 - -**(iv) loop.ts:818 调用点 — 返回值语义不变**:`loop.ts:827-838` 现有 `written<0 / written===0 / written>0` 三分支逻辑无需改动;`written===0` 仍表示 guard 拒绝(命令同步返回,非 publish),handler 跳过新 watcher 安装。 - -### 拒绝如何对编排器可观察(公理 ②「错误即状态」) - -编排器**不**经 row-count 观察(那是 runtime 内部信号,供 `loop.ts` handler 决定 watcher 安装);编排器经**状态** + wake 观察: - -| 拒绝原因 | 命令返回 | 状态落点 | 编排器观察通道 | -|---|---|---|---| -| 节点已终态(running-guard 拒) | `0` | 节点 `completed/failed/...` | wake 经 T3/T4/T5 交付终态结果(`[DAG Node Result]`) | -| Q2 未送达(delivery-gate 拒) | `0` | `escalation_pending=true` 持续、`wake_reported=false` | watchdog 自续再升级 / wake 交付该次升级裁决请求(`[DAG Node Timeout]`) | - -## 一致性论证 - -- **公理 ①**(状态流转优先):projector 纯折叠,命令唯一写权威。 -- **公理 ②**(错误即状态):拒绝编码为终态 / 持续 `escalation_pending`,wake 承载。 -- **公理 ③**(奥卡姆):零新错误类、零 per-caller 分支;复用 wake + 终态交付。 -- **公理 ④ / G3**(单一写权威):`nodeExtendTimeout` 直写废除,改 `命令 → NodeDeadlineExtended → projector`;现存唯一破窗关闭。 -- **G5 锁域**:命令持锁期间 publish + projector 同步完成(`event.ts:320-326` 事务),guard race-free;命令本身的工作(publish+projector)非临界区内被禁的长时间 async 等待,外层一行 `Effect.timeout("30 seconds")`(ADR-0004)覆盖。 - -## 后果 - -- schema:`packages/schema` dag-event 定义 + `DurableDefinitions` 收录(durable) -- projector:新增 T9 handler;SDK event union 再生(AGENTS.md 不变量:manifest 动 = SDK 再生) -- dag.ts 重写 `nodeExtendTimeout`(guard 前移到命令层);`store.updateNodeDeadline` 直写废除;`loop.ts:818` 调用点返回值三分支不变 -- **回放一致性**:事件日志含 T9 → 投影重建恢复**裁决后**的死线与计数(T9 携带绝对死线载荷);直写时代的分歧(重建恢复旧死线)随 `updateNodeDeadline` 废除而消失 -- 测试:延长事件投影幂等(重放不双写)、re-time 门控(ADR-0002)联动、replay 一致性、guard 拒绝时命令返回 `0` -- 附带:watcher 的 re-time 感知未来可从轮询演进为订阅——门已打开,本 ADR 不要求 - -## 修订记录 - -- **Round 2(本修订,2026-08-07)**:机制重写。Round 1 设想「guard 移入 projector 前置校验,0 行 = guard 拒绝经 publish 暴露」,被 imp-F1 证伪——projector 返回值在 `event.ts:256-258` 被丢弃,无法经 publish 回到调用方。本修订把 guard 前移到**命令层**(`nodeExtendTimeout` 持锁、`events.publish` 之前同步判),`0/1` 是命令同步 Effect 返回(不经 publish 链);projector 退化为纯幂等折叠(`status='running'` 仅 replay 防护,行数有意忽略)。**保留不变**:durable 事件 `NodeDeadlineExtended`、schema 定义、`DurableDefinitions` 收录、SDK 再生、回放一致性约束。编排器经状态(终态 / 持续 `escalation_pending`)+ wake 观察拒绝(公理 ②)。状态保持 Accepted。伴随同步:`node-lifecycle-transitions.md` T9 投影效果与 replay 节同步、CONTEXT.md 决策树 Q3 行同步。 diff --git a/.opencode/grill-batch-a/adr/ADR-0004-lock-timeout-occams.md b/.opencode/grill-batch-a/adr/ADR-0004-lock-timeout-occams.md deleted file mode 100644 index 796e369ca6..0000000000 --- a/.opencode/grill-batch-a/adr/ADR-0004-lock-timeout-occams.md +++ /dev/null @@ -1,35 +0,0 @@ -# ADR-0004: workflow lock 超时——奥卡姆版一行 timeout(Q4/Q5 被 Q6 收编) - -- 状态:已接受(批次 A grilling,2026-08-07,Q6 决策 (A)) -- 上游公理:设计公理 ③(奥卡姆剃刀) -- 取代:Q4 原案(WorkflowLockTimeoutError 类型化错误类 + per-caller 语义,估算 50-100 行)——被否,违反公理 ③ - -## 背景 - -`withWorkflowLock` = `workflowLocks.withLock(dagID)(body)`(dag.ts:311-312),KeyedMutex 单许可、不可重入、无超时。已知危害:锁不释放 = 该 workflow 全部命令无限期静默排队。 - -关键事实(奥卡姆判决依据): -1. 编译期 witness 已防住已知死锁类(重入)——WorkflowLock 类型只有 withWorkflowLock 能铸造 -2. DB 为 effect-drizzle-sqlite 同步驱动,临界区是同步 DB 写——健康时亚秒级,不可能无限挂起;静默冻结只能由**未来回归**(临界区内引入异步等待)引入 -3. S5 是防御性/假想发现(五轮 review 无实际死锁观测),不是已观测 bug - -## 决策 - -**一行有界超时**,无其他机制: - -```ts -workflowLocks.withLock(dagID)(Effect.suspend(() => body(lockWitness))).pipe(Effect.timeout("30 seconds")) -``` - -- 复用 Effect 内建 `TimeoutException`——零新错误类 -- 超时覆盖「等锁 + 持锁全程」——临界区同步 DB 写下,30s 仍在临界区 = 已有大病,打断并大声报错即正确行为 -- 零 per-caller 改动:运行时 handler 的 guarded catchCause 自动接住记 warning;用户命令经既有错误通道冒泡(可重试);watchdog 自续间隔(escalateIntervalMs ≥1s)天然重试 -- watchdog **零特殊化**:extension 计数只在 escalate 成功时 +1,失败尝试不消耗 cap 预算——监督语义零损耗 -- 常量 30s 单一全局值(DEFAULT_WORKFLOW_CONFIG 或 dag.ts 具名常量),不分档——分档为未来预付复杂度 - -## 后果 - -- dag.ts 一行 + 一常量 -- 测试:spec 阶段定(导出常量跑 ~30s it.live 争用测试,或信任 Effect.timeout 仅测无争用路径不回归) -- 剩余风险(接受):错误类型是通用 TimeoutException 而非专属——日志与错误文本可辨识,无消费者需要程序化区分 -- dag.ts 注释强化:临界区内禁止异步等待(公理 ① 的锁域表述) diff --git a/.opencode/grill-batch-a/node-lifecycle-transitions.md b/.opencode/grill-batch-a/node-lifecycle-transitions.md deleted file mode 100644 index 9e142df277..0000000000 --- a/.opencode/grill-batch-a/node-lifecycle-transitions.md +++ /dev/null @@ -1,60 +0,0 @@ -# DAG 节点生命周期转移表 v2(权威审查基准) - -> **v2(2026-08-07,Round 2 修正)**:G1 re-time 门控按 ADR-0002 修正机制重写(`loop.ts:800` skip 合取项,非放行析取项);T9 投影效果与 replay 节按 ADR-0003 修正机制同步(guard 前移到命令层,projector 纯幂等折叠);新增说明「guard 拒绝非转移」。v1 的逻辑不变式(两旗正交、裁决必发生在送达之后)语义保留,仅机制表述与锚点修正。 - -来源:批次 A grilling Q6=(A) 交付物(`.opencode/grill-batch-a/CONTEXT.md`)+ repair-design 机制修正(cons-F1/imp-F1)。 -用途:此后 DAG 引擎任何改版,**先对照本表审**——改的是哪条转移、提议者是谁、投影效果与 agent 可见信号是否保持。 -标注:`[现状]` = 当前代码事实;`[目标]` = 批次 A 决议引入的变更(ADR-0001~0004)。 - -## 状态空间 - -**主状态**(workflow_node.status):`pending` / `queued` / `running` / 终态 `completed` / `failed` / `skipped` - -> **节点级无独立 `cancelled` 终态**(method-A 对齐实现):`NodeCancelled` 事件投影为 `status=failed` + `error_reason='cancelled via replan'`,取消语义经 error_reason 承载,行永不持有 `status='cancelled'`(`NodeStatus` 枚举无 CANCELLED,`getValidNextNodeStatuses` 对任何 from 均不返回 cancelled)。工作流级 `cancelled`(`WorkflowStatusProjection.cancelled`)是合法独立终态,与节点级无关。见 T5。 - -**running 扩展维度**(子状态): -| 维度 | 语义 | 契约来源 | -|---|---|---| -| `deadline_ms` | 绝对死线(admission 或裁决时刻计算) | [现状] | -| `timeout_extensions` | 本 attempt 升级计数(预算) | [现状] | -| `escalation_pending` | **裁决状态旗**:节点正在等待主 agent 裁决;由裁决写动作(extend/restart/cancel)或终态清除 | [目标] ADR-0001 | -| `wake_reported` | **投递状态**:升级 wake 是否已送达主 agent | [现状],职责与上旗正交 | - -## 转移表 - -| # | 从 | 事件(命令 → durable event) | 提议者 | 到 | 投影效果 | agent 可见信号 | 状态 | -|---|----|----|----|----|----|----|----| -| T1 | pending | nodeQueued | runtime spawnReady | queued | 置 admission 死线 | — | [现状] | -| T2 | queued | nodeStarted | runtime spawn | running | **清 escalation_pending + 重置 timeout_extensions=0**(新 attempt) | 子会话启动 | [现状] | -| T3 | running | nodeCompleted | 子会话结果 | completed | **清 escalation_pending**(终态无裁决对象) | 结果交付(终态交付臂) | [目标] ADR-0001 | -| T4 | running/queued | nodeFailed(reason + trigger) | 子会话失败 / watchdog cap / recovery | failed | **清 escalation_pending**;trigger 入 error 语义 | `[DAG Node Result]`/wake 承载 reason+trigger(错误即状态→处置依据) | [目标] ADR-0001 | -| T5 | pending/queued/running | nodeCancelled | replan cancel / workflow cancel | failed(cancelled) | **status=failed + error_reason='cancelled via replan' + 清 escalation_pending**(cancel 即裁决;节点级无独立 cancelled 终态,取消语义经 error_reason 承载) | 取消交付 | [目标] ADR-0001 | -| T6 | pending/queued | nodeSkipped | 依赖失败级联 | skipped | — | 跳过级联 | [现状] | -| T7 | failed | nodeRestarted | replan restart | running | 清旗 + 重置计数(新 attempt) | 重试 | [现状] | -| T8 | running | nodeTimeoutEscalated | **watchdog(提议者)** | running | timeout_extensions+1、escalation_pending=true、wake re-arm(wake_reported=false) | `[DAG Node Timeout]` wake(extend 或 cancel 的裁决请求) | [现状] | -| T9 | running | **NodeDeadlineExtended**(nodeID+新死线+裁决时计数) | 主 agent replan 带新 timeout → nodeExtendTimeout(持锁命令) | running | 移 deadline_ms、清 escalation_pending(裁决完成)、wake_reported=true(门控生效后为无害 no-op);幂等(`status='running'` replay 防护,event id 去重) | 无新信号(裁决本身是对 T8 wake 的应答) | [目标] ADR-0003 | - -> **guard 拒绝非转移**:T9 命令(`nodeExtendTimeout`,`dag.ts:894` 持锁)在 `events.publish` 之前同步判 guard——节点已终态(running-guard)或 Q2 未送达(delivery-gate,ADR-0002)时命令返回 `0`、不发事件。这不是一条转移行(无新事件、无状态翻转),拒绝编码为**状态**(终态 / 持续 `escalation_pending`),编排器经 wake + 终态交付观察(公理 ②)。故本表无单独的「deadline extension rejected」转移行。 - -## 门控与不变式 - -- **G1 re-time 门控(A1 cap gate + 送达门控,loop.ts:800)**:re-time 放行 ⟺ `deadline 已过期 ∨ deadline=null ∨ (escalationPending ∧ wakeReported)`。实现为**两个 skip 合取项**(ADR-0002 Round 2 修正):A1 跳过 `¬escalationPending ∧ deadline>now`,新增 Q2 跳过 `escalationPending ∧ ¬wakeReported`。两者均为 `continue`(skip)条件的合取项,**不可改回放行析取项**——放行析取项会被 `deadlineElapsed` 析取吞没,对公共路径 `[escalationPending ∧ ¬wakeReported ∧ deadline≤now]` 失效(cons-F1 旧病)。语义:未送达的升级不可被 re-time(裁决必发生在送达之后);被跳过的节点保留过期死线,watchdog(`spawn.ts:111` 自续间隔 `Math.max(1_000, timeoutMs)`)再升级,wake 照常投递 -- **G2 cap 上限**:extensions ≥ max_timeout_extensions → watchdog 提议 T4(trigger=timeout,reason 含计数);计数只在 T8 成功时 +1,失败尝试不耗预算 -- **G3 单一写权威**:一切节点状态变更走「dag 命令 → durable 事件 → projector」;行直写 = 破窗(`store.updateNodeDeadline` 直写由 T9 事件化废除,ADR-0003;guard 在命令层判,projector 纯折叠不返回行数) -- **G4 交付边界**:wake 投递条件 = `escalationPending ∨ (timeoutExtensions>0 ∧ terminal)`——两臂与旗子语义正交后自然正确。节点级谓词锚点 `loop.ts:949-953`,工作流级决策 `loop.ts:960-967`,summary 谓词 `store.ts:245-260`(`escalatedRows`:`escalation_pending=true ∧ status='running'`) -- **G5 锁域(ADR-0004)**:全部命令经 per-dagID KeyedMutex 串行 + 一行 `Effect.timeout("30 seconds")`(超时 = TimeoutException,guarded 记 warning 跳过,watchdog 自续重试);**临界区内禁止异步等待**。命令持锁期间的 publish+projector 同步事务(`event.ts:320-326`)属命令自身工作,非被禁的长时间 async 等待,guard race-free - -## watchdog 职责边界(转移提议者,非监督权威) - -- 只做两件事:过期且预算未尽 → 提议 T8(`spawn.ts:181`);预算耗尽 → 提议 T4(`spawn.ts:160`,+ 取消子会话) -- 不写节点行、不改旗子、不裁决、**从不调用 `nodeExtendTimeout`**(全仓 re-time 唯一路径是主 agent replan 经 `loop.ts:818`) -- 自续:每次提议后 sleep `escalateIntervalMs`(`= max(1s, nodeTimeout)`,`spawn.ts:111`)再读行——升级被裁决(T9/T7)则读到新死线安睡;未裁决则再升级,计数爬向 G2 -- **已知瑕疵(入表待修,不另开工单)**:cap 路径先 `promptSvc.cancel` 子会话、后发 nodeFailed——副作用先于转移事件;应改为事件后处置 - -## 错误即状态(trigger 分类 → agent 处置依据) - -nodeFailed.trigger ∈ { `timeout`(watchdog cap / 死线类), `exec_failed`(执行层), `verdict_fail`(契约层), … }——wake 文案按 trigger 承载处置建议(extend/restart/cancel/接受),agent 依据状态而非原始堆栈做判断。 - -## replay 一致性(ADR-0003 后果) - -事件日志含 T1-T9 全部转移 → 投影重建恢复**裁决后**的死线与计数(T9 携带绝对死线载荷);直写时代的分歧(重建恢复旧死线)随 `updateNodeDeadline` 废除而消失。T9 projector 幂等:`status='running'` 条件确保崩溃重放时终态行 0 行 benign skip,不双写。 diff --git a/.opencode/handoff-batch-b.md b/.opencode/handoff-batch-b.md deleted file mode 100644 index 09e58a754b..0000000000 --- a/.opencode/handoff-batch-b.md +++ /dev/null @@ -1,49 +0,0 @@ -# 批次 B 交接文档(新会话入口) - -> 本会话(批 A 全链路)已极长,批次 B 在新对话执行。引用本文件 + 下述证据路径即可开工。 - -## 起点状态(交接时) -- 基线:**dev**(批 B 规划基线 `3e8368f37`;每票开工前重新同步最新 `dev`) -- 批次 A 全闭环:Q1-Q6 引擎语义 + 接受期绑定校验 + flaky 根治(豁免清单已清零)+ 技术债清零(PR #185-#189 全合入) -- lint 棘轮:4852(CI 口径;本地 = CI − 10 生成物差,本地基线 ≤4842)——**只紧不松** -- 台账:.scratch/batch-a/issues/01-11(01-09 完成,10/11 closed) - -## 批次 B 四组票(审计后路由,证据已在案) - -### 组 1:U-1 / U-2 + Transport mid-stream-stall(一个 spec 三张票) -三个 abort-path 集成测试,同源同批。规格已收敛到 `.scratch/batch-b/abort-path-contracts.md`,按 01→02→03 逐票 /implement。 - -### 组 2:F3 / F4 测试卫生债 -小票直接做(无需 grill),每票新上下文 /implement。 - -### 组 3:O1 remote config last-known-good 缓存 -批 A 已完成“网络/响应体失败时 warn + skip”;剩余增量只有持久化 last-known-good。先完成 `.scratch/batch-b/issues/06-o1-lkg-spec.md` 的小规格,再按 07 实现;不得重新实现离线降级。 - -### 组 4:S7 recovery INVENTED 推断 ⚠️ -当前已有 ownership-lost 后暂停工作流的缓解,尚无用户态缺陷实证。**必须走 /diagnosing-bugs**:先建立一条确定性、快速、可红灯的复现命令;不能建立反馈回路则记录尝试并停止,不得先改生产代码。若红灯成立,另开修复票与新上下文。 - -## 证据路径(不用重新调查,票据直接引用) -- `.scratch/batch-b/evidence.md`:已追踪的稳定证据快照,含当前代码路径纠偏与验收边界;新 worktree 只依赖此文件 -- `.opencode/promotion-review-round1/*.md`、`.opencode/.dag-specs/evidence/*.md`:原始本地评审产物,当前未追踪,仅用于复核来源,不作为跨 worktree 前置 -- `.scratch/batch-b/abort-path-contracts.md`:U-1/U-2/mid-stream-stall 的已追踪规格;本地 OpenSpec 原件受 `.gitignore` 约束,不作为跨 worktree 前置 - -## 工程纪律(仓库铁律 + 本项目惯例) -- 分支:从最新 **dev** 切票据指定分支;生产 feature 用 `feat/**`,测试/规格债用 `test/**` 或 `docs/**`,均符合 branch-naming ruleset -- PR → dev:Typecheck 门禁;push dev 自动触发全量测试(Typecheck + Unit + E2E×2) -- 测试从包目录跑(packages/opencode 等),禁根目录;typecheck 用 `bun typecheck` 不用裸 tsc -- 每票一个新上下文会话执行(/implement 内含 /tdd),票间清上下文 -- HTTP API 路由若被触及:再生 SDK(./packages/sdk/js/script/build.ts)+ 更新 httpapi-exercise 场景 - -## 收束清单(批 B + 批 C 全部完成后) -1. dev 全量 CI 绿 → 一次性 dev→main 晋级 PR(四项门禁)→ 手动 release-fork -2. 分支一并清理(用户确认后手跑,dcg 拦 agent 删除): - ```bash - git worktree prune # 先清 opencode/* 残留 worktree(git worktree list 查路径) - git branch -d feat/dag-timeout-escalation feat/event-batch-publish feat/goal-pause-resume \ - feat/llm-request-timeout feat/session-runner-hotpath fix/config-offline-degrade \ - fix/deep-review-fixes review/dev-promotion <批B/C分支> - ``` -3. 台账惯例:新票记 .scratch/batch-b/issues/,完成翻 closed 附 PR/commit 实证 - -## 批 C(观测后再动,勿提前) -- P8 spawnReady O(ready×nodes):当前规模无实感,挂 /improve-codebase-architecture 巡检候选,疼了再做 diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 60c261eb0e..b0f7d59447 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -2,7 +2,7 @@ "$schema": "https://opencode.ai/config.json", "provider": {}, "permission": {}, - "reference": { + "references": { "effect": { "repository": "github.com/Effect-TS/effect-smol", "description": "Use for Effect v4 and effect-smol implementation details", diff --git a/.opencode/workflows/GRAPH-ENGINEERING.md b/.opencode/workflows/GRAPH-ENGINEERING.md deleted file mode 100644 index 8acf46a8f7..0000000000 --- a/.opencode/workflows/GRAPH-ENGINEERING.md +++ /dev/null @@ -1,41 +0,0 @@ -# Graph Engineering workflow catalog - -GraphAgent treats these workflows as durable, executable reference topologies. -The parent agent selects the closest shape, injects the current task, and derives -the actual DAG while preserving its protected fail-closed gates. - -## Reference graphs - -| Workflow | Protected spine | Use it for | -|---|---|---| -| `design-decision-loop` | internal grill → reasoner → fresh audit → PASS-only finalization | Deep development-document work and design-level debugging before implementation | -| `parallel-development-loop` | frozen contract → parallel modules → local audit → wiring → reasoner + verification → parallel review → arbiter | Medium/high-scale project implementation with bounded local correction waves | -| `deep-review-dag-module` | parallel exploration → parallel review → claim verification → arbiter → PASS report or targeted LOOP | Deep review of an already-built subsystem; retarget its lanes to the current project area | -| `change-review` | survey → parallel review/verification → arbiter | A compact fixed review when the medium/high-scale graph would be wasteful | - -The project-scoped `reasoner` used by the first two graphs lives at -`.opencode/agent/reasoner.md`. `/dag-flow` selects the closest reference from the -request. Start a saved graph by name only when its embedded target and inputs already -fit. Generic design/development requests must be derived into a one-off DAG with the -actual task injected into the root node; deep review requests retarget the hard-coded -DAG-module lanes unless that module is the real target. - -## Agent adaptation contract - -1. **Derive, do not blindly replay.** Preserve the reference graph's phase order and real artifact edges, then choose the actual module count, reviewer lanes, and local scope for the task. -2. **Protected nodes cannot be pruned.** Fresh-context review gates, deterministic verification, the single arbiter, and PASS-only finalization always remain. A parent may replace them only with equivalent fresh nodes carrying the same contract. -3. **Every prune is evidence-bearing.** Record `{node, prune_reason, replacement_coverage}`. Missing either field is fail-closed and the next gate must return `BLOCKED`, not silently accept the smaller graph. -4. **Every loop is local and acyclic.** A gate returns `PASS | LOOP | BLOCKED`. `LOOP` identifies the smallest preceding slice to revisit; the parent pauses, replans new correction/review nodes, and resumes. Completed nodes are never restarted in place. -5. **Expansion stays bounded.** New fan-out must have disjoint work or independent context, real downstream consumers, one merge owner, and enough remaining concurrency/node/replan budget. - -## Gate disposal rules - -| Verdict | Required parent action | Required evidence | -|---|---|---| -| `PASS` | Continue to the next protected phase or finalize | Coverage of every material criterion; no unresolved material finding | -| `LOOP` | Pause, add a fresh local correction/review wave with new node IDs, resume | Reason, minimal `loop_scope`, acceptance condition, `stop_reason` | -| `BLOCKED` | Stop and report; do not reinterpret as advisory success | Missing evidence/decision, unresolved contradiction, no progress, or a reached cap | - -These graphs are project-authored execution contracts, not framework-specific -examples. Their value lies in enforceable edges, evidence gates, bounded local -loops, and explicit parent-agent disposal rules. diff --git a/.opencode/workflows/algo-complexity-review.yaml b/.opencode/workflows/algo-complexity-review.yaml deleted file mode 100644 index 56c8696d52..0000000000 --- a/.opencode/workflows/algo-complexity-review.yaml +++ /dev/null @@ -1,246 +0,0 @@ -title: Algorithm Complexity Review (opencode-dag) -mode: standard -config: - name: algo-complexity-review - max_concurrency: 6 - max_node_replan_attempts: 2 - worker_config: - timeout_ms: 900000 - nodes: - - # ============================================================ - # Wave 1: 6 parallel algorithm reviewers (one per process cluster) - # ============================================================ - - - id: review-llm-pipeline - name: Review LLM Request Pipeline - worker_type: general - depends_on: [] - prompt_template: - inline: | - 你是算法复杂度审查专家。审查 opencode-dag 的 LLM 请求构建管道,验证知识图谱给出的静态复杂度指标是否与真实代码吻合。 - - ## 背景(已知热点指标,待你用真实代码验证或推翻) - - `convertToOpenAIResponsesInput` @ packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts:21 — cx=47, cog=261, alloc_in_loop=19, lines≈308。每次 LLM provider turn 必经。 - - `normalizeMessages` @ packages/opencode/src/provider/transform.ts — cx=47, ail=2。调用 sanitizeSurrogates + sanitizeToolResultOutput + scrub。 - - `prepareResponsesTools` @ packages/core/src/github-copilot/responses/openai-responses-prepare-tools.ts — ail=8。 - - `variants` @ packages/opencode/src/provider/transform.ts — cx=57, cog=135。 - - `convertToOpenAICompatibleChatMessages` @ packages/core/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts — ail=6。 - - ## 审查重点(按优先级) - 1. O(n²) 或更差模式:消息数 n 的循环内,每条消息是否再对消息列表/工具列表做线性扫描?嵌套 switch 是否随 n 放大? - 2. 循环内分配(alloc_in_loop):每个对象字面量/数组 push/JSON.stringify 的实际位置(file:line)及其是否可提到循环外。 - 3. 缺失 memoization:同一计算(如工具列表序列化、message key)是否在一次 turn 内重复执行。 - 4. 数据结构选择:是否有 Array.indexOf/includes/find 可改 Map/Set。 - 5. JSON.stringify 在热路径中的开销。 - - ## 工具使用 - - 用 Read 工具读取上述文件确认实现细节(精确 file:line)。 - - 可用 codebase-memory-mcp 的 get_code_snippet / trace_path(project="opencode-dag")查调用关系与邻居。 - - 调用链:packages/opencode/src/session/llm.ts → transformParams → normalizeMessages → 3 个 sanitizer;openai-responses-language-model.ts → getArgs/doGenerate/doStream → convertToOpenAIResponsesInput + prepareResponsesTools。 - - ## 输出要求(Markdown,提交为最终文本) - 对每个目标输出: - ### 目标名 (file:line) - - **已验证问题**:逐条列出,每条含 [severity] 描述 + `file:line` 证据 + 实测复杂度(含 n 代表什么)+ 修复建议。 - - **被推翻/夸大的声明**:指标与实际不符之处(如 alloc_in_loop 计数偏高/偏低,注明原因)。 - - **unverified_claims**:你无法从静态代码确认、需运行时 profiling 的点。 - 末尾给该 cluster 一个 **总体严重度评级**(CRITICAL/HIGH/MEDIUM/LOW)与一句话结论。区分"每次 LLM 请求"是已验证(代码路径必经)还是推测。 - - - id: review-dag-replan - name: Review DAG Replan Engine - worker_type: general - depends_on: [] - prompt_template: - inline: | - 你是算法复杂度审查专家。审查 opencode-dag 的 DAG replan 引擎,验证环检测与图重建的算法复杂度。 - - ## 背景(待验证指标) - - `planReplan` @ packages/core/src/dag/core/replan.ts:79 — cx=47, cog=87, alloc_in_loop=15, loop_count=14, tld=2, lines≈168。14 个循环遍历节点集合,15 处循环内分配。 - - 调用 `DependencyGraph.addNode/addEdge`、`DependencyGraph.hasCycle/findCycles`(声称 O(V+E) 环检测)。 - - 调用方:packages/opencode/src/dag/dag.ts。每次 DAG replan 操作触发。 - - ## 审查重点 - 1. **planReplan 的 O(n²) 风险**:survivingIds 集合构建是否真的需要 3 次遍历?14 个循环是否可合并?循环内是否对节点集合做 Array.includes/线性查找(应用 Set.has)。 - 2. **环检测开销**:定位 DependencyGraph 实现(find packages/core/src/dag 下的 dependency-graph)。hasCycle + findCycles 是否重复遍历?是 DFS 还是 Tarjan?replan 中是否每次都全图重算,而非增量。 - 3. **数据结构**:邻接表 vs 邻接矩阵;Map/Set vs Array。 - 4. **alloc_in_loop=15** 的真实位置(file:line)。 - 5. 大工作流(100+ 节点)时的最坏复杂度推导。 - - ## 工具 - - Read 上述文件;用 codebase-memory-mcp search_graph / get_code_snippet 找 DependencyGraph 实现(project="opencode-dag")。 - - trace_path(trace function_name="planReplan") 看调用方频率。 - - ## 输出(同上格式) - 重点给出:planReplan 的实测时间复杂度(用 n=节点数、e=边数表达),环检测是否增量,以及 100 节点时的推算开销。区分"O(n²)"是已验证(代码确有嵌套线性查找)还是推测。 - - - id: review-event-persist - name: Review Event Persistence & Reduction - worker_type: general - depends_on: [] - prompt_template: - inline: | - 你是算法复杂度审查专家。审查 opencode-dag 的持久化事件提交管道与会话数据归约。 - - ## 背景(待验证指标) - - `commitDurableEvent` @ packages/core/src/event.ts:126 — recursive=true, cx=13, lines≈163。每个持久化事件必经。含 DB 事务(immediate)、多次 SELECT/INSERT、isDeepStrictEqual 深比较。 - - `publish` @ packages/core/src/event.ts:340(通过 publishEvent 间接递归)。 - - `reduceSessionData` @ packages/opencode/src/cli/cmd/run/session-data.ts — cx=63, cog=139。fan-in 高(被 createLayer/applyChildEvent/bootstrapChildEvent/session-replay.apply 等 5+ 调用方使用)。 - - ## 审查重点 - 1. **递归安全性**:commitDurableEvent 的递归终止条件(守卫)是什么?最坏递归深度?是否尾递归/有深度上限?会否因恶意/深层 payload 栈溢出? - 2. **isDeepStrictEqual 开销**:定位其调用点 file:line,确认比较对象大小(payload?整条事件?),是否 O(n) 且在每个事件上执行。是否可用哈希替代。 - 3. **DB 往返**:确认每个事件的确切 SELECT/INSERT 次数(声称 2 SELECT + 2 INSERT)。是否可批量化/合并。immediate 事务模式的锁开销。 - 4. **去重检查**:SELECT EventTable 去重是否在主键/唯一索引上? - 5. **reduceSessionData**:cog=139 的高认知复杂度来源,是否有重复归约/未 memoize 的派生计算;事件序列化是否每次重建。 - - ## 工具 - - Read packages/core/src/event.ts 全文;Read session-data.ts。 - - codebase-memory-mcp trace_path(function_name="commitDurableEvent") 与 trace_path("reduceSessionData") 看真实 fan-in(project="opencode-dag")。 - - ## 输出(同上格式) - 重点:递归深度上界(已验证 vs 推测)、每事件 DB 往返实测数、isDeepStrictEqual 比较粒度。区分"全局热路径"是已验证(所有 durable event 必经)还是推测。 - - - id: review-frontend-render - name: Review Frontend Render Hot Paths - worker_type: general - depends_on: [] - prompt_template: - inline: | - 你是算法复杂度审查专家。审查 opencode-dag 前端(React/SolidJS app 包)渲染热路径的算法与重渲染开销。 - - ## 背景(待验证指标) - - `LegacyLayout` @ packages/app/src/pages/layout.tsx — cx=222, cog=249, tld=5, lsil=1, ail=4。全项目最高圈复杂度,每次渲染。 - - `Page` (session) @ packages/app/src/pages/session.tsx — cx=194, tld=6,含 revealMessage(unguarded_recursion=true)。 - - `PromptInput` @ packages/app/src/components/prompt-input.tsx — cx=148, cog=208,handleKeyDown(cx=33) 每次按键。 - - `MessageTimeline` @ packages/app/src/pages/session/timeline/message-timeline.tsx — cx=119, tld=6,长会话渲染开销。 - - ## 审查重点 - 1. **循环内线性扫描 (lsil)**:LegacyLayout 的 lsil=1 实际是哪段代码(file:line)?是否在 render 中对数组做 indexOf/find。MessageTimeline 是否对消息列表做 O(n²)。 - 2. **缺失 memoization**:useMemo/useCallback 缺失导致每次渲染重算;派生数据是否在 render 内现算而非 selector。 - 3. **重渲染触发面**:LegacyLayout 作为主布局,哪些 state 变化触发它重渲染;是否有 context 过度订阅。 - 4. **revealMessage 无守卫递归**:定位实现,确认终止条件与最坏深度,DOM 滚动场景会否栈溢出。 - 5. **PromptInput.handleKeyDown**:cx=33 的按键处理是否有重复计算/未 debounce。 - - ## 工具 - - Read 上述 .tsx 文件确认实现。注意区分 React(useMemo/useCallback/useEffect) vs SolidJS(createMemo/Signal) —— 先看包用的是哪个框架。 - - codebase-memory-mcp search_graph 可辅助(project="opencode-dag")。 - - ## 输出(同上格式) - 重点:每项的实测最坏复杂度(用消息数 m / 渲染次数 r 表达),lsil 的确切位置,memoization 缺口清单。区分"每次渲染/每次按键"是已验证(确认在 render/事件处理体内)还是推测。 - - - id: review-cli-tui - name: Review CLI Transport & TUI Hot Paths - worker_type: general - depends_on: [] - prompt_template: - inline: | - 你是算法复杂度审查专家。审查 opencode-dag 的 CLI 会话传输层与 TUI 输入热路径。 - - ## 背景(待验证指标) - - `createLayer` (stream.transport) @ packages/opencode/src/cli/cmd/run/stream.transport.ts — cx=79, cog=104, tld=6, ail=4。CLI 每个事件经过此路径。 - - `Autocomplete` (TUI) @ packages/tui/src/component/prompt/autocomplete.tsx — cx=45, tld=3, ail=2,每次按键。 - - `Session` (TUI) @ packages/tui/src/routes/session/index.tsx — cx=81, cog=100, tld=6。 - - 关联:reduceSessionData @ session-data.ts(另一 reviewer 负责其本体,你关注 createLayer 如何调用它)。 - - ## 审查重点 - 1. **createLayer tld=6**:6 层传递循环深度的实际代码结构(file:line)。是嵌套 for/while 还是递归?每个会话事件是否触发全量归约而非增量。 - 2. **ail=4** 的循环内分配位置,是否可提循环外。 - 3. **事件传输开销**:每个事件是否复制/序列化整个 session state;有无增量更新机制。 - 4. **Autocomplete 每次按键**:候选集构建是否在按键处理内重算;是否线性扫描候选;有无缓存。 - 5. **Session(TUI)** tld=6 的来源,渲染开销。 - - ## 工具 - - Read 上述文件;codebase-memory-mcp trace_path(function_name="createLayer", project="opencode-dag")。 - - ## 输出(同上格式) - 重点:createLayer 的传递循环结构(嵌套 vs 递归)、每事件开销是否随 session 大小放大。区分 tld=6 是真实嵌套循环还是误统计(如 Effect.gen 链)。 - - - id: review-recursion-risks - name: Review Recursion & Cross-Cut Event Sync - worker_type: general - depends_on: [] - prompt_template: - inline: | - 你是算法复杂度审查专家。审查 opencode-dag 的递归函数栈安全性与前端事件同步链。 - - ## 背景(待验证指标) - - `revealMessage` @ packages/app/src/pages/session.tsx — **unguarded_recursion=true**(高风险)。 - - `toModelOutput` @ packages/core/src/tool/edit.ts — **unguarded_recursion=true**(高风险)。 - - `flatten` @ packages/core/src/observability/logging.ts — unguarded_recursion,日志展平。 - - `settle`/`run` @ packages/core/src/session/run-coordinator.ts — 有守卫递归。 - - `applyDirectoryEvent` @ packages/app/src/context/global-sync/event-reducer.ts — cx=56, cog=153, tld=4,每个目录事件。 - - `createLLMEventPublisher` @ packages/core/src/session/runner/publish-llm-event.ts — cx=46, cog=96,每个 LLM 流事件。 - - ## 审查重点 - 1. **revealMessage / toModelOutput 无守卫递归**:读取实现,确认终止条件是否存在、最坏递归深度、触发场景,栈溢出可能性(给出具体 file:line)。这是本审查最高优先级。 - 2. **flatten** 递归:展平对象深度是否可控,恶意深层结构会否栈溢出。 - 3. **applyDirectoryEvent** cog=153:153 认知复杂度的具体来源;每个事件是否做全量重算;有无增量 reducer 模式缺失;是否有 O(n²) 事件应用。 - 4. **createLLMEventPublisher**:每个 LLM 流事件的开销;是否对每个 token 做重计算;事件去重/合并是否缺失。 - - ## 工具 - - Read 上述文件确认实现(unguarded 递归务必读到函数体与调用点)。 - - codebase-memory-mcp trace_path(project="opencode-dag")。 - - ## 输出(同上格式) - 重点:每个 unguarded 递归给出「终止条件是否存在 + 最坏深度 + 栈溢出风险评级」三要素,附 file:line。区分 unguarded_recursion 标记是否属实(有时是 mutual/indirect recursion 被误标,需你核实)。 - - # ============================================================ - # Wave 2: Arbiter — consolidate, dedupe, rank, emit verdict - # ============================================================ - - - id: arbitrate - name: Arbitrate & Consolidate Findings - worker_type: general - depends_on: - - review-llm-pipeline - - review-dag-replan - - review-event-persist - - review-frontend-render - - review-cli-tui - - review-recursion-risks - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, severity_ranked_findings, cross_cutting_patterns, unverified_claims, top_fixes, next_action] - properties: - verdict: - type: string - enum: [ACCEPT, REVISE, REJECT, BLOCKED] - description: "ACCEPT=审查完成结论可信;BLOCKED=证据不足" - severity_ranked_findings: - type: array - description: "按 CRITICAL>HIGH>MEDIUM>LOW 排序的最终发现,每条含 target, file:line, severity, 实测复杂度, 修复建议" - cross_cutting_patterns: - type: array - description: "跨多个 cluster 的共性算法反模式(如多处缺 memoization、多处 Array 代替 Set)" - unverified_claims: - type: array - description: "需运行时 profiling 才能确认的点" - top_fixes: - type: array - description: "投入产出比最高的 3-5 个修复,按 ROI 排序" - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: - type: array - prompt_template: - inline: | - 你是仲裁专家。6 位算法审查员已分别审查了 opencode-dag 的 6 个 cluster(LLM 管道 / DAG replan / 事件持久化 / 前端渲染 / CLI&TUI / 递归风险)。他们的发现作为你的结构化输入已附在上方。 - - ## 你的任务 - 1. **去重与合并**:同一问题被多个 reviewer 提及则合并,保留最强证据。 - 2. **核实冲突**:若 reviewer 间结论冲突,以 file:line 代码证据为准,必要时自行 Read 关键文件复核。 - 3. **按严重度排序**:CRITICAL > HIGH > MEDIUM > LOW。判定依据:实测最坏复杂度 × 触发频率 × 影响面(是否热路径)。每条给出「实测复杂度(含 n 的含义)」。 - 4. **提取横切反模式**:跨 cluster 的共性(如 alloc_in_loop 普遍、Set/Map 用 Array 代替普遍、缺 memoization 普遍)。 - 5. **厘清 unverified_claims**:把所有"需运行时验证"的点单列,不混入已验证发现。 - 6. **输出 top_fixes**:投入产出比最高的 3-5 个修复,按 ROI 排序(预期收益 ÷ 改动风险)。 - 7. **verdict**:若证据充分结论可信 → ACCEPT;若关键目标证据不足 → BLOCKED 并说明缺什么。 - - 严格区分 ✅已验证(代码路径确认必经、复杂度推导自源码)vs ⚠️未验证(频率/运行时开销推测)。调用 submit_result 提交符合 output_schema 的结构化结果。 diff --git a/.opencode/workflows/dag-module-review-v2.yaml b/.opencode/workflows/dag-module-review-v2.yaml deleted file mode 100644 index da2a7f223f..0000000000 --- a/.opencode/workflows/dag-module-review-v2.yaml +++ /dev/null @@ -1,255 +0,0 @@ -title: DAG Module Deep Review -config: - name: dag-module-review - max_concurrency: 5 - max_node_replan_attempts: 3 - max_total_nodes: 20 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 600000 - nodes: - - id: explore-dag - name: explore-dag - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - Map the DAG module at packages/opencode/src/dag/ in this repository. - - The module has 13 source files across these clusters: - - Core workflow control: dag.ts (withWorkflowLock, pause, resume, step, cancel, complete, parseWorkflowConfig, normalizeModel, computeMergedConfig) - - Configuration: config.ts (load, tierModel, globalConfigDir) - - Model resolution: model.ts (resolve) - - Admission QA: admission.ts (transitionAdmission, fingerprintBrief, createAdmissionRecord, validateAdmission, evaluateQa, projectBriefForNode) - - Review lifecycle: review-lifecycle.ts (validateReviewLifecycle, validateReviewExecutionInput, reviewImplementationFingerprint, isReviewWorker) - - Templates: templates/resolve.ts, templates/sanitize.ts (renderTemplate, sanitizeInput, preserveEvidence) - - Runtime: runtime/spawn.ts (spawnNode), runtime/loop.ts, runtime/recovery.ts (settle), runtime/capture.ts (registerCaptureSlot), runtime/eval.ts (evaluateCondition, resolveInputMapping), runtime/summary-publisher.ts (schedulePublish) - - Tests are in packages/opencode/test/dag/ (25 test files). - - Your task: - 1. Read each source file and produce a responsibility map: file → public API → internal dependencies → external dependencies (imports from outside src/dag/). - 2. Identify the dependency direction between clusters. Flag any circular or upward dependencies. - 3. List all cross-module consumers: who outside src/dag/ imports from this module? - 4. Note any files that appear under-tested or over-tested relative to their complexity. - - Output a structured inventory with file paths and line numbers for key exports. - - - id: review-arch - name: review-arch - worker_type: general - depends_on: [explore-dag] - prompt_template: - inline: | - You are the PROSECUTOR in an adversarial architecture review of the DAG module at packages/opencode/src/dag/. - - Your mandate: argue the structure is WRONG. Focus on: - - Coupling violations: clusters that should be independent but share state or call each other - - Hidden invariants: assumptions that are enforced by convention rather than types or runtime checks - - Failure modes: what happens when withWorkflowLock contention occurs, when spawn fails mid-graph, when recovery races with a live scheduler - - Layer violations: runtime/ depending on dag.ts control flow, or templates/ reaching into runtime state - - Missing abstractions: god-objects, files doing too much, logic that should be extracted - - Use the exploration inventory as your starting point: {{explore-dag}} - - RULES: - - Every finding MUST cite file:line evidence from the actual source code - - Read the source files directly to verify your claims - - List any claim you could NOT verify as unverified_claims with the reason - - Severity: CRITICAL (data loss/corruption), HIGH (incorrect behavior), MEDIUM (maintainability), LOW (style) - - Output format: - ## Findings - [numbered list with severity, file:line, description, evidence] - - ## unverified_claims - [claims you made but could not confirm from code, with reason] - - - id: review-logic - name: review-logic - worker_type: general - depends_on: [explore-dag] - prompt_template: - inline: | - You are a CORRECTNESS reviewer for the DAG module at packages/opencode/src/dag/. - - Your mandate: find logic bugs, race conditions, and edge-case failures. Focus on: - - State machine correctness: admission transitions (admission.ts), workflow status transitions in dag.ts - - Concurrency: withWorkflowLock implementation, can two operations interleave incorrectly? Does recovery.ts race with loop.ts? - - Condition evaluation: eval.ts edge cases (undefined references, circular conditions, type coercion) - - Loop termination: can the execution loop in runtime/loop.ts get stuck? Are guard counters correct? - - Schema validation: can invalid payloads pass validateAgainstSchema? Are error paths handled? - - Recovery semantics: does settle() in recovery.ts correctly handle all crash states? Can it lose node results? - - Use the exploration inventory as your starting point: {{explore-dag}} - - RULES: - - Every finding MUST cite file:line evidence from the actual source code - - Read the source files directly, trace execution paths - - List any claim you could NOT verify as unverified_claims with the reason - - Severity: CRITICAL (data loss/corruption), HIGH (incorrect behavior), MEDIUM (edge case), LOW (theoretical) - - Output format: - ## Findings - [numbered list with severity, file:line, description, evidence] - - ## unverified_claims - [claims you made but could not confirm from code, with reason] - - - id: review-style - name: review-style - worker_type: general - depends_on: [explore-dag] - prompt_template: - inline: | - You are a CONVENTIONS reviewer for the DAG module at packages/opencode/src/dag/. - - Your mandate: check adherence to the project's documented coding standards from AGENTS.md. Focus on: - - Import conventions: no aliases, no star imports, prefer dynamic imports for heavy modules - - Variable style: const over let, ternaries over reassignment, no unnecessary destructuring - - Control flow: early returns over else, no try/catch where avoidable - - Effect patterns: services bound to named variables before method calls, no nested service yields - - Type safety: no `any`, rely on type inference where possible - - Comments: should only exist for non-obvious constraints (flag unnecessary comments) - - Schema/Drizzle: snake_case field names (if applicable) - - Helper extraction: single-use helpers should be inlined; helpers should not return Effect unless effectful - - Use the exploration inventory as your starting point: {{explore-dag}} - - RULES: - - Every finding MUST cite file:line evidence from the actual source code - - Read the source files directly - - List any claim you could NOT verify as unverified_claims with the reason - - Severity: HIGH (violates explicit AGENTS.md rule), MEDIUM (violates spirit), LOW (inconsistency) - - Output format: - ## Findings - [numbered list with severity, file:line, description, evidence] - - ## unverified_claims - [claims you made but could not confirm from code, with reason] - - - id: verify-claims - name: verify-claims - worker_type: explore - depends_on: [review-arch, review-logic, review-style] - prompt_template: - inline: | - You are the CLAIM VERIFIER. Three reviewers produced findings about the DAG module at packages/opencode/src/dag/. - - Architecture prosecutor findings: - {{review-arch}} - - Correctness reviewer findings: - {{review-logic}} - - Conventions reviewer findings: - {{review-style}} - - Your task: - 1. Collect ALL items from every reviewer's unverified_claims section - 2. Collect all CRITICAL and HIGH severity findings - 3. For each item, read the actual source code at the cited file:line and determine: CONFIRMED (evidence supports the claim), REFUTED (evidence contradicts it), or PARTIAL (partially true, explain) - 4. For disputed claims between reviewers (one says X, another implies not-X), resolve the conflict with evidence - - Output format: - ## Verification Results - [for each claim: reviewer source, claim summary, file:line checked, verdict (CONFIRMED/REFUTED/PARTIAL), evidence] - - ## Conflicts Resolved - [where reviewers disagreed, state who was right and why] - - ## Remaining Uncertain - [claims that remain genuinely uncertain after your check, with reason] - - - id: arbitrate - name: arbitrate - worker_type: general - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, summary, findings, required_actions, next_action] - properties: - verdict: - type: string - enum: [ACCEPT, REVISE, REJECT, BLOCKED] - summary: - type: string - findings: - type: array - items: - type: object - properties: - severity: - type: string - file: - type: string - description: - type: string - verified: - type: boolean - required_actions: - type: array - items: - type: string - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: - type: array - items: - type: string - prompt_template: - inline: | - You are the ARBITER for the DAG module review. Rule on VERIFIED evidence only. - - The claim verifier's results: - {{verify-claims}} - - Your task: - 1. Discard any finding that was REFUTED by the verifier - 2. For CONFIRMED and PARTIAL findings, rule finding-by-finding: is this a real issue requiring action? - 3. Deduplicate findings that describe the same root cause - 4. Rank by severity and impact - 5. Emit a structured verdict: - - ACCEPT: no CRITICAL/HIGH issues remain after verification - - REVISE: CRITICAL/HIGH issues confirmed but bounded and actionable - - REJECT: systemic issues requiring redesign - - BLOCKED: cannot determine without additional information - 6. For each confirmed finding in the output, set verified=true; for any you include from PARTIAL, set verified=false - 7. required_actions: concrete, file-specific fixes - 8. next_action: if ACCEPT → operation=complete; if REVISE → operation=extend with targets being the files to fix; if REJECT → operation=replan - - Call submit_result with your structured verdict. - - - id: deep-dive - name: deep-dive - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict != "ACCEPT"' - report_to_parent: true - prompt_template: - inline: | - The arbiter did not accept the DAG module review. Its verdict and findings: - {{arbitrate}} - - Your task: - 1. For each required_action, read the actual code at the cited location - 2. Produce a concrete, implementable fix proposal for each action: - - Exact file and line range to modify - - What the code currently does (quote it) - - What it should do instead - - Why the fix is safe (what invariants it preserves) - 3. If any finding is actually a false positive on closer inspection, say so and explain - 4. Rank the fixes by priority (CRITICAL first) - - Output a prioritized fix plan that a developer can execute directly. diff --git a/.opencode/workflows/dag-module-review.yaml b/.opencode/workflows/dag-module-review.yaml deleted file mode 100644 index 636ae194e2..0000000000 --- a/.opencode/workflows/dag-module-review.yaml +++ /dev/null @@ -1,235 +0,0 @@ -title: DAG Module Deep Review -config: - name: dag-module-review - max_concurrency: 5 - max_node_replan_attempts: 3 - node_defaults: - report_to_parent: false - worker_config: - timeout_ms: 600000 - nodes: - - id: explore-dag - name: explore-dag - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - Explore the DAG workflow orchestration module at packages/opencode/src/dag/ in this repository. - - Use the codebase-memory-mcp tools (project name: "opencode-dag") for structural analysis: - - search_graph, trace_path, get_code_snippet, query_graph - - Map the complete module structure: - 1. List every file with its responsibility (admission.ts, config.ts, dag.ts, model.ts, review-lifecycle.ts, runtime/*.ts, templates/*.ts) - 2. Identify the core data flow: workflow creation → node scheduling → execution → completion - 3. Map cross-file dependencies and coupling points - 4. Identify the public API surface (exported functions/classes) - 5. Note integration points with the rest of the system (Session, EventV2, Provider, etc.) - 6. Flag any files over 300 lines or functions with high cyclomatic complexity - - Output a structured inventory: file paths, responsibilities, key exports, dependency edges, and integration boundaries. - - - id: review-arch - name: review-arch - worker_type: general - depends_on: [explore-dag] - prompt_template: - inline: | - You are an architecture reviewer. Review the DAG module at packages/opencode/src/dag/ for architectural soundness. - - Context from exploration: - {{explore-dag}} - - Use codebase-memory-mcp (project: "opencode-dag") and file reads to examine the actual code. - - Review dimensions: - - Module boundaries: are responsibilities cleanly separated? (admission vs config vs runtime vs templates) - - Coupling: does dag.ts (core) leak runtime concerns? Does runtime/ properly abstract execution? - - Layer violations: does the module respect the LayerNode/defaultLayer self-containment invariant from AGENTS.md? - - State management: is workflow state mutation properly guarded (withWorkflowLock)? - - Extension points: are templates, model resolution, and config properly pluggable? - - HARD REQUIREMENTS: - - Every finding MUST cite file:line evidence - - List any claims you could NOT verify as `unverified_claims` - - Do NOT modify any files - - Rate each finding: CRITICAL / HIGH / MEDIUM / LOW - - - id: review-logic - name: review-logic - worker_type: general - depends_on: [explore-dag] - prompt_template: - inline: | - You are a correctness reviewer. Review the DAG module at packages/opencode/src/dag/ for logic correctness and edge-case safety. - - Context from exploration: - {{explore-dag}} - - Use codebase-memory-mcp (project: "opencode-dag") and file reads to examine the actual code. - - Review dimensions: - - State machine correctness: workflow lifecycle transitions (pending → running → completed/failed/cancelled/paused) - - Concurrency safety: lock usage in withWorkflowLock, race conditions in node scheduling - - Error handling: are failures properly propagated? Can a node failure leave the workflow in an inconsistent state? - - Recovery logic (runtime/recovery.ts): does crash recovery correctly handle mid-flight nodes? - - Condition evaluation (runtime/eval.ts): can conditions reference undefined outputs? - - Admission state machine (admission.ts): are transitions exhaustive and mutually exclusive? - - HARD REQUIREMENTS: - - Every finding MUST cite file:line evidence - - List any claims you could NOT verify as `unverified_claims` - - Do NOT modify any files - - Rate each finding: CRITICAL / HIGH / MEDIUM / LOW - - - id: review-style - name: review-style - worker_type: general - depends_on: [explore-dag] - prompt_template: - inline: | - You are a code style and conventions reviewer. Review the DAG module at packages/opencode/src/dag/ against the project's documented style guide in AGENTS.md. - - Context from exploration: - {{explore-dag}} - - Use codebase-memory-mcp (project: "opencode-dag") and file reads to examine the actual code. - - Review dimensions: - - Effect usage: are services bound to named variables before method calls? No nested service yields? - - Import style: no aliases, no star imports, dynamic imports for heavy modules? - - Variable style: const over let, ternaries over reassignment, no unnecessary destructuring? - - Control flow: early returns over else, no try/catch where avoidable? - - Schema/Drizzle conventions: snake_case field names? - - Type safety: no `any`, proper use of Schema helpers for JSON parsing? - - Comment discipline: comments only for non-obvious constraints? - - HARD REQUIREMENTS: - - Every finding MUST cite file:line evidence - - List any claims you could NOT verify as `unverified_claims` - - Do NOT modify any files - - Rate each finding: CRITICAL / HIGH / MEDIUM / LOW - - - id: verify-claims - name: verify-claims - worker_type: general - depends_on: [review-arch, review-logic, review-style] - required: true - prompt_template: - inline: | - You are a claim verification agent. Three reviewers produced findings about the DAG module at packages/opencode/src/dag/. - - Architecture review: - {{review-arch}} - - Logic/correctness review: - {{review-logic}} - - Style review: - {{review-style}} - - Your job: - 1. Collect ALL items listed as `unverified_claims` from all three reviews - 2. Collect all CRITICAL and HIGH findings - 3. For EACH item, verify it against the actual source code using file reads and codebase-memory-mcp (project: "opencode-dag") - 4. Mark each as: CONFIRMED (evidence supports it), REFUTED (evidence contradicts it), or PARTIAL (partially true) - 5. For CONFIRMED findings, note the exact file:line evidence - 6. For REFUTED findings, explain why the reviewer was wrong - - Output a verification report with: - - verified_findings: list of CONFIRMED findings with evidence - - refuted_findings: list of REFUTED findings with counter-evidence - - partial_findings: list of PARTIAL findings with nuance - - unresolved: items you could not determine - - Do NOT modify any files. - - - id: arbitrate - name: arbitrate - worker_type: general - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, summary, findings, required_actions, next_action] - properties: - verdict: - type: string - enum: [ACCEPT, REVISE, REJECT, BLOCKED] - summary: - type: string - findings: - type: array - items: - type: object - properties: - id: { type: string } - severity: { type: string } - category: { type: string } - description: { type: string } - evidence: { type: string } - verified: { type: boolean } - required_actions: - type: array - items: { type: string } - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: - type: array - items: { type: string } - prompt_template: - inline: | - You are the final arbiter for a code review of the DAG module at packages/opencode/src/dag/. - - Verification report: - {{verify-claims}} - - Your job: - 1. Rule finding-by-finding on the VERIFIED evidence only (CONFIRMED and PARTIAL items) - 2. Discard REFUTED findings entirely - 3. Deduplicate overlapping findings across reviewers - 4. Resolve conflicts between reviewers using the verification evidence - 5. Produce a final ranked list of findings by severity - 6. Decide verdict: - - ACCEPT: no CRITICAL or HIGH verified findings remain - - REVISE: HIGH findings exist but are actionable and bounded - - REJECT: CRITICAL findings exist or the module has fundamental structural problems - - BLOCKED: cannot determine due to unresolved verification gaps - 7. Specify required_actions for each confirmed finding - 8. Set next_action: if ACCEPT → complete; if REVISE/REJECT → extend with targets being the problem areas - - Call submit_result with your structured verdict. - - - id: deep-dive - name: deep-dive - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict != "ACCEPT"' - report_to_parent: true - prompt_template: - inline: | - The arbiter did not accept the DAG module review. Verify each required action against the actual code and produce a corrected, evidence-backed action plan. - - Arbiter verdict: - {{arbitrate}} - - For each required_action: - 1. Read the relevant source files in packages/opencode/src/dag/ - 2. Confirm the problem exists with exact file:line evidence - 3. Propose a concrete fix (code-level, not abstract) - 4. Assess fix risk and blast radius - - Output a prioritized remediation plan with: - - Each action confirmed or dismissed - - Concrete code suggestions for confirmed actions - - Dependency order for fixes - - Risk assessment per fix - - Do NOT modify any files. diff --git a/.opencode/workflows/dag-review.yaml b/.opencode/workflows/dag-review.yaml deleted file mode 100644 index f1bac27152..0000000000 --- a/.opencode/workflows/dag-review.yaml +++ /dev/null @@ -1,166 +0,0 @@ -title: "DAG Module Deep Review" -config: - name: dag-module-review - max_concurrency: 5 - max_node_replan_attempts: 3 - max_total_nodes: 20 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 600000 - nodes: - - id: explore-core - name: explore-core - worker_type: explore - depends_on: [] - prompt_template: - id: code-explore - input: - target: "packages/opencode/src/dag core lifecycle files: dag.ts, config.ts, model.ts, admission.ts, review-lifecycle.ts. Map workflow state machine transitions, locking strategy (withWorkflowLock), node lifecycle (spawn/complete/fail/cancel/pause/resume/step), config normalization, model resolution, and admission QA protocol. Identify state ownership, concurrency guards, and cross-module contracts." - - - id: explore-runtime - name: explore-runtime - worker_type: explore - depends_on: [] - prompt_template: - id: code-explore - input: - target: "packages/opencode/src/dag/runtime execution engine: loop.ts (scheduling loop, layer computation, concurrency control), spawn.ts (child session creation), recovery.ts (crash recovery, reconciliation), eval.ts (condition evaluation, input mapping), capture.ts (output schema validation, submit_result), summary-publisher.ts (event emission). Map the scheduling algorithm, session lifecycle, error propagation, and recovery invariants." - - - id: explore-templates - name: explore-templates - worker_type: explore - depends_on: [] - prompt_template: - id: code-explore - input: - target: "packages/opencode/src/dag/templates template system: resolve.ts (template resolution, rendering, interpolation) and sanitize.ts (input sanitization, injection prevention). Map template loading (by ID from .opencode/dag-prompts, inline), variable interpolation mechanics, and the sanitization boundary. Identify trust assumptions and injection vectors." - - - id: review-arch - name: review-arch - worker_type: review - depends_on: [explore-core, explore-runtime, explore-templates] - prompt_template: - id: review-arch - - - id: review-logic - name: review-logic - worker_type: review - depends_on: [explore-core, explore-runtime, explore-templates] - prompt_template: - id: review-logic - - - id: review-style - name: review-style - worker_type: review - depends_on: [explore-core, explore-runtime, explore-templates] - prompt_template: - id: review-style - - - id: verify-claims - name: verify-claims - worker_type: general - depends_on: [review-arch, review-logic, review-style] - required: true - prompt_template: - inline: | - You are a claim verifier. Three reviewers produced findings and unverified_claims about the DAG module (packages/opencode/src/dag/). - - Your job: take EVERY item listed under `unverified_claims` from all three reviews and check it against the actual source code. For each claim: - 1. Open the cited file(s) and read the relevant code. - 2. Determine: CONFIRMED (the claim is true, cite evidence), REFUTED (the claim is false, cite counter-evidence), or INCONCLUSIVE (cannot determine from code alone, state why). - 3. Also spot-check any finding marked CRITICAL/P0 that lacks a clear file:line citation. - - Output a structured verdict per claim. Never modify any file. - - ## Reviewer outputs to verify: - - ### Architecture Review - {{review-arch}} - - ### Logic Review - {{review-logic}} - - ### Style Review - {{review-style}} - - - id: arbitrate - name: arbitrate - worker_type: review - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, summary, findings, required_actions, next_action] - properties: - verdict: - type: string - enum: [ACCEPT, REVISE, REJECT, BLOCKED] - summary: - type: string - findings: - type: array - items: - type: object - properties: - severity: { type: string } - title: { type: string } - evidence: { type: string } - status: { type: string, enum: [confirmed, refuted, inconclusive] } - required_actions: - type: array - items: { type: string } - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: - type: array - items: { type: string } - prompt_template: - inline: | - You are the arbiter for a deep review of the DAG workflow engine (packages/opencode/src/dag/). - - Three reviewers (architecture, logic, style) produced findings. A verification node then checked all unverified_claims against the actual code. - - Your task: - 1. Rule finding-by-finding: accept only findings with CONFIRMED evidence. Discard REFUTED claims. Flag INCONCLUSIVE items as residual risk. - 2. Deduplicate overlapping findings across reviewers. - 3. Rank confirmed findings by severity and blast radius. - 4. Emit a structured verdict: - - ACCEPT: no CRITICAL/P0 confirmed findings, module is sound. - - REVISE: confirmed findings exist but are addressable without redesign. - - REJECT: confirmed CRITICAL/P0 findings require structural rework. - - BLOCKED: verification was insufficient to rule. - 5. Provide required_actions (concrete, file-scoped) and next_action for the orchestrator. - - Never modify any file. Base your ruling ONLY on verified evidence from the verification node. - - ## Verification Results - {{verify-claims}} - - - id: deep-dive - name: deep-dive - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict != "ACCEPT"' - report_to_parent: true - prompt_template: - inline: | - The arbiter did not ACCEPT the DAG module review. Its findings and required actions are below. - - For each required_action: - 1. Open the cited file(s) and verify the problem still exists at the stated location. - 2. Produce a corrected, evidence-backed remediation plan: exact file, function, what to change, and why. - 3. Identify any dependencies between actions (ordering constraints). - 4. Flag any action that is infeasible or would cause a regression. - - Output a prioritized remediation plan. Never modify any file. - - ## Arbiter Verdict - {{arbitrate}} diff --git a/.opencode/workflows/deep-perf-review.yaml b/.opencode/workflows/deep-perf-review.yaml deleted file mode 100644 index c81e88d9c6..0000000000 --- a/.opencode/workflows/deep-perf-review.yaml +++ /dev/null @@ -1,346 +0,0 @@ -title: "Deep Performance Review - opencode-dag" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "对 opencode-dag 仓库进行深度性能审查,识别热路径、启动瓶颈、I/O 低效、并发问题和内存压力" - scope: - in: - - "packages/opencode (core runtime, session, DAG engine, event system, storage)" - - "packages/tui (SolidJS rendering, reactivity)" - - "packages/plugin (plugin lifecycle)" - - "packages/schema (serialization)" - - "packages/sdk/js (generated client)" - - "packages/client (HTTP client)" - out: - - "功能正确性(非性能维度)" - - "代码风格/命名" - - "测试覆盖率(除非性能测试缺失)" - constraints: - - "只读审查,不修改任何文件" - - "所有发现必须附带 file:line 证据" - - "无法验证的声明必须标记为 unverified_claims" - assumptions: - - "当前工作树即审查目标" - - "Bun 为运行时" - - "Effect-TS 为核心框架" - acceptance_criteria: - - "每个发现都有 file:line 代码引用" - - "发现按严重程度排序(CRITICAL/HIGH/MEDIUM/LOW)" - - "包含具体优化建议" - - "unverified_claims 经过验证波次确认" - evidence_required: - - "代码引用(file:line)" - - "算法复杂度分析" - - "调用链/数据流路径" - risks: - - "跨模块性能回归被遗漏" - - "Effect-TS 特有的性能陷阱(Layer 构建、Fiber 泄漏)" - - "DAG 引擎在大规模工作流下的扩展性" - review_plan: - - "Wave 1: 探索性能关键表面" - - "Wave 2: 5 个并行维度审查(启动/热路径/IO/并发/TUI)" - - "Wave 3: 验证波次检查 unverified_claims" - - "Wave 4: 仲裁节点裁决并排序" - open_questions: [] - blocking_questions: [] -config: - name: deep-perf-review - max_concurrency: 6 - max_node_replan_attempts: 3 - max_total_nodes: 20 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - - id: explore-perf - name: "Performance Surface Mapping" - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - 你是性能探索专家。映射 opencode-dag 仓库中所有性能关键的代码表面。 - - 重点探索: - 1. 启动路径:入口点 → 服务初始化 → Layer 构建链(packages/opencode/src 的 main/index/entry) - 2. 热循环:DAG 调度循环、事件处理循环、Session drain 循环 - 3. I/O 边界:SQLite/Drizzle 查询、文件读写、HTTP 请求、MCP 通信 - 4. 内存敏感区:事件存储、会话历史、知识图谱缓存 - 5. TUI 渲染:SolidJS 响应式更新、列表渲染、事件流消费 - - 输出格式: - - 每个表面:文件路径、关键函数、调用深度、潜在瓶颈假设 - - 标记哪些区域需要深入审查 - - 列出 packages/opencode/src 下的核心模块及其职责 - - 使用 codebase-memory-mcp 工具(search_graph, trace_path, get_architecture)优先,grep/glob 作为补充。 - - - id: review-startup - name: "Startup & Lazy Loading Review" - worker_type: review - depends_on: [explore-perf] - review: - phase: design - prompt_template: - inline: | - 你是启动性能审查专家。审查 opencode-dag 的启动性能和模块加载策略。 - - 审查维度: - 1. 入口点到首个可交互状态的完整路径(packages/opencode/src/index.ts 或 main) - 2. Effect Layer 构建链:哪些 Layer 是 eager 的?哪些可以 lazy? - 3. 动态 import 使用情况:是否有不必要的静态 import 拖慢启动? - 4. 服务初始化顺序:是否有可以并行化的串行初始化? - 5. 配置加载、插件发现、MCP 服务器启动的开销 - - 规则: - - 每个发现必须引用 file:line - - 无法确认的声明放入 unverified_claims 部分 - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 给出具体的优化建议 - - 输出结构: - ## findings - ## unverified_claims - ## recommendations - - - id: review-hotpath - name: "Hot Path & Algorithmic Complexity Review" - worker_type: review - depends_on: [explore-perf] - review: - phase: design - prompt_template: - inline: | - 你是算法性能审查专家。审查 opencode-dag 中的热路径和算法复杂度问题。 - - 审查维度: - 1. DAG 调度器:节点就绪检测、拓扑排序、并发调度的复杂度 - 2. 事件系统:事件分发、订阅匹配、历史回放的效率 - 3. Session 管理:消息序列化/反序列化、上下文窗口管理 - 4. 搜索/索引:codebase-memory-mcp 集成、代码搜索路径 - 5. 循环内的线性扫描(find/includes/indexOf in loop) - 6. 不必要的重复计算或缺失的缓存 - - 使用 codebase-memory-mcp 的 query_graph 查询: - - transitive_loop_depth >= 3 的函数 - - linear_scan_in_loop >= 1 的函数 - - alloc_in_loop 的函数 - - 规则: - - 每个发现必须引用 file:line - - 标注大 O 复杂度 - - 无法确认的声明放入 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 输出结构: - ## findings - ## unverified_claims - ## recommendations - - - id: review-io - name: "I/O & Database Efficiency Review" - worker_type: review - depends_on: [explore-perf] - review: - phase: design - prompt_template: - inline: | - 你是 I/O 性能审查专家。审查 opencode-dag 的数据库访问、文件 I/O 和网络通信效率。 - - 审查维度: - 1. SQLite/Drizzle 查询:N+1 查询、缺失索引、全表扫描、事务粒度 - 2. 文件 I/O:是否有可以批处理的小文件读写?Bun.file() 使用是否最优? - 3. HTTP 通信:SDK client 调用是否有不必要的串行请求?连接复用? - 4. MCP 通信:stdio/SSE 传输的效率、消息序列化开销 - 5. 事件持久化:写入频率、批量策略、WAL 模式使用 - 6. 日志系统:是否有过度日志影响性能? - - 规则: - - 每个发现必须引用 file:line - - 标注 I/O 模式(同步/异步/批量) - - 无法确认的声明放入 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 输出结构: - ## findings - ## unverified_claims - ## recommendations - - - id: review-concurrency - name: "Concurrency & Memory Review" - worker_type: review - depends_on: [explore-perf] - review: - phase: design - prompt_template: - inline: | - 你是并发和内存性能审查专家。审查 opencode-dag 的异步模式、内存使用和 Effect-TS 特有性能问题。 - - 审查维度: - 1. Fiber 管理:是否有泄漏的 Fiber?未取消的后台任务? - 2. Layer 构建:重复构建昂贵 Layer?Layer 缓存策略? - 3. 背压:事件流消费是否有背压机制?DAG 节点产出是否有限流? - 4. 内存:大对象生命周期、缓存淘汰策略、会话历史增长 - 5. 锁/信号量:是否有不必要的串行化?死锁风险? - 6. GC 压力:频繁分配/释放、大数组拷贝、字符串拼接 - - 规则: - - 每个发现必须引用 file:line - - 标注并发模式(fork/join/race/stream) - - 无法确认的声明放入 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 输出结构: - ## findings - ## unverified_claims - ## recommendations - - - id: review-tui - name: "TUI Rendering & Reactivity Review" - worker_type: review - depends_on: [explore-perf] - review: - phase: design - prompt_template: - inline: | - 你是前端/TUI 渲染性能审查专家。审查 packages/tui 的 SolidJS/opentui 渲染性能。 - - 审查维度: - 1. 响应式更新:是否有过度细粒度的 signal 导致频繁重渲染? - 2. 列表渲染:大列表(会话列表、DAG 节点列表)是否虚拟化? - 3. 事件流消费:SSE 事件处理是否高效?是否有节流/防抖? - 4. 组件结构:是否有不必要的组件重建?memo 使用是否合理? - 5. 状态管理:store 更新是否触发不必要的依赖计算? - 6. 键盘输入处理:是否有输入延迟? - - 规则: - - 每个发现必须引用 file:line(packages/tui/src/ 下) - - 无法确认的声明放入 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 输出结构: - ## findings - ## unverified_claims - ## recommendations - - - id: verify-claims - name: "Claim Verification" - worker_type: verify - depends_on: [review-startup, review-hotpath, review-io, review-concurrency, review-tui] - required: true - prompt_template: - inline: | - 你是性能声明验证专家。你的任务是验证上游 5 个审查节点产出的 unverified_claims。 - - 对每个 unverified_claim: - 1. 定位到实际代码(file:line) - 2. 确认或否定该声明 - 3. 如果确认,补充具体证据(代码片段、调用链、复杂度分析) - 4. 如果否定,说明为什么该声明不成立 - - 同时交叉检查: - - 不同审查者是否对同一代码段有矛盾结论? - - 是否有跨维度的复合性能问题(如 I/O + 并发)? - - 输出: - ## verified_claims(确认的声明 + 证据) - ## rejected_claims(否定的声明 + 原因) - ## cross_cutting_issues(跨维度问题) - ## conflicts(审查者间的矛盾) - - - id: arbitrate - name: "Performance Arbitration" - worker_type: general - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, summary, findings, required_actions, next_action] - properties: - verdict: - type: string - enum: [ACCEPT, REVISE, REJECT, BLOCKED] - summary: - type: string - findings: - type: array - items: - type: object - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - category: - type: string - location: - type: string - description: - type: string - recommendation: - type: string - required_actions: - type: array - items: - type: string - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: - type: array - items: - type: string - prompt_template: - inline: | - 你是性能审查仲裁者。基于验证波次的结果,对 opencode-dag 的性能状况做出最终裁决。 - - 你的输入: - - 5 个维度审查者的发现(startup, hotpath, io, concurrency, tui) - - 验证节点的确认/否定结果 - - 裁决规则: - 1. 只基于已验证的证据做判断,忽略被否定的声明 - 2. 去重:多个审查者报告同一问题时合并 - 3. 排序:按实际性能影响排序(CRITICAL > HIGH > MEDIUM > LOW) - 4. 每个 finding 必须包含:severity, category, location(file:line), description, recommendation - 5. 解决审查者间的矛盾,给出理由 - - verdict 含义: - - ACCEPT: 无 CRITICAL/HIGH 问题,性能状况可接受 - - REVISE: 有 HIGH 问题需要关注但不阻塞 - - REJECT: 有 CRITICAL 问题需要立即修复 - - BLOCKED: 无法做出判断(证据不足) - - 调用 submit_result 提交结构化裁决。 - - - id: deep-dive - name: "Performance Deep Dive" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict != "ACCEPT"' - report_to_parent: true - prompt_template: - inline: | - 仲裁者发现了需要关注的性能问题。对每个 required_action 进行深入分析: - - 1. 定位具体代码,确认问题存在 - 2. 分析根因(为什么会出现这个问题?) - 3. 评估影响范围(哪些用户场景受影响?影响程度?) - 4. 给出具体的修复方案(代码级别的建议) - 5. 评估修复成本和风险 - - 输出:按优先级排序的修复计划,每项包含: - - 问题描述 + file:line - - 根因分析 - - 影响评估 - - 修复方案(具体代码变更建议) - - 成本/风险评估 diff --git a/.opencode/workflows/full-codebase-critical-review.yaml b/.opencode/workflows/full-codebase-critical-review.yaml deleted file mode 100644 index 6363aecc5b..0000000000 --- a/.opencode/workflows/full-codebase-critical-review.yaml +++ /dev/null @@ -1,251 +0,0 @@ -title: "OpenCode Full Codebase Review - Critical Bugs & Performance" -config: - name: full-codebase-critical-review - max_concurrency: 4 - max_node_replan_attempts: 3 - node_defaults: - worker_config: - timeout_ms: 600000 - nodes: - - id: review-concurrency - name: "Concurrency & Resource Leaks" - worker_type: general - depends_on: [] - report_to_parent: false - prompt_template: - inline: | - 你是并发与资源泄漏专家。对 opencode 代码库进行只读审查,只关注严重 bug 和性能问题。 - - ## 审查维度 - - 竞态条件:并发访问共享状态无保护、TOCTOU - - 资源泄漏:未关闭的进程/文件句柄/定时器/EventSource/AbortController - - 死锁/活锁:Effect 层中的循环依赖、Fiber 泄漏 - - 内存泄漏:无限增长的 Map/Array、未清理的订阅 - - ## 重点文件(来自知识图谱热点) - - packages/opencode/src/cli/cmd/run/stream.transport.ts (complexity 79, 43 callees) - - packages/opencode/src/cli/cmd/run/runtime.ts (complexity 46, 16 callees) - - packages/opencode/src/lsp/server.ts (unguarded_recursion in spawn) - - packages/opencode/src/session/ (session lifecycle) - - packages/core/src/process.ts (unguarded_recursion in describeCommand) - - packages/core/src/session/runner/ (LLM event publishing) - - packages/app/src/context/terminal.tsx (clearWorkspaceTerminals) - - packages/opencode/src/dag/ (DAG runtime, workflow execution) - - ## 工具使用 - 优先使用 codebase-memory-mcp 工具(project: "opencode-dag"): - - search_graph 查找函数定义 - - get_code_snippet 读取源码 - - trace_path 追踪调用链 - - query_graph 查询复杂模式 - - ## 输出要求 - 对每个发现输出: - - 严重性:CRITICAL / HIGH - - 文件:行号 - - 问题描述(一句话) - - 证据(代码片段或调用链) - - 潜在影响 - - 只报告 CRITICAL 和 HIGH 级别。不要报告风格问题、命名问题或低影响问题。 - 最多报告 10 个最严重的问题。 - - - id: review-performance - name: "Performance Hotspots" - worker_type: general - depends_on: [] - report_to_parent: false - prompt_template: - inline: | - 你是性能优化专家。对 opencode 代码库进行只读审查,只关注严重性能问题。 - - ## 审查维度 - - O(n²) 或更差的算法复杂度(循环内线性扫描) - - 热路径上的不必要分配(alloc_in_loop) - - 大数据集的同步阻塞操作 - - 不必要的重复计算/渲染 - - 内存膨胀(大对象未释放、缓存无上限) - - ## 重点文件(来自知识图谱热点) - - packages/app/src/pages/session.tsx (Page complexity 194!, transitive_loop_depth 6) - - packages/app/src/pages/session/timeline/message-timeline.tsx (complexity 119, 7 loops) - - packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts (complexity 47, 19 alloc_in_loop!) - - packages/opencode/src/cli/cmd/run/session-data.ts (reduceSessionData complexity 63) - - packages/session-ui/src/components/session-turn.tsx (complexity 44, transitive_loop_depth 7) - - packages/app/src/context/global-sync/event-reducer.ts (complexity 56) - - packages/tui/src/component/prompt/autocomplete.tsx (complexity 45, 2 alloc_in_loop) - - packages/console/app/src/routes/zen/util/handler.ts (complexity 113, cognitive 214) - - ## 工具使用 - 优先使用 codebase-memory-mcp 工具(project: "opencode-dag"): - - query_graph 查询性能模式 - - get_code_snippet 读取源码 - - trace_path 追踪热路径 - - ## 输出要求 - 对每个发现输出: - - 严重性:CRITICAL / HIGH - - 文件:行号 - - 问题描述(一句话) - - 证据(代码片段 + 复杂度数据) - - 性能影响估算 - - 只报告 CRITICAL 和 HIGH 级别。不要报告微小的优化机会。 - 最多报告 10 个最严重的问题。 - - - id: review-error-handling - name: "Error Handling & Data Corruption" - worker_type: general - depends_on: [] - report_to_parent: false - prompt_template: - inline: | - 你是错误处理与数据完整性专家。对 opencode 代码库进行只读审查,只关注可能导致数据损坏或系统崩溃的严重问题。 - - ## 审查维度 - - 静默吞掉错误导致状态不一致 - - 未处理的 Promise rejection / Effect 失败 - - 数据库/文件写入的原子性缺失 - - 状态机非法转换 - - 类型断言绕过安全检查(as any, as unknown) - - ## 重点文件 - - packages/opencode/src/dag/ (DAG 状态机、workflow 持久化) - - packages/opencode/src/session/ (session 生命周期、消息持久化) - - packages/opencode/src/server/ (HTTP API 错误处理) - - packages/core/src/session/runner/ (LLM 流式处理错误) - - packages/llm/src/route/executor.ts (retryStatusFailures - unguarded_recursion) - - packages/llm/src/protocols/shared.ts (removeNullSchemas - unguarded_recursion) - - packages/opencode/src/plugin/ (插件系统错误隔离) - - ## 工具使用 - 优先使用 codebase-memory-mcp 工具(project: "opencode-dag"): - - search_code 搜索错误处理模式 - - get_code_snippet 读取源码 - - trace_path 追踪错误传播路径 - - ## 输出要求 - 对每个发现输出: - - 严重性:CRITICAL / HIGH - - 文件:行号 - - 问题描述(一句话) - - 证据(代码片段) - - 数据损坏/崩溃场景 - - 只报告 CRITICAL 和 HIGH 级别。不要报告日志缺失或错误消息不清晰。 - 最多报告 10 个最严重的问题。 - - - id: review-critical-logic - name: "Critical Path Logic Bugs" - worker_type: general - depends_on: [] - report_to_parent: false - prompt_template: - inline: | - 你是逻辑正确性专家。对 opencode 代码库的核心路径进行只读审查,只关注可能导致功能完全失效的严重逻辑 bug。 - - ## 审查维度 - - 边界条件错误(off-by-one、空集合、null/undefined) - - 异步时序错误(await 缺失、竞态窗口) - - 状态不一致(读写不同源、缓存失效) - - 协议违规(HTTP/SSE/WebSocket 协议假设错误) - - 递归终止条件缺失 - - ## 重点文件 - - packages/opencode/src/dag/runtime/ (DAG 调度、节点生命周期) - - packages/opencode/src/session/v2/ (V2 session 核心) - - packages/core/src/session/runner/ (模型调用循环) - - packages/llm/src/route/ (LLM 路由、流式解析) - - packages/opencode/src/server/routes/ (API 路由) - - packages/cli/src/commands/handlers/serve.ts (unguarded_recursion in next) - - packages/opencode/src/acp/service.ts (16 callees, complexity 16) - - ## 工具使用 - 优先使用 codebase-memory-mcp 工具(project: "opencode-dag"): - - get_code_snippet 读取核心函数源码 - - trace_path 追踪关键调用链 - - query_graph 查询递归/循环模式 - - ## 输出要求 - 对每个发现输出: - - 严重性:CRITICAL / HIGH - - 文件:行号 - - 问题描述(一句话) - - 证据(代码片段 + 触发条件) - - 失效场景 - - 只报告 CRITICAL 和 HIGH 级别。不要报告代码可读性或设计偏好。 - 最多报告 10 个最严重的问题。 - - - id: arbitrate - name: "Arbiter - Synthesize & Rank" - worker_type: general - depends_on: [review-concurrency, review-performance, review-error-handling, review-critical-logic] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, summary, findings, required_actions, next_action] - properties: - verdict: - type: string - enum: [ACCEPT, REVISE, REJECT, BLOCKED] - summary: - type: string - findings: - type: array - items: - type: object - required: [severity, file, description, evidence] - properties: - severity: - type: string - enum: [CRITICAL, HIGH] - file: - type: string - line: - type: string - description: - type: string - evidence: - type: string - impact: - type: string - category: - type: string - required_actions: - type: array - items: - type: string - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: - type: array - prompt_template: - inline: | - 你是高级仲裁者。四个独立审查者分别从并发/资源泄漏、性能热点、错误处理/数据损坏、关键路径逻辑 bug 四个维度审查了 opencode 代码库。 - - ## 你的任务 - 1. 去重:合并指向同一根因的发现 - 2. 验证:对每个发现评估证据是否充分,标记证据不足的为"待验证" - 3. 排序:按影响范围和触发概率排序 - 4. 裁决: - - ACCEPT:所有发现都有充分证据,可以输出最终报告 - - REVISE:部分发现需要更深入的代码验证 - - REJECT:大部分发现证据不足 - - BLOCKED:无法完成审查 - - ## 输出 - 调用 submit_result 提交结构化结果: - - verdict: 裁决 - - summary: 一句话总结代码库健康度 - - findings: 去重排序后的发现列表(最多 15 个最严重的) - - required_actions: 建议的修复优先级 - - next_action: 工作流下一步操作 - - 严格只保留 CRITICAL 和 HIGH 级别的发现。每个 finding 必须有具体的文件路径和代码证据。 diff --git a/.opencode/workflows/perf-deep-review.yaml b/.opencode/workflows/perf-deep-review.yaml deleted file mode 100644 index 76c13c42a4..0000000000 --- a/.opencode/workflows/perf-deep-review.yaml +++ /dev/null @@ -1,376 +0,0 @@ -title: "Deep Performance Review - opencode-dag" -mode: deep -admission: - brief_revision: 1 - qa_mode: LIGHT - verdict: READY - brief: - goal: "深度性能审查 opencode-dag 项目,识别算法瓶颈、资源泄漏、I/O 阻塞、启动延迟、数据层低效" - scope: - in: - - "packages/core" - - "packages/opencode" - - "packages/tui" - - "packages/app" - - "packages/server" - - "packages/llm" - - "packages/sdk" - - "packages/client" - - "packages/plugin" - - "packages/schema" - out: - - "第三方依赖内部实现" - - "CI/CD 配置" - - "storybook mocks" - constraints: - - "只读审查,不修改任何文件" - - "所有发现必须引用 file:line 证据" - - "使用 codebase-memory-mcp 工具进行结构化分析" - assumptions: - - "工作树即审查目标" - - "main 分支当前 HEAD 为审查版本" - acceptance_criteria: - - "每个性能发现都有 file:line 证据支撑" - - "发现按严重程度排序(CRITICAL/HIGH/MEDIUM/LOW)" - - "提供可操作的修复建议" - - "区分已验证事实和未验证推测" - evidence_required: - - "代码引用(file:line)" - - "复杂度指标(loop_depth, linear_scan_in_loop 等)" - - "调用链分析" - risks: - - "遗漏跨包性能回归" - - "误报:静态分析无法确认运行时热点" - review_plan: - - "Wave 1: 探索性能关键表面(hotspots + architecture)" - - "Wave 2: 5 维度并行审查(算法/内存/IO/启动/数据层)" - - "Wave 3: 证据验证(检查 unverified_claims)" - - "Wave 4: 仲裁(去重、排序、结构化裁决)" - - "Wave 5: 深挖(verdict 驱动的条件节点)" - open_questions: [] - blocking_questions: [] -config: - name: perf-deep-review - max_concurrency: 8 - max_node_replan_attempts: 3 - max_total_nodes: 30 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 900000 - nodes: - - id: explore-hotspots - name: "Explore Performance Hotspots" - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - 你是性能探索专家。使用 codebase-memory-mcp 工具分析 opencode-dag 项目的性能热点。 - - 执行以下分析: - 1. 使用 query_graph 查找高复杂度函数: - - transitive_loop_depth >= 3 的函数 - - linear_scan_in_loop >= 1 的函数 - - alloc_in_loop >= 1 的函数 - - recursive 标记为 true 的函数 - 2. 使用 trace_path 追踪 fan_in 最高的 top 20 函数的调用链 - 3. 识别 packages/core, packages/opencode, packages/tui, packages/server 中的热路径 - - 输出格式: - - 按严重程度排序的热点列表 - - 每个热点包含:qualified_name, file:line, 复杂度指标, 调用频率估计 - - 标注跨包调用链中的性能瓶颈 - - 项目名:opencode-dag - - - id: explore-arch - name: "Explore Performance Architecture" - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - 你是架构性能分析专家。探索 opencode-dag 项目的性能相关架构设计。 - - 分析以下方面: - 1. 缓存策略:搜索 cache, memo, lru 相关实现,评估是否有无界缓存 - 2. 懒加载设计:搜索 dynamic import, lazy 模式,评估启动路径是否过重 - 3. 事件系统:搜索 EventBus, subscribe, emit 模式,评估是否有监听器泄漏风险 - 4. 连接管理:搜索 HttpClient, WebSocket, connection pool 模式 - 5. 序列化开销:搜索 JSON.parse, JSON.stringify, Schema.decode 在热路径中的使用 - - 使用 codebase-memory-mcp 的 search_code 和 search_graph 工具。 - 项目名:opencode-dag - - 输出:每个方面的架构现状 + 潜在性能风险 + file:line 证据 - - - id: review-algorithmic - name: "Review: Algorithmic Complexity" - worker_type: general - depends_on: [explore-hotspots] - prompt_template: - inline: | - 你是算法复杂度审查专家。基于以下热点探索结果,深入审查算法性能问题。 - - 探索结果: - {{explore-hotspots}} - - 审查重点: - 1. O(n²) 或更差的算法模式(嵌套循环 + 线性搜索) - 2. 不必要的重复计算(缺少 memoization) - 3. 递归深度风险(无尾递归优化、无深度限制) - 4. 数据结构选择不当(应该用 Map/Set 却用 Array 查找) - 5. 排序/过滤在热路径中的重复执行 - - 规则: - - 每个发现必须引用 file:line - - 使用 codebase-memory-mcp 的 get_code_snippet 验证具体实现 - - 无法验证的声明标记为 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 项目名:opencode-dag - - - id: review-memory - name: "Review: Memory & Resource Management" - worker_type: general - depends_on: [explore-hotspots, explore-arch] - prompt_template: - inline: | - 你是内存与资源管理审查专家。审查 opencode-dag 的内存泄漏和资源管理问题。 - - 架构探索结果: - {{explore-arch}} - - 审查重点: - 1. 事件监听器未清理(subscribe 无对应 unsubscribe) - 2. 无界缓存/集合持续增长(Map, Set, Array 无淘汰策略) - 3. 闭包持有大对象引用(阻止 GC) - 4. Effect 纤维泄漏(fork 后未 join/interrupt) - 5. 文件句柄/连接未关闭 - 6. 定时器(setInterval/setTimeout)未清理 - - 规则: - - 每个发现必须引用 file:line - - 使用 search_code 搜索 subscribe/addEventListener/setInterval 等模式 - - 无法验证的声明标记为 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 项目名:opencode-dag - - - id: review-io - name: "Review: I/O & Concurrency" - worker_type: general - depends_on: [explore-arch] - prompt_template: - inline: | - 你是 I/O 与并发审查专家。审查 opencode-dag 的异步性能和阻塞问题。 - - 架构探索结果: - {{explore-arch}} - - 审查重点: - 1. 同步阻塞操作在异步上下文中(readFileSync, execSync) - 2. 串行化的并行机会(await 循环 vs Promise.all/Effect.all) - 3. 缺少背压控制的流处理 - 4. HTTP 请求无超时/重试/连接复用 - 5. 大文件一次性读入内存(应流式处理) - 6. 锁竞争或过度序列化 - - 规则: - - 每个发现必须引用 file:line - - 使用 search_code 搜索 readFileSync/execSync/await.*for 等模式 - - 无法验证的声明标记为 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 项目名:opencode-dag - - - id: review-startup - name: "Review: Startup & Bundle Performance" - worker_type: general - depends_on: [explore-arch] - prompt_template: - inline: | - 你是启动与构建性能审查专家。审查 opencode-dag 的启动时间和 bundle 效率。 - - 架构探索结果: - {{explore-arch}} - - 审查重点: - 1. 入口点(packages/opencode/src/index.ts, packages/cli)的 import 深度 - 2. 应该动态导入但静态导入的重模块 - 3. 模块初始化副作用(顶层 await、立即执行的数据库连接) - 4. Layer 构建顺序中的不必要串行化 - 5. TUI 渲染首屏的关键路径长度 - 6. 重复初始化(同一服务被多个路径触发) - - 规则: - - 每个发现必须引用 file:line - - 使用 trace_path 追踪入口点的 outbound 调用深度 - - 无法验证的声明标记为 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 项目名:opencode-dag - - - id: review-data - name: "Review: Data Layer Performance" - worker_type: general - depends_on: [explore-hotspots, explore-arch] - prompt_template: - inline: | - 你是数据层性能审查专家。审查 opencode-dag 的数据库和序列化性能。 - - 热点探索结果: - {{explore-hotspots}} - - 架构探索结果: - {{explore-arch}} - - 审查重点: - 1. SQLite 查询缺少索引(WHERE/JOIN 字段) - 2. N+1 查询模式(循环中逐条查询) - 3. 大结果集未分页(SELECT * 无 LIMIT) - 4. 频繁序列化/反序列化(JSON.parse/stringify 在热路径) - 5. Drizzle ORM 使用不当(缺少事务批量操作) - 6. 事件存储的读写放大 - - 规则: - - 每个发现必须引用 file:line - - 使用 search_code 搜索 db.select/db.insert/JSON.parse 等模式 - - 检查 packages/core/src 和 packages/opencode/src 中的数据访问层 - - 无法验证的声明标记为 unverified_claims - - 按 CRITICAL/HIGH/MEDIUM/LOW 分级 - - 项目名:opencode-dag - - - id: verify-claims - name: "Verify Performance Claims" - worker_type: verify - depends_on: [review-algorithmic, review-memory, review-io, review-startup, review-data] - required: true - prompt_template: - inline: | - 你是性能声明验证专家。验证以下 5 个审查报告中所有标记为 unverified_claims 的声明,以及所有 CRITICAL/HIGH 级别发现。 - - 审查报告: - 算法复杂度:{{review-algorithmic}} - 内存资源:{{review-memory}} - I/O并发:{{review-io}} - 启动性能:{{review-startup}} - 数据层:{{review-data}} - - 验证方法: - 1. 对每个 unverified_claim,使用 codebase-memory-mcp 的 get_code_snippet 读取实际代码 - 2. 对每个 CRITICAL/HIGH 发现,确认 file:line 引用准确且问题真实存在 - 3. 检查是否有误报(代码已修复、有缓解措施、或分析错误) - 4. 使用 query_graph 验证复杂度指标 - - 输出: - - VERIFIED: 确认为真的发现列表 - - REFUTED: 被证伪的声明列表(附原因) - - UNCERTAIN: 无法静态确认的声明(需要运行时 profiling) - - 项目名:opencode-dag - - - id: arbitrate - name: "Performance Arbitration" - worker_type: general - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, summary, findings, required_actions, next_action] - properties: - verdict: - type: string - enum: [ACCEPT, REVISE, REJECT, BLOCKED] - summary: - type: string - findings: - type: array - items: - type: object - required: [severity, category, description, evidence, recommendation] - properties: - severity: - type: string - enum: [CRITICAL, HIGH, MEDIUM, LOW] - category: - type: string - description: - type: string - evidence: - type: string - recommendation: - type: string - required_actions: - type: array - items: - type: string - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: - type: array - items: - type: string - prompt_template: - inline: | - 你是性能审查仲裁专家。基于已验证的证据,做出最终裁决。 - - 验证结果: - {{verify-claims}} - - 仲裁规则: - 1. 只采纳 VERIFIED 的发现作为正式 findings - 2. REFUTED 的声明不得出现在 findings 中 - 3. UNCERTAIN 的发现降级为 MEDIUM 并标注"需要运行时验证" - 4. 去重:相同根因的多个表现合并为一条 - 5. 按严重程度排序:CRITICAL > HIGH > MEDIUM > LOW - 6. 每条 finding 必须有可操作的修复建议 - - Verdict 判定: - - ACCEPT: 无 CRITICAL/HIGH 发现,项目性能状况良好 - - REVISE: 存在 HIGH 发现但无 CRITICAL,需要改进 - - REJECT: 存在 CRITICAL 发现,需要立即修复 - - BLOCKED: 无法完成审查(工具不可用等) - - next_action: - - ACCEPT → operation: complete - - REVISE/REJECT → operation: extend, targets: 需要深挖的具体模块/文件 - - 使用 submit_result 提交结构化裁决。 - 项目名:opencode-dag - - - id: deep-dive - name: "Performance Deep Dive" - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict != "ACCEPT"' - report_to_parent: true - prompt_template: - inline: | - 你是性能深挖专家。仲裁未通过,需要对确认的问题区域进行深入分析。 - - 仲裁裁决: - {{arbitrate}} - - 任务: - 1. 对每个 CRITICAL/HIGH finding,追踪完整的调用链(使用 trace_path) - 2. 量化影响:估算受影响的用户操作路径 - 3. 提供具体的修复方案(代码级别,不是泛泛建议) - 4. 识别修复的依赖顺序(哪些必须先修) - 5. 评估修复风险(是否可能引入回归) - - 输出: - - 每个 CRITICAL/HIGH 问题的完整修复计划 - - 修复优先级排序 - - 风险评估 - - 项目名:opencode-dag diff --git a/.opencode/workflows/review-dag-subsystem.yaml b/.opencode/workflows/review-dag-subsystem.yaml deleted file mode 100644 index 3c5cd7245d..0000000000 --- a/.opencode/workflows/review-dag-subsystem.yaml +++ /dev/null @@ -1,206 +0,0 @@ -title: "DAG Subsystem Review" -config: - name: dag-subsystem-review - max_concurrency: 5 - max_node_replan_attempts: 3 - node_defaults: - required: false - report_to_parent: false - worker_config: - timeout_ms: 600000 - nodes: - - id: explore-structure - name: explore-structure - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - Explore the DAG orchestration subsystem at packages/opencode/src/dag/. - Map: - 1. All files and their responsibilities (one line each) - 2. Internal module boundaries and dependency direction between files - 3. Public API surface (exports consumed by other packages/modules) - 4. External dependencies (what this subsystem imports from outside) - Output a structured inventory with file paths. - - - id: explore-runtime - name: explore-runtime - worker_type: explore - depends_on: [] - required: true - prompt_template: - inline: | - Explore the runtime behavior of the DAG subsystem at packages/opencode/src/dag/. - Map: - 1. Entry points: how workflows are created, started, and scheduled - 2. State machine: workflow and node lifecycle states, transitions - 3. Concurrency model: how parallel nodes execute, locking, coordination - 4. Error handling and recovery: crash recovery, replan, pause/resume - 5. Integration points: how the DAG connects to sessions, tools, events - Output structured findings with file:line references. - - - id: review-arch - name: review-arch - worker_type: review - depends_on: [explore-structure, explore-runtime] - prompt_template: - inline: | - Architecture review of the DAG subsystem at packages/opencode/src/dag/. - Focus: - - Layering violations and dependency direction - - Coupling between modules (circular deps, god objects) - - Separation of concerns (state vs execution vs persistence) - - Extension points and their adequacy - - Whether the module boundaries match the conceptual model - - RULES: - - Cite file:line for every finding - - List claims you could not verify as unverified_claims - - Do NOT modify any file - Output: findings list with severity (HIGH/MEDIUM/LOW), evidence, and unverified_claims section. - - - id: review-logic - name: review-logic - worker_type: review - depends_on: [explore-structure, explore-runtime] - prompt_template: - inline: | - Logic correctness review of the DAG subsystem at packages/opencode/src/dag/. - Focus: - - State machine completeness: unreachable states, missing transitions - - Race conditions in concurrent node scheduling - - Edge cases: empty graphs, single node, max concurrency boundaries - - Error propagation: does a node failure correctly affect dependents? - - Replan/recovery correctness: can state become inconsistent? - - Condition evaluation and skip semantics - - RULES: - - Cite file:line for every finding - - List claims you could not verify as unverified_claims - - Do NOT modify any file - Output: findings list with severity (HIGH/MEDIUM/LOW), evidence, and unverified_claims section. - - - id: review-conventions - name: review-conventions - worker_type: review - depends_on: [explore-structure, explore-runtime] - prompt_template: - inline: | - Code style and conventions review of the DAG subsystem at packages/opencode/src/dag/. - Check against the project AGENTS.md style guide: - - No unnecessary destructuring (prefer dot notation) - - No import aliases or star imports - - const over let, ternaries over reassignment - - No else statements (prefer early returns) - - Effect conventions: named service bindings, no nested yields - - Drizzle schema: snake_case fields - - Single-use helpers should be inlined - - Prefer Bun APIs - - No comments unless non-obvious constraints - - RULES: - - Cite file:line for every finding - - List claims you could not verify as unverified_claims - - Do NOT modify any file - Output: findings list with severity (HIGH/MEDIUM/LOW), evidence, and unverified_claims section. - - - id: verify-claims - name: verify-claims - worker_type: general - depends_on: [review-arch, review-logic, review-conventions] - required: true - prompt_template: - inline: | - You are the claim verification step. Three reviewers produced findings about - packages/opencode/src/dag/. Your job: - - 1. Collect all unverified_claims from all three reviews - 2. Collect all HIGH severity findings - 3. For each, open the actual source code and verify whether the claim is TRUE, FALSE, or PARTIALLY TRUE - 4. For disputed findings (where reviewers disagree), check the code and state which is correct - - RULES: - - Read the actual files. Do not speculate. - - For each claim, output: claim text, source file:line, verdict (CONFIRMED/REFUTED/PARTIAL), evidence - - Do NOT modify any file - - Upstream reviews: - {{review-arch}} - {{review-logic}} - {{review-conventions}} - - - id: arbitrate - name: arbitrate - worker_type: general - depends_on: [verify-claims] - required: true - report_to_parent: true - output_schema: - type: object - required: [verdict, summary, findings, required_actions, next_action] - properties: - verdict: - type: string - enum: [ACCEPT, REVISE, REJECT, BLOCKED] - summary: - type: string - findings: - type: array - items: - type: object - properties: - severity: { type: string } - description: { type: string } - evidence: { type: string } - verified: { type: boolean } - required_actions: - type: array - items: { type: string } - next_action: - type: object - required: [operation, targets] - properties: - operation: - type: string - enum: [continue, extend, replan, complete, stop] - targets: - type: array - items: { type: string } - prompt_template: - inline: | - You are the final arbiter for the DAG subsystem review. - - Based on the verified claims, rule finding-by-finding: - 1. Only include findings that were CONFIRMED or PARTIAL in the verification step - 2. Deduplicate overlapping findings across reviewers - 3. Rank by severity and impact - 4. Emit a structured verdict: - - ACCEPT: no HIGH findings survived verification - - REVISE: HIGH findings exist but are fixable with targeted changes - - REJECT: fundamental design issues require rethinking - - BLOCKED: cannot determine without additional information - 5. Provide required_actions (concrete fix steps) and next_action - - Verified evidence: - {{verify-claims}} - - - id: deep-dive - name: deep-dive - worker_type: general - depends_on: [arbitrate] - condition: 'arbitrate.output.verdict != "ACCEPT"' - report_to_parent: true - prompt_template: - inline: | - The arbiter did not accept the DAG subsystem review. - For each required_action in the arbiter's findings: - 1. Open the relevant source files - 2. Verify the problem exists as described - 3. Propose a concrete, minimal fix with code-level detail - 4. Identify any secondary effects of the proposed fix - - Do NOT modify any file. Output an evidence-backed action plan. - - Arbiter findings: - {{arbitrate}} diff --git a/.scratch/batch-a/issues/01-q1-escalation-pending-lifecycle.md b/.scratch/batch-a/issues/01-q1-escalation-pending-lifecycle.md deleted file mode 100644 index 62c43fde33..0000000000 --- a/.scratch/batch-a/issues/01-q1-escalation-pending-lifecycle.md +++ /dev/null @@ -1,17 +0,0 @@ -# 01 — Q1:escalation_pending 裁决旗生命周期闭环 - -**What to build:** 节点到终态(completed/failed/aborted)或被取消时,裁决状态旗 escalation_pending 必清零——裁决不可能悬挂超过它所属的裁决周期。wake_reported 投递旗不受影响(两旗正交)。终态清旗发生在事件折叠侧,与既有的 NodeStarted/NodeRestarted 清旗点共同构成完整生命周期。 - -规格依据:ADR-0001-escalation-pending-semantics + 节点生命周期转移表 v2(Q1 行)。 - -**Blocked by:** None — can start immediately - -**Status:** closed(PR #186,merge commit `4ddeaf2fc`) - -**Completion evidence:** 批次 A 实现与测试随 PR #186 合入 `dev`,并随 PR #188 通过 main 全量门禁。 - -- [x] 节点终态转移(completed/failed/aborted)与取消路径清 escalation_pending -- [x] wake_reported 在清旗路径上不被触碰(两旗正交测试) -- [x] 已有 NodeStarted/NodeRestarted 清旗点保持不回退 -- [x] replay/恢复场景下清旗经事件折叠重放一致 -- [x] dag 测试套件 + typecheck 绿 diff --git a/.scratch/batch-a/issues/02-q2-delivery-gated-retime.md b/.scratch/batch-a/issues/02-q2-delivery-gated-retime.md deleted file mode 100644 index 955ae776e6..0000000000 --- a/.scratch/batch-a/issues/02-q2-delivery-gated-retime.md +++ /dev/null @@ -1,17 +0,0 @@ -# 02 — Q2:送达门控 re-time(watchdog 提案者门控) - -**What to build:** wake 已送达未裁决期间,watchdog 不得抢占式 re-time。按 v2 机制实现:在 re-time 唯一写路径的发起点加 skip 合取项(escalationPending 且裁决未完成 ⇒ 跳过),而非放行析取项——deadline 驱动的初始升级路径不受影响,watchdog 保持纯提议者(提案不改状态)。 - -规格依据:ADR-0002-delivery-gated-retime(Round 2 修订版)+ 转移表 v2 G1 门控不变式。旧机制(放行析取臂)已证伪,禁止复用其表述。 - -**Blocked by:** None — can start immediately - -**Status:** closed(PR #186,merge commit `4ddeaf2fc`) - -**Completion evidence:** 批次 A 实现与测试随 PR #186 合入 `dev`,并随 PR #188 通过 main 全量门禁。 - -- [x] skip 合取项落在 re-time 唯一发起点,全 re-time 触发路径逐条覆盖(测试枚举,不只抄规格) -- [x] 初始升级(deadline ⟹ 首次 wake)不受门控影响 -- [x] 裁决写入后 re-time 能力恢复的测试 -- [x] watchdog 无状态写(仅提案)的断言保持 -- [x] dag 测试套件 + typecheck 绿 diff --git a/.scratch/batch-a/issues/03-q3-node-deadline-extended-event.md b/.scratch/batch-a/issues/03-q3-node-deadline-extended-event.md deleted file mode 100644 index f09f418f09..0000000000 --- a/.scratch/batch-a/issues/03-q3-node-deadline-extended-event.md +++ /dev/null @@ -1,18 +0,0 @@ -# 03 — Q3:NodeDeadlineExtended durable 事件 + guard 前移命令层 - -**What to build:** deadline 延长成为 durable 事件:NodeDeadlineExtended(nodeID + 新 deadline)入事件日志,废除直写 deadline 的旧路径。guard(延长是否被允许)前移到命令层执行——0 行 = 命令失败,编排器即时可观察(错误即状态);事件只记录成功,projector 保持纯幂等折叠(确定性重放)。guard 拒绝不是转移,不进事件日志。 - -规格依据:ADR-0003-node-deadline-extended-event(Round 2 修订版)+ 转移表 v2 T9/T11。旧机制(publish 返回行数契约)已证伪——发布链丢弃返回值,禁止复用。 - -**Blocked by:** 01 — Q1:escalation_pending 裁决旗生命周期闭环(projector 折叠侧写集串行) - -**Status:** closed(PR #186,merge commit `4ddeaf2fc`) - -**Completion evidence:** 批次 A 实现与测试随 PR #186 合入 `dev`,并随 PR #188 通过 main 全量门禁。 - -- [x] Schema 定义 NodeDeadlineExtended + 入 EventManifest.Definitions -- [x] 命令层执行 guard:拒绝时命令失败并携带 typed 错误,编排器可区分拒绝与成功 -- [x] 直写 deadline 旧路径废除(无遗留调用方) -- [x] projector 纯折叠:无事件发布、无返回值契约依赖 -- [x] 恢复/replay 一致性测试(事件日志重放 ⟺ 活跃态) -- [x] dag 测试套件 + typecheck 绿 diff --git a/.scratch/batch-a/issues/04-q3-sdk-regen-consumers.md b/.scratch/batch-a/issues/04-q3-sdk-regen-consumers.md deleted file mode 100644 index b099b39682..0000000000 --- a/.scratch/batch-a/issues/04-q3-sdk-regen-consumers.md +++ /dev/null @@ -1,15 +0,0 @@ -# 04 — Q3 派生:SDK 再生 + 消费者对齐 - -**What to build:** 03 落地后再生 JS SDK,使 NodeDeadlineExtended 进入生成的事件联合类型;对齐一切消费事件流/类型联合的消费者(TUI sync、httpapi-exercise 场景若涉及),保证 CI 生成物新鲜度门禁与 HttpAPI 契约门禁通过。 - -**Blocked by:** 03 — Q3:NodeDeadlineExtended durable 事件 + guard 前移命令层 - -**Status:** closed(PR #186,merge commit `4ddeaf2fc`) - -**Completion evidence:** 批次 A 生成物与消费者更新随 PR #186 合入 `dev`,并随 PR #188 通过 main 全量门禁。 - -- [x] SDK 再生脚本执行,生成物提交 -- [x] 事件联合类型包含 NodeDeadlineExtended,消费方编译绿 -- [x] `check:generated`(SDK + client)零 diff -- [x] 涉及响应/事件形状的 httpapi-exercise 场景已更新(如有) -- [x] 全量单元测试(含 httpapi 契约)绿 diff --git a/.scratch/batch-a/issues/05-s5-workflow-lock-timeout.md b/.scratch/batch-a/issues/05-s5-workflow-lock-timeout.md deleted file mode 100644 index d568bd49c1..0000000000 --- a/.scratch/batch-a/issues/05-s5-workflow-lock-timeout.md +++ /dev/null @@ -1,17 +0,0 @@ -# 05 — S5:withWorkflowLock 一行超时(奥卡姆版) - -**What to build:** 工作流锁获取加 30 秒上限——withWorkflowLock 外层一行 Effect.timeout,复用 TimeoutException:零新错误类、零 per-caller 改动、watchdog 零特殊化(自续间隔秒级重试天然继续,延长计数只在成功延长时 +1)。禁止引入新错误类型或按调用方分支。 - -规格依据:ADR-0004-lock-timeout-occams + CONTEXT.md 决策树 Q6。 - -**Blocked by:** None — can start immediately - -**Status:** closed(PR #186,merge commit `4ddeaf2fc`) - -**Completion evidence:** 批次 A 实现与测试随 PR #186 合入 `dev`,并随 PR #188 通过 main 全量门禁。 - -- [x] 唯一改动点在 withWorkflowLock 包装层(一行 + 常量) -- [x] 30s 超限产生 TimeoutException,编排器按既有 error_class 分诊规则处置 -- [x] 无新错误类、无 per-caller 分支的断言 -- [x] watchdog 自续行为在锁超时后仍正确的测试 -- [x] dag 测试套件 + typecheck 绿 diff --git a/.scratch/batch-a/issues/06-flaky-stdout-pollution.md b/.scratch/batch-a/issues/06-flaky-stdout-pollution.md deleted file mode 100644 index 57abd202ff..0000000000 --- a/.scratch/batch-a/issues/06-flaky-stdout-pollution.md +++ /dev/null @@ -1,16 +0,0 @@ -# 06 — Flaky:stdout 污染族根治(run-process ×9 + ShareNext 污染分量) - -**What to build:** 非交互子进程 stdout 断言失败的根因修复:测试 LLM/fixture 输出污染了被测进程的 stdout,导致 `expect(stdout).toBe("...")` 精确匹配族在慢主机/并发下失败。按豁免清单已定位的根因修复(污染源隔离或断言确定性化),不削弱断言语义。 - -规格依据:.opencode/promotion-review-round1/exemption-manifest.md(13 项中 run-process ×9 + ShareNext 污染分量)+ Round 1/2 深审根因记录。 - -**Blocked by:** None — can start immediately - -**Status:** closed(PR #186,merge commit `4ddeaf2fc`) - -**Completion evidence:** flaky 根因修复与验证随 PR #186 合入 `dev`,并随 PR #188 通过 main 全量门禁。 - -- [x] 污染源定位经可复现测试验证(修复前红、修复后绿) -- [x] run-process 9 项断言不削弱、不删除,本地重复跑(≥5 次)稳定绿 -- [x] ShareNext 的 stdout 污染分量同步修复(计时问题归 07 票) -- [x] opencode 包测试套全绿(除豁免清单剩余项) diff --git a/.scratch/batch-a/issues/07-flaky-sharenext-timing.md b/.scratch/batch-a/issues/07-flaky-sharenext-timing.md deleted file mode 100644 index ed5980f57e..0000000000 --- a/.scratch/batch-a/issues/07-flaky-sharenext-timing.md +++ /dev/null @@ -1,15 +0,0 @@ -# 07 — Flaky:ShareNext 计时预算稳定化 - -**What to build:** ShareNext 合并测试(15s 超时)在慢 CI 主机上压线失败。以发布就绪信号等待替代墙体时间等待(测试 AGENTS.md 的 pollWithTimeout 惯用法——等信号不等 sleep),或给出经证据支撑的预算调整;禁止单纯放大超时掩盖真实竞态。 - -规格依据:exemption-manifest.md(ShareNext 项)+ 测试 AGENTS.md「Synchronizing With Concurrent Work」节。 - -**Blocked by:** 06 — Flaky:stdout 污染族根治(同一测试文件,写集串行) - -**Status:** closed(PR #186,merge commit `4ddeaf2fc`) - -**Completion evidence:** flaky 稳定化与验证随 PR #186 合入 `dev`,并随 PR #188 通过 main 全量门禁。 - -- [x] 修复走信号等待惯用法;若改预算须附 CI 计时证据 -- [x] 本地重复跑(≥5 次)+ 模拟负载下稳定绿 -- [x] 无新增 Effect.sleep 等待 forked fiber 的反模式 diff --git a/.scratch/batch-a/issues/08-flaky-workspace-timing.md b/.scratch/batch-a/issues/08-flaky-workspace-timing.md deleted file mode 100644 index 0402fbbc16..0000000000 --- a/.scratch/batch-a/issues/08-flaky-workspace-timing.md +++ /dev/null @@ -1,15 +0,0 @@ -# 08 — Flaky:workspace sync 计时预算稳定化 - -**What to build:** workspace sync 历史回放测试(20s 超时)在慢 CI 主机上压线失败。处置同 07 票:信号等待替代墙体时间,或证据支撑的预算调整。若根因并非计时(先诊断后修——诊断优先于理论, tight feedback loop 先行),按实际根因修复并记录。 - -规格依据:exemption-manifest.md(workspace sync 项)。 - -**Blocked by:** None — can start immediately - -**Status:** closed(PR #186,merge commit `4ddeaf2fc`) - -**Completion evidence:** flaky 稳定化与验证随 PR #186 合入 `dev`,并随 PR #188 通过 main 全量门禁。 - -- [x] 先复现并确认根因(计时 vs 其他),根因记录入票 -- [x] 修复后本地重复跑(≥5 次)+ 模拟负载下稳定绿 -- [x] 无新增固定 sleep 反模式 diff --git a/.scratch/batch-a/issues/09-promote-dev-to-main.md b/.scratch/batch-a/issues/09-promote-dev-to-main.md deleted file mode 100644 index bca0d7dada..0000000000 --- a/.scratch/batch-a/issues/09-promote-dev-to-main.md +++ /dev/null @@ -1,14 +0,0 @@ -# 09 — 收束:dev → main 晋级 PR - -**What to build:** 全部批次 A 与 flaky 票合入 dev 且 dev CI 真绿后,开 dev→main 晋级 PR:全量门禁(Typecheck + Unit Tests + E2E linux + E2E windows)通过即合并,使 main 恢复可 release-fork 状态。 - -**Blocked by:** 01、02、03、04、05、06、07、08 全部合入 dev - -**Status:** closed(PR #188,merge commit `e837dcbfa`) - -**Completion evidence:** dev→main 晋级 PR #188 的 Typecheck、Unit Tests (linux)、E2E Tests (linux/windows) 全部通过并合入。 - -- [x] dev 最新 push 的 CI 四项检查全绿(Typecheck、Unit、E2E linux、E2E windows) -- [x] 豁免清单清零或逐项重新裁决留档 -- [x] PR 描述附批次 A 交付清单(Q1/Q2/Q3/S5 + flaky 根因修复)与两轮深审 PASS 证据链接 -- [x] 合并后 main 可手动 release-fork diff --git a/.scratch/batch-a/issues/10-backlog-phantom-cancelled-state.md b/.scratch/batch-a/issues/10-backlog-phantom-cancelled-state.md deleted file mode 100644 index 7f54f181c3..0000000000 --- a/.scratch/batch-a/issues/10-backlog-phantom-cancelled-state.md +++ /dev/null @@ -1,28 +0,0 @@ -# 10 — Backlog:phantom cancelled 节点态(N1-T5 规格-实现漂移) - -**What to build:** 消除节点级状态空间中 phantom `cancelled` 态的规格-实现漂移,二选一收敛: -- 方案 A(对齐实现):状态空间与转移表 T5 取消节点级 `cancelled` 目标态——NodeCancelled 事件维持现投影(status=failed + error_reason 承载取消语义),T5 改写为 to=failed(cancelled);同步转移表 v2、CONTEXT.md 状态机词汇。 -- 方案 B(对齐规格):projector 产出真正的节点级 `cancelled` 终态,审计全部读节点状态的消费方(调度资格、wake 汇总、TUI 展示、恢复路径)对新终态的处置,测试覆盖。 -先做设计裁决(影响面 A≪B:B 触及终态判定函数 isNodeTerminalStatus 与全部消费方),再按裁决实施。 - -**来源证据(批次 A 续作图终审 DEDUP-N1-T5,severity=low,四方接受):** -- projector.ts:35,348:NodeCancelled → status=failed;无任何投影产出节点级 cancelled -- 转移表 v2 T5 声明 to=cancelled——规格侧存在、实现侧不可达 -- 纠错记录:reasoner 曾以 store.ts:452/462 为证,被 review-logic 纠正——那两处查的是 WorkflowTable,工作流级 cancelled 是合法状态,与节点级无关 -- 运行时影响:零(取消语义经 error_reason 保留);批次 A 仅向 cancelled 投影补 EP:false,漂移系既有 -- accepted_by:reasoning N1(附错误证据)/ review-logic(PARTIAL,证据已纠正)/ review-architecture I1 / review-tests GAP-T5 - -**Blocked by:** None(独立设计决策) - -**Status:** closed(方案 A 已实施 — PR #189,commit 67d1ca2b1) - -## 裁决与实施记录(batch-a-residuals DAG,终审 PASS) -- 裁决:方案 A(对齐实现)——消费方核验确认仅 TUI 存在 phantom dead branch 读节点级 cancelled,投影写 status=failed 故永不触发,无真实依赖 -- T5 改写 to=failed(cancelled);状态空间删除节点级 cancelled 目标态(保留工作流级);CONTEXT.md 同步;projector 投影注释固化契约 -- 新增 core 测试断言 NodeCancelled 重放 → status=failed + error_reason 承载取消语义 -- 对抗审查:检察官/辩护人/证据矩阵三路 + 第四方 claim 核验,终审 PASS - -- [x] 设计裁决 A/B(含消费方影响面清单) -- [x] 按裁决实施 + 测试 -- [x] 转移表 v2 与 CONTEXT.md 状态机词汇同步 -- [x] typecheck + dag 套件绿 diff --git a/.scratch/batch-a/issues/11-backlog-spurious-t8-budget.md b/.scratch/batch-a/issues/11-backlog-spurious-t8-budget.md deleted file mode 100644 index 8b01cd76c3..0000000000 --- a/.scratch/batch-a/issues/11-backlog-spurious-t8-budget.md +++ /dev/null @@ -1,27 +0,0 @@ -# 11 — Backlog:F8 spurious T8——watchdog 陈旧读每周期耗一个 cap 预算单位 - -**What to build:** 消除(或显式预算化)F8 spurious T8:watchdog 以陈旧快照读判定超时 → 发延长 → 重放 T8,每发生一次消耗一个 max_timeout_extensions 预算单位。代码自我记录为 cosmetic(loop.ts:860-868 注释:"cap accounting still holds because the count did climb"),但语义上节点并未真正获得有效延长窗口却消耗了预算——极端场景下提前耗尽延长预算。 -修复方向(裁决后实施): -- 方案 A:延长判定前在 workflow lock 内重读节点状态(deadline/状态新鲜读),陈旧读不发延长——根治,注意不引入锁争用回归 -- 方案 B:把"陈旧读引发的延长"与真实延长分开计数(预算只认真实延长)——改计数语义,影响面含恢复/审计 -- 方案 C(维持现状 + 显式化):把预算消耗语义写入 ADR 与转移表,加监控/测试断言行为,不改机制 - -**来源证据(批次 A 续作图终审 DEDUP-N3,severity=low,双方接受):** -- loop.ts:860-868(机制与自我记录注释)、spawn.ts:192-198(watchdog 读路径) -- Q2 送达门控落地后仍存在(门控管的是 re-time 发起,不管陈旧读判定) -- 既有问题,非批次 A 回归 -- accepted_by:reasoning N3(Notable)/ review-logic(CONFIRM pre-existing cosmetic) - -**Blocked by:** None(独立设计决策) - -**Status:** closed(方案 A 已实施 — PR #189,commit b35630486) - -## 裁决与实施记录(batch-a-residuals DAG,终审 PASS) -- 裁决:方案 A(锁内新鲜读)——watchdog 以 staleDeadlineMs 守卫判定:陈旧读触发的延长被新鲜读否决时不发布 NodeDeadlineExtended、不递增 timeoutExtensions -- 真红→绿:test/dag/dag-retime-stale-read.test.ts,case1(陈旧读抑制)vs case2(真实超时延长)对照 -- 不变式保持:-2/0/1 三值契约、N1 监督不变式(running 节点总有 watcher) -- 对抗审查两项开放担忧裁决:U1(Effect.timeout 败者中断产生孤立节点)经 Effect v4 源码分析 REFUTED(TimeoutError=Cause.Fail,raceAllFirst 败者中断不经 hasInterrupts 匹配);N1(抑制守卫 >staleDeadlineMs 缺 >now)经三方一致论证为有界自愈(下一 tick 必发布,延迟 ≤1 escalateIntervalMs,预算不丢) - -- [x] 裁决修复方向(A/B/C,含锁交互与预算语义影响面) -- [x] 按裁决实施 + 测试(含陈旧读复现场景) -- [x] typecheck + dag 套件绿 diff --git a/.scratch/batch-b/README.md b/.scratch/batch-b/README.md deleted file mode 100644 index 7a94dbb54e..0000000000 --- a/.scratch/batch-b/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# 批次 B 执行台账 - -**规划基线:** `dev@3e8368f37`(PR #190) - -**当前状态:** 规格与票据已就绪;实现代码尚未开始。 - -## 审计结论 - -- 批次 A 已经 PR #188 晋级 `main`;残余修复 PR #189 与交接 PR #190 已进入 `dev`。 -- U-1/U-2/mid-stream-stall 的 OpenSpec 已完成并通过校验;仓库规定 `/openspec/` local-only,跨 worktree 使用已追踪镜像 `.scratch/batch-b/abort-path-contracts.md`。 -- 原始 `.opencode/promotion-review-round1/` 与 `.opencode/.dag-specs/evidence/` 是未追踪本地文件;跨 worktree 统一引用 `.scratch/batch-b/evidence.md`。 -- O1 不是直接实现票:离线降级已完成,剩余 LKG 的持久化、键、失效和安全边界需先写小规格。 -- S7 只有静态 bug 气味;先诊断,不能复现就无代码收口。P8 维持批 C 的观测候选,不阻塞批 B。 - -## 串行顺序 - -1. 01 U-1 → 02 U-2 → 03 mid-stream-stall;02/03 写同一测试文件,禁止并行。 -2. 04 F3 → 05 F4;两票只清测试债,不顺带改运行时。 -3. 06 O1 规格 → 07 O1 实现。 -4. 08 S7 诊断;只有红灯成立才新建独立修复票。 -5. 批 B 全部处置 + 批 C P8 观测记录关闭后,执行 09 的 dev→main 晋级与 release-fork。 - -每票从最新 `dev` 创建符合仓库规则的分支,单独 PR → `dev`,单独新任务执行。票据完成时将状态改为 `closed`,附 PR、merge commit 与验证命令。 diff --git a/.scratch/batch-b/abort-path-contracts.md b/.scratch/batch-b/abort-path-contracts.md deleted file mode 100644 index eaaa7dde92..0000000000 --- a/.scratch/batch-b/abort-path-contracts.md +++ /dev/null @@ -1,53 +0,0 @@ -# 批次 B:abort-path integrity 规格 - -**Status:** accepted -**Applies to:** 01 U-1、02 U-2、03 Transport mid-stream-stall -**Local OpenSpec source:** `openspec/changes/batch-b-abort-path-contracts/`(仓库规定 local-only;`openspec validate --changes` 已通过) - -## 目标 - -三个边界目前只有实现/注释声明,没有真实失败路径证据:Session fork 嵌套发布失败时的批次回滚、HTTP timeout 对真实连接的取消传播、合法首帧后的逐帧间隔 timeout。本规格只补集成测试;红灯暴露违约时,才允许做对应契约所需的最小生产修复。 - -## Requirement 1:fork 复制批次原子回滚 - -系统 SHALL 在 `Session.fork` 的消息/part 复制批次中保持原子性:任一嵌套 durable event 发布失败时,同一外层事务内此前写入的复制事件及其投影全部回滚。 - -### Scenario:部分复制后嵌套发布失败 - -- WHEN fork 已创建目标 session,至少一个 message/part 发布完成,随后嵌套发布失败 -- THEN fork 调用失败,目标 session 不存在本批复制出的 message/part projections 与 durable copy events -- THEN 源 session 的 message/part 保持不变 -- THEN 复制事务外已经提交的目标 Session Created 记录可以保留 - -**设计裁决:** 必须扩展 `packages/opencode/test/session/fork-batch.test.ts` 的真实 SQLite fixture;adapter-only savepoint 测试不足以证明 EventV2/projector 共用外层连接。 - -## Requirement 2:provider timeout 取消真实 transport - -系统 MUST 在 provider HTTP timeout 到期时,以现有 Transport/Timeout 结束 LLM stream,并取消仍在进行的真实 HTTP response stream,使 provider 端观察到连接或响应体取消。 - -### Scenario:真实 provider response 超时后仍保持打开 - -- WHEN loopback provider 已接收请求并返回超过 timeout 仍保持打开的 response stream -- THEN 客户端在有界时间内以现有 `LLMError` Transport/Timeout 失败 -- THEN provider 在有界时间内观察到 response cancellation 或等价 request abort - -**设计裁决:** 使用真实 `Bun.serve` loopback 与生产 fetch-backed client。以 response cancellation 为确定性主信号,request abort 可作补充;内存 HttpClient 和显式 Fiber interrupt 都不能替代本场景。 - -## Requirement 3:timeout 约束每个帧间隔 - -系统 SHALL 将 stream timeout 作为相邻数据帧之间的最大间隔,而不是只覆盖 response headers 或首帧等待。 - -### Scenario:合法首帧后永久停顿 - -- WHEN provider 在 timeout 内发出至少一个合法 SSE frame,随后不关闭且不再发送数据 -- THEN timeout 前到达的 frame 已交付消费者 -- THEN 停顿超过 timeout 后,stream 以现有 Transport/Timeout 失败 - -**设计裁决:** 使用 fence 证明首帧已交付,再用 TestClock 越过下一帧间隔;本场景验证 stream timing,不重复真实 socket 取消测试。 - -## 非目标与顺序 - -- 不要求把 fork session creation 与复制批次合并为同一事务。 -- 不改变 timeout 默认值、错误词汇、retry policy 或 provider protocol。 -- 01 → 02 → 03 串行落地;02/03 都修改 `packages/llm/test/transport-timeout.test.ts`。 -- 若 Bun 在目标 CI 平台无法提供可重复的服务端取消信号,02 停在诊断结论,不能退化为重复断言 Timeout 错误。 diff --git a/.scratch/batch-b/config-lkg-spec.md b/.scratch/batch-b/config-lkg-spec.md deleted file mode 100644 index 3b6e329459..0000000000 --- a/.scratch/batch-b/config-lkg-spec.md +++ /dev/null @@ -1,269 +0,0 @@ -# remote-config-lkg — OpenSpec apply-ready 镜像 - -- **基线:** `dev@4675435d94d462d2f9317d6688ddab2f0105c746` -- **change:** `remote-config-lkg` -- **local-only 原件:** `openspec/changes/remote-config-lkg/` -- **状态:** `4/4 artifacts complete`,apply-ready -- **校验:** `openspec validate --changes` → `1 passed, 0 failed` -- **严格校验:** `openspec validate remote-config-lkg --type change --strict --no-interactive` → `valid` -- **实施入口:** 按末尾 `tasks.md` 由票 07 以 TDD 执行;本镜像不包含生产代码。 - -以下四段在提交前按字节与 local-only 原件逐段核对。 - - -## Why - -remote config 在 transport 或 body 读取失败时会告警并跳过来源;长期离线时,这会让最近一次已验证的在线配置不可用。需要一个持久化 last-known-good(LKG),同时严格避免缓存或日志泄露认证材料,并且不掩盖认证与 schema 错误。 - -## What Changes - -- 为每个规范化 remote URL 保存最近一次完整通过解析与 schema 验证的远端响应;缓存保留 Environment 替换前的响应内容,不保存请求 header、token 或替换后的秘密。 -- 仅在既有可降级错误类别上读取 LKG;401/403、HTML 登录/认证响应和 schema decode 错误继续硬失败。 -- 使用同目录临时文件、原子 rename 和最终 `0600` 文件模式更新 LKG;写入失败不影响在线读取,也不破坏旧 LKG。 -- 损坏或空 LKG 告警后按既有 warn + skip 行为继续;日志不包含缓存正文或凭据。 -- LKG 不因年龄自动过期;记录 `writtenAt`,年龄只用于安全诊断,下一次合法在线成功原子覆盖旧值。 -- 用目标测试锁定在线写入后离线回退、失败写入保留旧值、硬失败边界、损坏缓存、安全键与日志以及原子文件语义。 - -## Capabilities - -### New Capabilities - -- `remote-config-lkg`: 定义 remote config 已验证响应的持久化、回退边界、隐私约束、耐久写入和诊断行为。 - -### Modified Capabilities - -无。 - -## Impact - -- 主要影响 `packages/opencode/src/config/config.ts`、一个位于 `packages/opencode/src/config/` 的 LKG 持久化模块,以及 `packages/opencode/test/config/wellknown-offline.test.ts` 的集成场景。 -- 在 OpenCode 的 XDG cache 根下新增用户私有的 remote-config LKG 文件;不改变 HTTP API、SDK、配置 schema 或依赖。 -- 没有 breaking change;没有可配置 TTL,也不重写当前无可用来源时的 warn + skip 降级。 - - - -## Context - -`Config.layer` 目前按以下顺序加载一个 well-known 认证来源:请求 `/.well-known/opencode`,解析并 decode `ConfigV1.WellKnown`,对 `remote_config.url` 与请求 headers 做 Environment 替换,可选请求第二跳 JSON,把内嵌与第二跳配置合并,最后由 `loadConfig` 执行 Environment 替换、JSONC 解析和 `ConfigV1.Info` schema 验证。transport、非认证 HTTP 不可用和 body 读取失败当前会 warn + skip;HTML 登录响应与 decode 失败会中止该配置加载。 - -LKG 必须接在这条流程上,而不能缓存最终的 `Info`。最终 `Info` 已经包含 Environment 替换结果,可能固化 token 或文件引用中的秘密。缓存还必须区分“在线 body 不是 JSON”与“JSON 可解析但不符合 schema”:前者属于允许降级的 body 失败,后者属于必须暴露的配置错误。 - -## Goals / Non-Goals - -**Goals:** - -- 在长期离线或远端暂时不可用时,复用同一 remote URL 最近一次完整验证成功的原始响应 body。 -- 保持认证、HTML 登录和 schema 错误为硬失败,保证 LKG 不掩盖需要用户处理的问题。 -- 让缓存更新具备用户私有权限和单文件原子替换语义;任何写入失败都不改变在线成功结果或旧 LKG。 -- 保持没有可用 LKG 时现有 warn + skip 的结果与合并边界。 - -**Non-Goals:** - -- 不增加 TTL 配置、后台刷新、跨设备同步、缓存清理命令或多版本迁移框架。 -- 不缓存请求 header、认证 token、Environment 替换后的正文、最终合并后的 `Info` 或 HTTP API 数据。 -- 不改变 remote config 的优先级、插件解析、普通本地配置加载、HTTP API 或 SDK。 -- 不借本变更重写现有 warn + skip 流程;只在允许降级的失败点插入 LKG 读取。 - -## Decisions - -### 1. 每个 HTTP remote URL 保存一个原始响应 LKG - -well-known 第一跳与可选 remote-config 第二跳各自以其请求 URL 标识缓存记录。记录格式固定为版本化 JSON envelope: - -```json -{ - "version": 1, - "writtenAt": "2026-08-09T00:00:00.000Z", - "body": "{...the exact response text...}" -} -``` - -`body` 是 Environment 替换前的响应文本。envelope 不保存响应 headers/status,也不保存请求 URL、请求 headers、认证 token、Environment map 或 decode 后对象。读取 LKG 后,body 必须重新经过与在线 body 相同的 JSON 解析、请求级 schema decode、remote-config 对象检查、Environment 替换和最终 `ConfigV1.Info` 验证。 - -在线 body 先进入暂存结果,只有该 well-known 来源的完整下游流程通过解析与 schema 验证后才允许写入本次在线取得的记录。任一在线 auth、HTML、JSON shape、对象检查、Environment 替换或最终 `ConfigV1.Info` 失败时,不写本次来源暂存的任何 LKG。选择延迟提交而不是在 `fetchRemoteJson` decode 后立即写,是为了避免把“JSON 合法但最终配置无效”的响应提升为 LKG。 - -未选择缓存最终 `Info`,因为它已经过 Environment 替换;也未选择缓存 request/response headers,因为它们不是离线重放配置所需的数据,并可能携带凭据。 - -### 2. cache key 只有规范化 URL 的稳定摘要 - -URL 用 WHATWG `URL` 规范化:清除不会随 HTTP 请求发送的 fragment,依赖 URL 实现统一 scheme/host 大小写、默认端口与转义形式,并保留会改变资源身份的 path 与 query。缓存文件名是规范化 URL UTF-8 字节的 SHA-256 小写十六进制摘要加 `.json`;目录固定为 `Global.Path.cache/remote-config-lkg/`。 - -文件名和 envelope 都不拼接原始 URL、认证 header、token、Environment 变量值或配置正文。摘要是 key 中唯一由 URL 派生的值;headers、token、Environment map 与 body 不参与额外的 key 组成。remote-config 日志使用稳定摘要和 `well-known`/`remote-config` 角色标识,不记录请求 headers、token、Environment 值、缓存 body 或响应 body;错误原因先归类,不直接序列化可能回显请求的底层错误对象。 - -未选择可读 URL 文件名,因为 query/userinfo 可能携带凭据;未选择 header/token 分区,因为它会把认证材料引入持久身份并违反本票边界。 - -### 3. 明确在线失败分类,再决定是否读取 LKG - -`fetchRemoteJson` 将在线结果表达为成功、允许降级失败或硬失败,而不是用一个捕获所有错误的分支: - -- 允许降级并尝试 LKG:DNS/连接/timeout 等 transport 错误;除 401/403 外的现有不可用 HTTP 状态;body stream 读取失败;内容不是 HTML 登录页但 JSON 语法不可解析。 -- 直接硬失败且不得读取 LKG:HTTP 401/403;content-type 或 body 特征识别出的 HTML/login/auth 响应;JSON 可解析但请求级 schema decode 失败;第二跳结果不是对象;Environment 替换、JSONC 解析或最终 `ConfigV1.Info` schema decode 失败。 - -在线 JSON 语法解析与 schema decode 必须分成两个可观察步骤,才能保持上述边界。401/403 在进入通用非 2xx 降级分支前转换为现有 `RemoteAuthError` 语义。已有 LKG 也不能改变硬失败结果,且硬失败不能覆盖旧 LKG。 - -第一跳允许降级失败且没有可用 LKG时,继续跳过整个 well-known 来源。第二跳同类失败且没有可用 LKG时,继续返回空的 fetched config,使 well-known 内嵌 config 按现状合并。这个分支只复用原行为,不重新定义 warn + skip。 - -### 4. 损坏、空或缺失缓存不成为新的硬错误 - -仅在在线失败属于允许降级类别时读取 LKG。文件缺失表示没有 LKG,保留原在线失败告警并执行原 skip;空文件、JSON envelope 损坏、版本不支持、`writtenAt` 无效、空 body 或缓存 body 无法重新解析/decode 则额外记录不含正文与凭据的 warning,并把缓存视为不可用,随后执行同一 skip 分支。缓存自身的 schema 错误属于缓存损坏,不提升为在线 schema 硬失败。 - -未选择让损坏缓存中止启动,因为 LKG 是可丢弃的恢复材料,不能比当前无缓存流程更脆弱。 - -### 5. 同目录临时文件保证旧值不被失败更新破坏 - -实现放在新的 `packages/opencode/src/config/remote-lkg.ts` 自包含模块,公开一个窄的读取/写入接口给 `config.ts`。写入顺序为:确保专用目录存在;在目标同目录创建唯一临时文件并以 `0600` 写完整 envelope;关闭文件;原子 rename 到摘要目标;确认最终目标模式为 `0600`。成功 rename 前的任意失败只做安全 warning 和临时文件 best-effort 清理,旧目标保持不变。因为临时文件从创建起就是 `0600`,rename 后不会出现更宽权限窗口。 - -缓存写入是在线成功路径的 best-effort side effect。任一记录写失败只保留对应 URL 的旧文件并继续返回已验证在线配置;多条暂存记录独立提交,失败不会回滚配置加载。并发更新采用“最后一个完整 rename 获胜”,每个可见文件始终是完整 envelope,不增加锁服务。 - -未选择直接 truncate 目标文件,因为进程崩溃或磁盘错误会破坏旧 LKG;未选择跨目录临时文件,因为 rename 可能失去原子性。 - -### 6. LKG 永不过期,年龄只用于诊断 - -读取逻辑不以 `writtenAt` 或文件 mtime 拒绝 LKG。回退成功时可以记录 `writtenAt` 或计算后的非敏感年龄诊断,但年龄不改变控制流。下一次合法在线成功按上述原子写流程覆盖同 URL 的记录。 - -未选择固定或可配置 TTL:LKG 的目标是支持长期离线,自动过期会在最需要它时恢复到 warn + skip,并引入本票不需要的策略面。 - -### 7. TDD 边界 - -07 先扩展 `packages/opencode/test/config/wellknown-offline.test.ts`,用真实 `Config.layer` 锁定两跳在线写入后离线回退、预置 LKG 下的 auth/decode 硬失败、损坏/空缓存和日志无凭据。持久化细节放在 `packages/opencode/test/config/remote-lkg.test.ts`,用隔离 cache 根验证稳定摘要文件名、Environment 替换前 body、同目录 rename、写失败保留旧值和 POSIX `0600`。测试可为 rename 失败提供最窄的文件系统故障注入点,其余路径使用真实文件系统。 - -生产改动限定为 `packages/opencode/src/config/config.ts` 与新的 `packages/opencode/src/config/remote-lkg.ts`;不改 `packages/core`、schema、路由或生成物。 - -## Risks / Trade-offs - -- [永不过期的 LKG 可能很旧] → 每次回退记录安全的 `writtenAt`/年龄诊断,并由下一次合法在线成功覆盖;不静默声称缓存新鲜。 -- [只按 URL 分区会让同一用户下不同认证上下文共享该 URL 的 LKG] → 文件保持用户私有 `0600`,不把凭据加入 key;这是安全 key 约束与认证维度隔离之间的明确取舍。 -- [磁盘写入、rename 或 chmod 失败] → 在线配置仍成功,旧 LKG 在 rename 前保持完整,日志只包含摘要与分类。 -- [Windows 不提供等价的 POSIX mode 语义] → 创建与替换仍请求 `0600`;POSIX 测试断言精确 mode,Windows 保留原子替换与不扩宽应用请求的行为。 -- [缓存 body 本身包含用户配置] → 只写入专用 cache 目录的 `0600` 文件,绝不写入 key 或日志,也不保存经过 Environment 替换的版本。 - -## Migration Plan - -无需迁移:首次合法在线成功按需创建 version 1 文件;没有文件时行为与当前版本相同。回滚代码后这些 cache 文件无人读取,可安全保留;未来不兼容版本按损坏/不支持缓存的 warn + skip 语义处理。 - -## Open Questions - -无。本票明确采用永不过期策略,不增加可配置 TTL 或后续扩展点。 - - - -## ADDED Requirements - -### Requirement: 只持久化完整验证成功的原始远端响应 -系统 MUST 以版本、`writtenAt` 和原始响应 body 组成 LKG;MUST 在对应 well-known 来源的请求级解析、schema decode、第二跳对象检查、Environment 替换和最终 `ConfigV1.Info` schema 验证全部成功后,才持久化本次在线取得的响应。系统 MUST 保存 Environment 替换前的 body,且 MUST NOT 把请求 headers、认证 token、Environment map、替换后的正文或最终合并 `Info` 作为缓存字段。 - -#### Scenario: 在线成功后写入并可离线复用 -- **WHEN** well-known 与第二跳 remote-config 在线响应均成功,完整配置通过最终 schema 验证,随后同一 URL 的 transport 请求失败 -- **THEN** 系统写入各 URL 的原始响应 LKG,并在后续离线加载中重新验证和应用 LKG,得到与上一次合法在线读取相同的配置语义 - -#### Scenario: Environment 秘密不被固化 -- **WHEN** 原始远端 body 包含 Environment 占位符,在线加载用当前 Environment 值完成替换并通过验证 -- **THEN** LKG body 保留占位符形式,缓存 envelope 不包含替换后的 Environment 值、请求 header 或 token,回退时使用当次 Environment 重新执行替换 - -#### Scenario: 下游验证失败不产生新 LKG -- **WHEN** 在线 body 可解析,但第二跳不是对象、Environment 替换失败或最终 `ConfigV1.Info` schema decode 失败 -- **THEN** 系统硬失败,MUST NOT 写入本次暂存响应,也 MUST NOT 覆盖已有 LKG - -### Requirement: 缓存身份与诊断不得泄露凭据 -系统 MUST 用规范化 remote URL 的 SHA-256 小写十六进制摘要作为唯一文件标识。规范化 MUST 清除 fragment,并统一 WHATWG URL 定义的 scheme/host 大小写、默认端口与转义形式,同时保留改变资源身份的 path 与 query。key、文件名和日志 MUST NOT 拼接原始 URL、认证 headers、token、Environment 变量值、缓存正文或配置正文;remote-config 诊断 MUST 只使用摘要、端点角色、失败分类和非敏感年龄信息。 - -#### Scenario: 等价 URL 使用同一稳定文件名 -- **WHEN** 两个 remote URL 仅在 host 大小写、默认 HTTPS 端口或 fragment 上不同 -- **THEN** 系统规范化后生成相同的 64 位十六进制摘要文件名 - -#### Scenario: 凭据与正文不出现在 key 或日志 -- **WHEN** remote config 请求包含认证 header、token、Environment 替换值和可识别的配置正文标记,并发生在线成功、缓存写入、离线回退及缓存错误诊断 -- **THEN** 缓存文件名、key 和捕获到的日志均不包含这些 header、token、Environment 值或正文标记,缓存 envelope 也不包含请求认证元数据 - -### Requirement: LKG 更新必须原子且用户私有 -系统 MUST 在目标文件同目录创建唯一临时文件,以 `0600` 写入完整 envelope,关闭后通过原子 rename 替换目标,并保证最终文件模式为 `0600`。缓存写入 MUST 是在线成功路径的 best-effort side effect;写入失败 MUST NOT 使已验证在线配置失败,也 MUST NOT 修改或删除旧 LKG。 - -#### Scenario: 同目录原子替换并设置文件模式 -- **WHEN** 系统首次写入或覆盖一个 LKG -- **THEN** 完整内容先写入目标同目录的临时文件,再由 rename 发布,最终目标是完整 envelope 且权限模式为 `0600` - -#### Scenario: rename 前写入失败保留旧 LKG -- **WHEN** 已存在可用旧 LKG,而新在线响应验证成功但临时写入、关闭或 rename 失败 -- **THEN** 系统记录不含正文和凭据的 warning,仍返回新在线配置,并保留旧 LKG 原封不动供后续允许降级的失败使用 - -#### Scenario: 并发写入不暴露部分文件 -- **WHEN** 同一 URL 的两个合法在线加载并发更新 LKG -- **THEN** 最终读者只能观察到某一个完整 envelope,不能观察到截断或混合内容 - -### Requirement: 只有允许降级的在线失败可以回退 -系统 MUST 先按现有 remote-config 错误语义分类在线失败。transport 错误、除 401/403 外的非认证不可用 HTTP 状态、body stream 读取失败以及非 HTML 的 JSON 语法不可解析 body MUST 尝试读取 LKG。HTTP 401/403、HTML/login/auth 响应、JSON 可解析后的请求级 schema decode 错误、第二跳非对象、Environment 替换错误和最终配置 schema decode 错误 MUST 硬失败,且 MUST NOT 读取 LKG 掩盖错误。 - -#### Scenario: transport 或非认证不可用状态回退 -- **WHEN** 已有可用 LKG,在线请求发生 DNS、连接、timeout 或除 401/403 外的既有不可用 HTTP 状态 -- **THEN** 系统告警该在线失败,重新验证 LKG,并用其继续 remote config 加载 - -#### Scenario: body 读取或 JSON 语法失败回退 -- **WHEN** 已有可用 LKG,在线 response body stream 读取失败,或 body 不是 HTML/login 响应但不是可解析 JSON -- **THEN** 系统把失败归入允许降级 body 类别并使用 LKG - -#### Scenario: 401 或 403 不回退 -- **WHEN** 已有可用 LKG,但在线 remote endpoint 返回 401 或 403 -- **THEN** 系统保持认证硬失败语义,不读取 LKG,也不覆盖旧 LKG - -#### Scenario: HTML 登录页不回退 -- **WHEN** 已有可用 LKG,但在线响应由 content-type 或 body 特征识别为 HTML/login/auth 页面 -- **THEN** 系统产生现有 `RemoteAuthError` 语义,不读取 LKG,也不覆盖旧 LKG - -#### Scenario: schema decode 错误不回退 -- **WHEN** 已有可用 LKG,但在线 body 是合法 JSON,随后在 well-known schema、第二跳对象检查或最终 `ConfigV1.Info` schema decode 中失败 -- **THEN** 系统暴露硬失败,不读取 LKG,也不覆盖旧 LKG - -### Requirement: 不可用缓存保留原 warn + skip 语义 -系统 MUST 把缺失 LKG 视为没有恢复材料;MUST 把空文件、损坏 envelope、不支持版本、无效 `writtenAt`、空 body 或无法重新解析/decode 的缓存视为不可用。损坏或空缓存 MUST 产生不含正文与凭据的 warning,随后 MUST 执行原有允许降级分支,而不是崩溃或改变在线错误类别。 - -#### Scenario: 第一跳损坏或空缓存告警后跳过来源 -- **WHEN** well-known 在线失败允许降级,但对应 LKG 为空或损坏 -- **THEN** 系统告警缓存不可用并跳过整个 well-known 来源,本地配置继续按现有行为加载 - -#### Scenario: 第二跳损坏或空缓存保留内嵌配置 -- **WHEN** 第二跳 remote-config 在线失败允许降级,但对应 LKG 为空或损坏 -- **THEN** 系统告警缓存不可用并按现有空 fetched-config 分支继续,well-known 内嵌配置仍可合并 - -#### Scenario: 缓存告警不回显缓存内容 -- **WHEN** 损坏缓存包含可识别的凭据或配置正文标记 -- **THEN** warning 只包含安全摘要、端点角色和损坏分类,不包含文件正文、底层解析输入或凭据标记 - -### Requirement: LKG 不因年龄自动过期 -系统 MUST 保存合法 RFC 3339 `writtenAt`,但 MUST NOT 以 `writtenAt`、文件 mtime 或任何固定/可配置 TTL 拒绝 LKG。年龄 MUST 只用于安全诊断;下一次同 URL 的合法在线成功 MUST 通过原子更新覆盖旧记录。系统 MUST NOT 为本能力增加可配置 TTL 扩展点。 - -#### Scenario: 很旧的 LKG 仍支持长期离线 -- **WHEN** 在线失败允许降级且可用 LKG 的 `writtenAt` 已经过任意长时间 -- **THEN** 系统仍重新验证并使用该 LKG,同时可记录不含凭据的年龄诊断,不因年龄执行 warn + skip - -#### Scenario: 合法在线成功覆盖旧记录 -- **WHEN** 使用旧 LKG 后,同一规范化 URL 再次获得并完整验证合法在线响应 -- **THEN** 系统原子覆盖旧 LKG,更新 `writtenAt`,且不创建或读取 TTL 配置 - - - -## 1. 红灯:锁定外部行为 - -- [x] 1.1 在 `packages/opencode/test/config/wellknown-offline.test.ts` 增加两跳在线成功写入、换实例后第一跳/第二跳 transport 与非认证 body 失败使用 LKG 的场景,并先运行目标文件确认新断言因 LKG 尚未实现而失败;对应[在线成功后写入并可离线复用](specs/remote-config-lkg/spec.md#scenario-在线成功后写入并可离线复用)与[允许降级回退](specs/remote-config-lkg/spec.md#scenario-transport-或非认证不可用状态回退)。 -- [x] 1.2 在同一集成测试预置可用 LKG,再覆盖 401、403、HTML login、合法 JSON 的 well-known schema 错误、第二跳非对象和最终 `ConfigV1.Info` decode 错误;逐项断言硬失败、未使用/未覆盖 LKG,对应[401/403](specs/remote-config-lkg/spec.md#scenario-401-或-403-不回退)、[HTML](specs/remote-config-lkg/spec.md#scenario-html-登录页不回退)与[schema decode](specs/remote-config-lkg/spec.md#scenario-schema-decode-错误不回退)。 -- [x] 1.3 在同一集成测试加入缺失、空、损坏和超旧缓存;断言第一跳保持 warn + skip、本地配置可用,第二跳保持内嵌 config 合并,超旧记录仍使用且只给安全年龄诊断;不得改写现有降级分支的结果。 -- [x] 1.4 新建 `packages/opencode/test/config/remote-lkg.test.ts`,用隔离 cache 根和真实文件系统锁定 URL 规范化摘要、原始 Environment 占位符、同目录 rename、完整 envelope、POSIX `0600`、并发完整性,并用最窄 rename 故障注入锁定失败更新后旧 LKG 仍可读。 -- [x] 1.5 在两份目标测试放置独特的 header/token/Environment/正文标记,断言文件名、key 和所有 remote-config/cache 日志不含标记,envelope 不含请求认证元数据;确认新增安全断言先红。 - -## 2. 绿灯:实现私有原子 LKG 模块 - -- [x] 2.1 新建 `packages/opencode/src/config/remote-lkg.ts` 并按 `src/config` 自导出规范提供窄接口:WHATWG URL 去 fragment、SHA-256 文件名、version 1 envelope decode,以及从 `Global.Path.cache/remote-config-lkg/` 读取原始 body;损坏、空和不支持版本返回可分类的不可用结果,不抛出正文。 -- [x] 2.2 在该模块实现 best-effort 写入:目标同目录唯一临时文件以 `0600` 写完整 envelope,关闭后原子 rename,最终模式 `0600`;失败时安全告警、best-effort 清理临时文件且不触碰旧目标。 -- [x] 2.3 保持 `writtenAt` 为合法 RFC 3339 诊断字段;读取不检查 TTL/mtime,不增加配置项、清理器、后台刷新或 TTL 扩展接口。 - -## 3. 绿灯:接入当前 remote config 流程 - -- [x] 3.1 仅在 `packages/opencode/src/config/config.ts` 调整 `fetchRemoteJson` 附近:把 JSON 语法解析与 schema decode 分开,并将结果分类为在线成功、允许降级失败和硬失败;401/403 与 HTML/login/auth 继续使用硬认证错误,合法 JSON 的 schema/object/final-config 错误继续硬失败。 -- [x] 3.2 well-known 与第二跳在线响应只暂存 Environment 替换前 body;完整来源通过 `loadConfig` 最终 schema 验证后才调用 LKG 写入。仅允许降级失败读取并重验 LKG;无可用 LKG 时分别复用现有“第一跳 skip 来源”和“第二跳空 fetched config”分支。 -- [x] 3.3 把触及的 remote-config/cache 诊断限定为摘要、端点角色、失败分类和非敏感年龄;不得序列化原始 URL、底层可能回显请求的错误对象、headers、token、Environment 值或 body。 - -## 4. 验收与范围门禁 - -- [x] 4.1 从 `packages/opencode` 运行 `bun test test/config/remote-lkg.test.ts test/config/wellknown-offline.test.ts`,确认在线→离线、旧 LKG、auth/decode、损坏/空缓存、永不过期、key/log 安全及原子/权限场景全绿。 -- [x] 4.2 从 `packages/opencode` 运行 `bun typecheck`;不得用 `bun run build` 代替类型门禁。 -- [x] 4.3 检查实现 diff 只涉及 `packages/opencode/src/config/config.ts`、`packages/opencode/src/config/remote-lkg.ts` 和上述两份 config 测试;如确需测试 fixture 的最小改动须在提交说明中列出,`packages/core`、HTTP routes、SDK 生成物、依赖与既有 warn + skip 语义保持零改动。 - diff --git a/.scratch/batch-b/evidence.md b/.scratch/batch-b/evidence.md deleted file mode 100644 index b2cfb8887e..0000000000 --- a/.scratch/batch-b/evidence.md +++ /dev/null @@ -1,61 +0,0 @@ -# 批次 B 证据快照 - -**代码基线:** `dev@3e8368f37` -**用途:** 给每票的新任务/worktree 提供稳定证据;无需重新调查原始评审。 -**原始来源:** 本地未追踪的 `.opencode/promotion-review-round1/*.md` 与 `.opencode/.dag-specs/evidence/*.md`。 - -## 组 1:abort-path 契约 - -### U-1 — Session fork 嵌套事务回滚 - -- 原始 finding 引用的 `packages/core/src/session.ts` 已过期;当前实现位于 `packages/opencode/src/session/session.ts` 的 `Session.fork`。 -- fork 的消息/part 复制由一个外层 `db.transaction` 包裹,嵌套 `events.publish` 会进入 Effect-Drizzle SQLite savepoint。 -- `packages/effect-drizzle-sqlite/src/effect-sqlite/session.ts` 已实现嵌套事务的 savepoint/rollback。 -- `packages/opencode/test/session/fork-batch.test.ts` 已验证成功路径与事务/savepoint 数量,缺口仅是“部分发布成功后失败”的整体回滚。 -- 目标契约、测试边界与非目标见 `.scratch/batch-b/abort-path-contracts.md`。 - -### U-2 — timeout 传播到底层 HTTP 取消 - -- `packages/llm/src/route/transport/http.ts` 对请求执行使用 `Effect.timeout`,对响应 stream 使用 `Stream.timeoutOrElse`。 -- `packages/llm/test/transport-timeout.test.ts` 已覆盖 headers 挂起、body 从不发帧、正常完成、默认 timeout 与选项合并,但使用内存 HTTP client,不能证明真实 socket/response body 被取消。 -- `packages/opencode/test/session/llm.test.ts` 已覆盖显式 Fiber interrupt 导致 provider response body 取消;本票必须验证“timeout 驱动”的取消,不能复制该场景。 -- 验收必须使用真实 loopback `Bun.serve` + 生产 fetch-backed client,并以服务端可观察的 response cancellation 为主信号。 - -### Transport mid-stream-stall - -- 当前 timeout suite 没有“合法首帧已交付,随后永久停顿”的场景。 -- 本票验证逐帧间隔 timeout;可使用 TestClock,不承担真实 socket 取消证明。 -- U-2 与本票都修改 `packages/llm/test/transport-timeout.test.ts`,必须先 02 后 03。 - -## 组 2:测试卫生债 - -### F3 — 固定订阅 settle sleep - -- `packages/opencode/test/goal/e2e-loop.test.ts` 定义 `SUBSCRIPTION_SETTLE_MS = 200`,共有 8 个固定 `Effect.sleep` 等待点。 -- `GoalLoop` 初始化主要读取实例状态并 fork 事件订阅;票据应以可观察 readiness/fence 或最小调度让步替代墙钟等待。 -- 验收要求旧的固定 settle sleep 全部消失,并重复运行目标测试;不把生产行为修改当作默认方案。 - -### F4 — DagStore 双重断言 - -- `packages/opencode/test/dag/dag-timeout-escalation-fixes.test.ts` 有两处 `as unknown as DagStore.Interface`,原始证据只记录了第一处。 -- 两处都需改为类型安全的 `Layer.mock`/fixture factory;不得只清一处。 - -## 组 3/4 与批 C - -### O1 — remote config last-known-good - -- PR #182/#189 前的现状已变化:`packages/opencode/src/config/config.ts` 在 remote transport/body 失败时会 warn + skip;HTML 登录页/auth 与 schema decode 仍硬失败。 -- 剩余需求仅是持久化 LKG。实施前需裁决:缓存内容、稳定键、原子写与权限、何种失败允许回退、损坏缓存行为、TTL。 -- 安全下限:缓存键不得含 header/token;LKG 不得掩盖 auth/decode 错误;损坏缓存只能 warn + skip。 - -### S7 — recovery INVENTED 推断 - -- `packages/opencode/src/dag/runtime/recovery.ts` 的 session checker 从最后一条 assistant finish 推断 active/terminal;tool-calls、unknown 或无 finish 会落入 active/unknown,并可能在 reconcile 中写入 `exec_failed`。 -- `packages/opencode/src/dag/runtime/loop.ts` 在 `ownershipLost` 后会暂停 workflow,现有测试已覆盖该缓解;目前没有已复现的用户态缺陷。 -- 只能按 `/diagnosing-bugs` 先建红灯反馈回路。若“durable transcript 已语义完成却被判 active 并写失败”无法稳定复现,结论应是无修复,不得凭静态推断改代码。 - -### P8 — spawnReady 复杂度 - -- `packages/opencode/src/dag/runtime/loop.ts` 的 `spawnReady` 对 ready 节点反复在全节点数组中 `.find`,静态复杂度为 `O(ready × nodes)`。 -- 原始性能评审明确标记“>50 节点的实际调度开销未实测”;当前无用户痛点或 benchmark。 -- 批 C 只记录观测结论;没有 trace/benchmark 证明影响时,以 no-code 关闭,不阻塞最终晋级。 diff --git a/.scratch/batch-b/issues/01-u1-fork-rollback.md b/.scratch/batch-b/issues/01-u1-fork-rollback.md deleted file mode 100644 index 6c5c8af49d..0000000000 --- a/.scratch/batch-b/issues/01-u1-fork-rollback.md +++ /dev/null @@ -1,23 +0,0 @@ -# 01 — U-1:Session fork 中途失败整体回滚 - -**What to build:** 在真实 SQLite 的 `Session.fork` 集成测试中注入确定性中途失败,证明一个嵌套 durable publication 失败会回滚同一复制批次中已写入的消息/part 事件及投影。 - -**Spec:** `.scratch/batch-b/abort-path-contracts.md` 的 Requirement 1 -**Evidence:** `.scratch/batch-b/evidence.md#u-1--session-fork-嵌套事务回滚` -**Branch:** `test/fork-rollback` -**Blocked by:** None -**Status:** done - -- [x] 复用 `packages/opencode/test/session/fork-batch.test.ts` 的真实 SQLite fixture;不写 adapter-only 替代测试 -- [x] 至少一个 message/part 发布完成后再确定性失败,旧实现若违约时测试能红 -- [x] 目标 session 无复制出的 durable events 与 projections,源 session 不变;Session Created 可保留 -- [x] 若红灯暴露生产缺陷,只做本契约所需的最小修复 -- [x] 在 `packages/opencode` 运行目标测试与 `bun typecheck`,结果附入票据 - -## 验证证据 - -- 基线:`dev@8f8465753b6517b3deeb6ad37002263d3da287fe`;分支:`test/fork-rollback`。 -- 失败注入:真实 SQLite trigger 在第二条复制 part 的 durable event insert 上执行 `RAISE(ABORT)`;此前 3 个嵌套 publication 已释放 savepoint。 -- mutation 红灯:临时移除外层复制事务后,目标 projection 残留 2 条 message(第一条含已复制 part),新增用例 0 pass / 1 fail;mutation 未保留。 -- `cd packages/opencode && bun test test/session/fork-batch.test.ts`:3 pass,0 fail,52 expect。 -- `cd packages/opencode && bun typecheck`:`tsgo --noEmit`,exit 0;现有生产实现满足契约,无生产代码修改。 diff --git a/.scratch/batch-b/issues/02-u2-transport-timeout-abort.md b/.scratch/batch-b/issues/02-u2-transport-timeout-abort.md deleted file mode 100644 index c140dc049e..0000000000 --- a/.scratch/batch-b/issues/02-u2-transport-timeout-abort.md +++ /dev/null @@ -1,24 +0,0 @@ -# 02 — U-2:HTTP timeout 传播到真实 response 取消 - -**What to build:** 使用 loopback `Bun.serve` 与生产 fetch-backed HTTP client,证明 provider timeout 不只返回 Transport/Timeout,还会取消仍打开的真实响应流。 - -**Spec:** `.scratch/batch-b/abort-path-contracts.md` 的 Requirement 2 -**Evidence:** `.scratch/batch-b/evidence.md#u-2--timeout-传播到底层-http-取消` -**Branch:** `test/transport-abort` -**Blocked by:** 01(同一 OpenSpec 串行落地) -**Status:** closed - -- [x] fixture 提供“请求已接收”与“response 已取消”的有界 fence,禁止用固定 sleep 猜时序 -- [x] timeout 后断言现有 `LLMError` Transport/Timeout 形状 -- [x] 服务端确定性观察到 response stream cancellation;request abort 只作补充信号 -- [x] 不用内存 HttpClient 或显式 Fiber interrupt 重复现有覆盖 -- [x] 在 `packages/llm` 连续运行目标测试至少 3 次并运行 `bun typecheck` - -## 验证证据 - -- 基线:`dev@55dd345491de4542dbf6fa7a4ba126a2c23104c4`;分支:`test/transport-abort`。 -- 实现提交:`8ec1ef192`;PR:[LeXwDeX/OpenCode-GraphAgent#193](https://github.com/LeXwDeX/OpenCode-GraphAgent/pull/193) → `dev`。 -- 真实 transport:公开 `LLMClient.stream(...)` 经 `FetchHttpClient.layer` 请求 loopback `Bun.serve`;2 秒有界 fence 分别证明请求已接收与服务端 response `cancel()` 已触发。 -- mutation 红灯:临时移除 response stream 的 `Stream.timeoutOrElse` 后,新增场景在 1 秒测试边界超时,0 pass / 1 fail;mutation 已恢复,生产文件无 diff。 -- `cd packages/llm && bun test test/transport-timeout.test.ts --timeout 30000`:连续 3 次均为 7 pass、0 fail、14 expect;`bun typecheck`:`tsgo --noEmit`,exit 0。 -- 现有生产实现满足 OpenSpec Requirement 2,无生产代码修改;Requirement 3 留给 03 票。 diff --git a/.scratch/batch-b/issues/03-transport-midstream-stall.md b/.scratch/batch-b/issues/03-transport-midstream-stall.md deleted file mode 100644 index 0e57d6a6c7..0000000000 --- a/.scratch/batch-b/issues/03-transport-midstream-stall.md +++ /dev/null @@ -1,24 +0,0 @@ -# 03 — Transport:合法首帧后的 stall 触发逐帧超时 - -**What to build:** 增加“合法 SSE 首帧已交付,连接随后永久停顿”的测试,固定 `Stream.timeoutOrElse` 是相邻帧间隔上界的契约。 - -**Spec:** `.scratch/batch-b/abort-path-contracts.md` 的 Requirement 3 -**Evidence:** `.scratch/batch-b/evidence.md#transport-mid-stream-stall` -**Branch:** `test/midstream-timeout` -**Blocked by:** None(02 已完成) -**Status:** closed - -- [x] 用 fence 证明 timeout 前合法首帧已经交付给消费者 -- [x] 用 TestClock 越过下一帧间隔,随后得到现有 Transport/Timeout -- [x] 不引入真实墙钟 sleep,不改变 timeout 默认值与错误词汇 -- [x] 完整 `transport-timeout.test.ts` 覆盖保持绿色 -- [x] 在 `packages/llm` 运行目标测试与 `bun typecheck` - -## 验证证据 - -- 基线:`dev@1a8635400f3f4b7c4985b06f0776e13d6f2e5e05`;分支:`test/midstream-timeout`。 -- 实现提交:`1d5d08f76`;PR:[LeXwDeX/OpenCode-GraphAgent#194](https://github.com/LeXwDeX/OpenCode-GraphAgent/pull/194) → `dev`。 -- 公开 seam:`LLMClient.stream(...)` 消费合法 SSE text delta;`Deferred` fence 只在 `text-delta` 的 `text === "Hello"` 已交付时解除,随后 `TestClock` 将 1000ms 帧间 timeout 推进到 2000ms。 -- mutation 红灯:临时绕过 response stream 的 `Stream.timeoutOrElse` 后,首帧 fence 仍解除,但新增场景因 stream 未结束而 0 pass / 1 fail;mutation 已恢复,生产文件无 diff。 -- `cd packages/llm && bun test test/transport-timeout.test.ts`:连续 3 次均为 8 pass、0 fail、16 expect;`bun typecheck`:`tsgo --noEmit`,exit 0。 -- 现有生产实现满足 OpenSpec Requirement 3;无生产代码、timeout 默认值、错误词汇或 provider protocol 修改。 diff --git a/.scratch/batch-b/issues/04-f3-subscription-readiness.md b/.scratch/batch-b/issues/04-f3-subscription-readiness.md deleted file mode 100644 index 6233c2602d..0000000000 --- a/.scratch/batch-b/issues/04-f3-subscription-readiness.md +++ /dev/null @@ -1,22 +0,0 @@ -# 04 — F3:用确定性 readiness 替代订阅 settle sleep - -**What to build:** 清除 `packages/opencode/test/goal/e2e-loop.test.ts` 的 `SUBSCRIPTION_SETTLE_MS = 200` 与 8 个固定 settle sleeps,用可观察 readiness/fence 或最小调度让步同步 GoalLoop 订阅就绪。 - -**Evidence:** `.scratch/batch-b/evidence.md#f3--固定订阅-settle-sleep` -**Branch:** `test/goal-readiness` -**Blocked by:** None(03 已完成;代码写集独立) -**Status:** closed - -- [x] 先证明每个 sleep 等待的具体事件/状态,不用另一个超时数值替换 200ms -- [x] 8 个固定 settle sleeps 全部删除或由同一确定性同步机制取代 -- [x] 默认不改生产行为;确需生产 readiness 信号时先在票内写明边界 -- [x] 目标测试连续运行至少 5 次稳定绿色 -- [x] 在 `packages/opencode` 运行目标测试与 `bun typecheck` - -## 完成证据 - -- Commit: `f77106bc03b5c4ec6816dd82d9bba8485974f790`(`test(goal): replace subscription settle sleeps`) -- PR: [#195](https://github.com/LeXwDeX/OpenCode-GraphAgent/pull/195) → `dev` -- TDD: 首场景保留 200ms sleep 时基线绿色;删除同步点后首次 idle 被漏掉,`judge call 1` 未触发;加入一次 `Effect.yieldNow` 后恢复绿色,再机械推广到其余 7 处。 -- 验证:目标文件连续 5 次全绿(每次 8/8);`packages/opencode` 的 `bun typecheck` 通过;提交钩子 lint 0 error、全仓 typecheck 29/29。 -- 范围:旧常量与 8 个固定 sleep 全部消失;生产代码零 diff。 diff --git a/.scratch/batch-b/issues/05-f4-type-safe-dag-store-fixtures.md b/.scratch/batch-b/issues/05-f4-type-safe-dag-store-fixtures.md deleted file mode 100644 index 5a196308ce..0000000000 --- a/.scratch/batch-b/issues/05-f4-type-safe-dag-store-fixtures.md +++ /dev/null @@ -1,21 +0,0 @@ -# 05 — F4:移除 DagStore fixture 的双重类型断言 - -**What to build:** 清除 `packages/opencode/test/dag/dag-timeout-escalation-fixes.test.ts` 中两处 `as unknown as DagStore.Interface`,改用类型安全的 `Layer.mock` 或测试 fixture factory。 - -**Evidence:** `.scratch/batch-b/evidence.md#f4--dagstore-双重断言` -**Branch:** `test/dag-store-fixtures` -**Blocked by:** None(04 已完成;代码写集独立) -**Status:** closed - -- [x] 两处双重断言都消失,不能只修原评审记录的第一处 -- [x] fixture 缺少/签名漂移的方法能在 typecheck 时暴露 -- [x] 不复制 DagStore 生产逻辑到测试 -- [x] timeout escalation 目标测试行为与断言不削弱 -- [x] 在 `packages/opencode` 运行目标测试与 `bun typecheck` - -## 红绿验证(基线 `403461e831aa8cda65d449f4a873db1d1686b44f`) - -- **红灯:** 直接删除两处 `as unknown as DagStore.Interface` 后,在 `packages/opencode` 运行 `bun typecheck`,退出码 2。`TS2740` 分别出现在原第 322、370 行:仅含 `getNode` 的对象缺少 `getWorkflow`、`listWorkflows`、`listBySession`、`listByProject` 与另外 13 个 `DagStore.Interface` 成员。 -- **绿灯:** 改为 `Layer.mock(DagStore.Service)`,通过 `Layer.unwrap` 将类型安全的 store fixture 注入 `Layer.mock(Dag.Service)`;`bun typecheck` 退出码 0。 -- **重复验证:** `bun test test/dag/dag-timeout-escalation-fixes.test.ts` 连续运行 3 次,每次均为 12 pass、0 fail、38 次断言。 -- **范围验证:** 相对上述基线,生产文件零 diff;只修改目标测试与批次 B 的票 05/06。 diff --git a/.scratch/batch-b/issues/06-o1-lkg-spec.md b/.scratch/batch-b/issues/06-o1-lkg-spec.md deleted file mode 100644 index 862d90e665..0000000000 --- a/.scratch/batch-b/issues/06-o1-lkg-spec.md +++ /dev/null @@ -1,22 +0,0 @@ -# 06 — O1:remote config LKG 小规格 - -**What to build:** 只为 remote config 的持久化 last-known-good 增量产出一份小规格;不修改生产代码。OpenSpec 原件在 local-only `/openspec/` 生成并校验,同时把可执行镜像写入 `.scratch/batch-b/config-lkg-spec.md`,再更新 07 的具体文件、验收与分支边界。 - -**Evidence:** `.scratch/batch-b/evidence.md#o1--remote-config-last-known-good` -**Branch:** `docs/config-lkg-spec` -**Blocked by:** 05(批次串行) -**Status:** closed - -- [x] 定义缓存内容与写入时机:只缓存完整下游验证成功的原始响应 body,持久化发生在 Environment 替换前,envelope 不保存请求 header/token -- [x] 定义稳定 cache key、原子写、文件权限:规范化 URL 的 SHA-256 文件名、同目录临时文件 + rename、最终 `0600`,key/日志不含凭据或正文 -- [x] 定义回退矩阵:transport、非 401/403 的不可用状态、body read 与非 HTML JSON 语法失败可回退;401/403、HTML/login/auth、schema/object/final decode 硬失败 -- [x] 定义损坏/空缓存 warn + skip 与 LKG 永不过期策略;`writtenAt`/年龄只做安全诊断,下一次合法在线成功原子覆盖 -- [x] OpenSpec 4/4 apply-ready,原件与镜像逐 artifact 字节一致,07 已获得 TDD 文件边界与可执行验收 - -## 交付记录 - -- **Change 原件(local-only):** `openspec/changes/remote-config-lkg/`(`proposal.md`、`design.md`、`specs/remote-config-lkg/spec.md`、`tasks.md`) -- **Tracked 镜像:** `.scratch/batch-b/config-lkg-spec.md` -- **校验 1:** `openspec validate --changes` → `✓ change/remote-config-lkg`,`1 passed, 0 failed` -- **校验 2:** `openspec validate remote-config-lkg --type change --strict --no-interactive` → `Change 'remote-config-lkg' is valid` -- **基线/范围:** `dev@4675435d94d462d2f9317d6688ddab2f0105c746`;本票没有修改 `packages/**` 或生产代码 diff --git a/.scratch/batch-b/issues/07-o1-lkg-implement.md b/.scratch/batch-b/issues/07-o1-lkg-implement.md deleted file mode 100644 index d0c367c4b1..0000000000 --- a/.scratch/batch-b/issues/07-o1-lkg-implement.md +++ /dev/null @@ -1,45 +0,0 @@ -# 07 — O1:实现 remote config last-known-good 缓存 - -**What to build:** 按 06 产出的已校验 OpenSpec 实现 LKG,并扩展现有 `packages/opencode/test/config/wellknown-offline.test.ts`;不得重新实现已存在的 warn + skip 离线降级。 - -**Evidence:** `.scratch/batch-b/evidence.md#o1--remote-config-last-known-good` -**Branch:** `feat/config-lkg` -**Blocked by:** 无(06 已关闭;OpenSpec `remote-config-lkg` 为 apply-ready) -**Status:** closed - -- [x] 06 的 OpenSpec requirements/scenarios 已写入下方“规格入口”;实施以 tracked 镜像为稳定入口,以 local-only change 为 OpenSpec 原件 -- [x] 在线成功后产生可复用 LKG,随后 transport/body 失败按规格回退 -- [x] auth/HTML login/decode 失败仍保持硬失败 -- [x] 损坏缓存不崩溃、不覆盖错误类别,且日志不含凭据 -- [x] 在 `packages/opencode` 运行目标测试与 `bun typecheck` - -## 实施证据 - -- **基线/分支:** `af0be65b0831c352cad28e6a32ac1c3c883fc5d8` → `feat/config-lkg` -- **公开 seam:** `Config.Service.get()` 的 well-known/remote-config 加载结果;持久化细节通过 `RemoteLkg.digest/read/write` 窄接口验证 -- **RED:** 在线→transport 新实例期望 `lkg/transport-model`、实际 `undefined`;持久化测试因 `@/config/remote-lkg` 不存在失败;401/403 预实现错误地返回成功 -- **GREEN:** `bun test test/config/remote-lkg.test.ts test/config/wellknown-offline.test.ts` 连续 3 次 `24 pass / 0 fail`;`bun typecheck` 绿色 -- **OpenSpec:** `remote-config-lkg` 为 `14/14`、`all_done`;`openspec validate --changes` 为 `1 passed, 0 failed`,strict change validate 为 valid;tracked 镜像 4/4 artifacts 与当前 local-only 原件逐字节一致 -- **范围:** 4 个指定代码/测试文件;无 fixture、依赖、lockfile、core、HTTP/SDK 改动 - -## 规格入口 - -- [只持久化完整验证成功的原始远端响应](../config-lkg-spec.md#requirement-只持久化完整验证成功的原始远端响应):[在线写入→离线复用](../config-lkg-spec.md#scenario-在线成功后写入并可离线复用)、[Environment 秘密不固化](../config-lkg-spec.md#scenario-environment-秘密不被固化)、[验证失败不写入](../config-lkg-spec.md#scenario-下游验证失败不产生新-lkg) -- [缓存身份与诊断不得泄露凭据](../config-lkg-spec.md#requirement-缓存身份与诊断不得泄露凭据):[规范化 URL 稳定摘要](../config-lkg-spec.md#scenario-等价-url-使用同一稳定文件名)、[key/log 无凭据正文](../config-lkg-spec.md#scenario-凭据与正文不出现在-key-或日志) -- [LKG 更新必须原子且用户私有](../config-lkg-spec.md#requirement-lkg-更新必须原子且用户私有):[同目录 rename + `0600`](../config-lkg-spec.md#scenario-同目录原子替换并设置文件模式)、[失败更新保留旧 LKG](../config-lkg-spec.md#scenario-rename-前写入失败保留旧-lkg) -- [只有允许降级的在线失败可以回退](../config-lkg-spec.md#requirement-只有允许降级的在线失败可以回退):[transport/body 回退](../config-lkg-spec.md#scenario-transport-或非认证不可用状态回退)、[401/403 不回退](../config-lkg-spec.md#scenario-401-或-403-不回退)、[HTML 不回退](../config-lkg-spec.md#scenario-html-登录页不回退)、[decode 不回退](../config-lkg-spec.md#scenario-schema-decode-错误不回退) -- [不可用缓存保留原 warn + skip](../config-lkg-spec.md#requirement-不可用缓存保留原-warn--skip-语义)与[LKG 不因年龄自动过期](../config-lkg-spec.md#requirement-lkg-不因年龄自动过期):[第一/第二跳损坏缓存边界](../config-lkg-spec.md#scenario-第一跳损坏或空缓存告警后跳过来源)、[长期离线继续使用](../config-lkg-spec.md#scenario-很旧的-lkg-仍支持长期离线) - -## 实现边界 - -- **生产文件:** 仅修改 `packages/opencode/src/config/config.ts`,新增 `packages/opencode/src/config/remote-lkg.ts` -- **测试文件:** 扩展 `packages/opencode/test/config/wellknown-offline.test.ts`,新增 `packages/opencode/test/config/remote-lkg.test.ts` -- **禁止扩张:** 不修改 `packages/core`、HTTP routes、SDK/生成物、依赖或配置 schema;不重写当前第一跳 skip / 第二跳空 fetched-config 的 warn + skip 结果 -- **方法:** 严格按镜像末尾 `tasks.md` 红-绿顺序实施;rename 失败只允许最窄文件系统故障注入,其余路径使用真实实现 - -## 可执行验收 - -1. `cd packages/opencode && bun test test/config/remote-lkg.test.ts test/config/wellknown-offline.test.ts` -2. `cd packages/opencode && bun typecheck` -3. 核对目标测试覆盖在线→离线、失败写入后旧 LKG、401/403/HTML/decode 硬失败、损坏/空缓存、key/log 无凭据、永久 LKG、`0600` 与同目录原子 rename -4. 核对实现 diff 只包含上述四个主要文件;任何最小 fixture 例外必须在提交说明中单列 diff --git a/.scratch/batch-b/issues/08-s7-recovery-invented-diagnosis.md b/.scratch/batch-b/issues/08-s7-recovery-invented-diagnosis.md deleted file mode 100644 index fcc13269c1..0000000000 --- a/.scratch/batch-b/issues/08-s7-recovery-invented-diagnosis.md +++ /dev/null @@ -1,23 +0,0 @@ -# 08 — S7:诊断 recovery INVENTED 推断 - -**What to build:** 仅诊断“已语义完成的 durable transcript 被 recovery 判为 active/ownershipLost 并写入 `exec_failed`”是否可复现。当前票不预设存在缺陷,也不授权先改生产代码。 - -**Method:** `/diagnosing-bugs` -**Evidence:** `.scratch/batch-b/evidence.md#s7--recovery-invented-推断` -**Branch:** `test/recovery-diagnosis` -**Blocked by:** 无(07 已关闭) -**Status:** closed-no-fix - -- [x] 第一项产出是一条确定性、快速、可由 agent 重复运行且能红灯的命令;在此之前不写理论/修复 -- [x] 症状必须包含“durable transcript 语义完成”与“reconcile 实际写 failed”,不能只单测 helper 返回 active -- [x] 红灯成立后才列 3–5 个可证伪假设、最小化复现并另开独立修复票(未出现红灯,因此未进入该阶段) -- [x] 无法建立反馈回路时记录尝试和阻塞原因,以 no-fix 关闭(已建立反馈回路且症状未复现,按 no-fix 关闭) -- [x] 不把既有 ownershipLost → workflow pause 缓解误报为未覆盖 - -## 关闭证据 - -- 命令:`cd packages/opencode && bun test test/dag/dag-recovery-transcript-diagnosis.test.ts` -- 三次结果:均为 `2 pass / 0 fail`,约 `1.13s`。 -- 完成态:真实 durable transcript `finish: "stop"` 经 `DagLoop.init → reconcileWorkflow` 后持久化为 `completed`,未写 `exec_failed`。 -- 对照态:真实 durable transcript `finish: "tool-calls"` 经同一路径持久化为 `failed/exec_failed`,workflow 随后被既有 recovery-pause 置为 `paused`。 -- 完整报告:`.scratch/batch-b/s7-diagnosis.md`;未创建票 09。 diff --git a/.scratch/batch-b/issues/09-promote-dev-main-release.md b/.scratch/batch-b/issues/09-promote-dev-main-release.md deleted file mode 100644 index a7bce5d9fa..0000000000 --- a/.scratch/batch-b/issues/09-promote-dev-main-release.md +++ /dev/null @@ -1,12 +0,0 @@ -# 09 — 收束:批 B+C 后 dev → main → release-fork - -**What to build:** 批 B 所有票关闭、批 C P8 完成观测处置后,确认最新 `dev` 全量 CI 绿色,发 dev→main 晋级 PR;四项门禁全绿后合并并手动运行 release-fork。 - -**Blocked by:** 01–08 全部关闭;`.scratch/batch-c/issues/01-p8-spawn-ready-observation.md` 已处置 -**Status:** blocked - -- [ ] 最新 dev push 的 Typecheck、Unit Tests (linux)、E2E Tests (linux/windows) 全绿 -- [ ] 每票 PR/merge commit/验证命令已回填,S7 若确认缺陷则其独立修复票也关闭 -- [ ] dev→main PR 描述列出批 B 交付与批 C no-code/benchmark 裁决 -- [ ] 四项 main PR 门禁全绿后合并 -- [ ] release-fork 从 main 成功产出正式版,再执行用户确认过的分支/worktree 清理 diff --git a/.scratch/batch-b/s7-diagnosis.md b/.scratch/batch-b/s7-diagnosis.md deleted file mode 100644 index aae0f0126c..0000000000 --- a/.scratch/batch-b/s7-diagnosis.md +++ /dev/null @@ -1,53 +0,0 @@ -# S7 — recovery INVENTED 推断诊断 - -- **Status:** closed-no-fix -- **基线:** `dev@18273554f4f2c18cab1370922eb1ec004ba5bad9` -- **分支:** `test/recovery-diagnosis` -- **日期:** 2026-08-09 - -## 反馈回路 - -从 `packages/opencode` 运行: - -```bash -bun test test/dag/dag-recovery-transcript-diagnosis.test.ts -``` - -诊断期间使用的 throwaway 测试已删除。它走过以下真实持久化链路: - -1. `Session.layer` 通过 `Session.updateMessage` 发布 durable transcript 事件,`SessionProjector` 写入数据库;测试再用 `Session.messages` 读回并断言完成边界。 -2. `DagLoop.init` 扫描 durable running workflow,进入 `recoverWorkflow`。 -3. `makeSessionStatusChecker` 通过真实 `Session.get/messages` 读取 child transcript,`reconcileWorkflow` 作出 settlement。 -4. `Dag.nodeCompleted/nodeFailed` 发布事件,`DagProjector` 投影到真实 `DagStore`;测试直接读取 node/workflow 持久化结果。 - -观测断言: - -| 输入 | transcript 证据 | 实际持久化结果 | workflow 结果 | -|---|---|---|---| -| 语义完成 | 最后一条 assistant 为 `finish: "stop"`,并带 `time.completed` | node `completed`,`errorClass: null` | `completed` | -| red-capable 对照 | 最后一条 assistant 为 `finish: "tool-calls"` | node `failed`,`errorClass: "exec_failed"` | `paused` | - -## 运行结果 - -最终 `DagLoop.init` seam 连续运行三次,均为 `2 pass / 0 fail`: - -| 次数 | 结果 | 耗时 | -|---|---|---| -| 1 | `2 pass / 0 fail` | `1.129s` | -| 2 | `2 pass / 0 fail` | `1.131s` | -| 3 | `2 pass / 0 fail` | `1.133s` | - -首次运行因工作区尚未安装 `@opentui/solid/preload`,在加载测试前退出;执行 `bun install --no-save` 后依赖就绪,未修改 lockfile。随后先在真实 `Session → reconcileWorkflow → DagStore` seam 连续跑绿三次,再收紧到上述 `DagLoop.init` seam 并连续跑绿三次。 - -## 语义边界核对 - -- `packages/opencode/src/session/prompt.ts` 的真实 loop 只有在最后 assistant 已有 finish、finish 不是 `tool-calls`、没有待处理 tool call 且 assistant 位于最后 user 之后时才走完成退出。 -- 同一 loop 将 `tool-calls` 与 `unknown` 明确视为需要 continuation;因此这两类 transcript 不能作为“系统语义已经完成”的证据。 -- 测试没有使用自定义完成布尔值。完成态由真实持久化 transcript 中的 `finish: "stop"` 与 `time.completed` 证明,并经生产 `Session.messages` 读回。 -- `tool-calls` 对照确实经过 recovery 写入 `exec_failed`,随后触发现有 `ownershipLost → workflow pause` 缓解;该行为用于证明反馈回路可红,不作为新缺陷上报。 - -## 结论 - -精确症状“已语义完成的 durable transcript 被 recovery 判为 active/ownershipLost,并实际持久化 `exec_failed`”未复现。完成态 transcript 在真实 recovery/loop 调用链中稳定投影为 node/workflow `completed`;会写 `exec_failed` 的对照 transcript 按现有 Session loop 语义仍需 continuation。 - -本票不修改生产代码,不保留诊断测试,不创建猜测性修复票,也不创建票 09。 diff --git a/.scratch/batch-c/issues/01-p8-spawn-ready-observation.md b/.scratch/batch-c/issues/01-p8-spawn-ready-observation.md deleted file mode 100644 index d4be883719..0000000000 --- a/.scratch/batch-c/issues/01-p8-spawn-ready-observation.md +++ /dev/null @@ -1,30 +0,0 @@ -# 01 — P8:spawnReady 复杂度观测处置 - -**What to build:** 记录 `spawnReady O(ready × nodes)` 是否有实际性能证据。没有 trace/benchmark/用户痛点时,以 no-code 关闭;不得仅凭静态复杂度实施缓存或索引改造。 - -**Evidence:** `.scratch/batch-b/evidence.md#p8--spawnready-复杂度` -**Branch:** `docs/p8-observation` -**Blocked by:** None -**Status:** closed-no-code - -- [x] 搜集已有生产 trace、benchmark 或明确用户场景,不为本票新造大规模优化工程 -- [x] 无量化证据:记录“当前不做”与重开阈值,状态改 closed-no-code -- [x] 有量化证据:另开 `/improve-codebase-architecture` 设计票,写明基线与目标(本次无量化证据,因此未开票) -- [x] 本观测票本身不改 `spawnReady` - -## 关闭结论 - -- 已复核 promotion evidence 与 DAG deep-review 报告;只有静态 `O(ready × nodes)` 推断。 -- 未发现生产 trace、可重复 benchmark 或明确用户场景能把可感知延迟归因到 `spawnReady`。 -- 当前不增加索引或缓存;在没有收益基线时,这类状态会扩大一致性与失效维护面。 -- 本票仅记录裁决,生产代码零改动。 - -## 重开阈值 - -满足任一条件时重开独立性能设计票: - -1. 用户态或生产 profile 将调度延迟明确归因到 `spawnReady`。 -2. 可重复 benchmark 显示 `spawnReady` 占一次 wake 调度耗时的 10% 以上。 -3. 实际工作流规模长期超过当前评审采用的 50 节点观察区间。 - -重开后必须先记录基线、目标与代表性图规模,再选择索引或缓存方案。 diff --git a/AGENTS.md b/AGENTS.md index b6a7f32ee1..6a95435adf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,6 +215,12 @@ Invariants for extending the SolidJS/opentui TUI. The DAG inspector (`src/featur - Keep EventV2 replay owner claims separate from clustered Session execution ownership. - Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned. +## DAG Configuration Repository + +The authoritative repository for curated DAG workflow YAML and configuration-owned block or prompt assets is [`LeXwDeX/opencode-dag-config`](https://github.com/LeXwDeX/opencode-dag-config). Inspect and update that repository when a task changes reference workflows, composable block configurations, or their embedded worker prompts; configuration-only changes do not belong in this runtime repository. + +This repository owns the DAG schema, compiler, validator, runtime, and release integration. Changes that cross the boundary land runtime support first, then update the config repository's `runtime-compat.json` to the merged full runtime commit SHA and pass its template-validation CI. + ## Agent skills ### Issue tracker diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..f3b1950516 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,178 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +**OpenCode-GraphAgent** (product name "GraphAgent"): a fork of the MIT-licensed +[opencode](https://github.com/anomalyco/opencode) terminal AI agent that adds a +**DAG workflow engine** for multi-agent orchestration. A task is decomposed into a +dependency graph of child-agent sessions, driven to completion with durable, +crash-recoverable, inspectable state. Upstream opencode capabilities (multi-provider +LLM, built-in LSP, TUI/desktop/web clients, client/server architecture) are preserved. + +**`AGENTS.md` is the canonical contributor guide.** It holds the full style guide, +git workflow (铁律), and二次开发 (extending) invariants. Read it for *how* to write +code here; this file covers the *what* and the big-picture architecture, and does not +repeat AGENTS.md. Default branch is `main`. + +## Commands + +Requirements: **Bun 1.3+** (`packageManager: bun@1.3.14`). All commands run from repo +root unless noted. + +```bash +bun install # install (postinstall fixes node-pty) + +# Run the app (bun dev == local equivalent of the built `opencode` CLI) +bun dev # TUI, in packages/opencode by default +bun dev # TUI against another dir (`bun dev .` for repo root) +bun dev serve # headless HTTP API server (default port 4096) +bun dev serve --port 8080 # custom port +bun dev web # server + web UI +bun run --cwd packages/app dev # web app dev server (needs `bun dev serve` running) +bun run --cwd packages/desktop dev # Electron desktop app + +# Quality gates +bun typecheck # turbo typecheck across all packages (the commit gate) +bun typecheck # also runnable from a package dir, e.g. packages/opencode +bun lint # oxlint, ratcheted: --max-warnings=4852 (see below) + +# Tests — NEVER run from repo root (guard: do-not-run-tests-from-root; bunfig enforces it) +cd packages/opencode && bun test # full suite (only-failures shown) +cd packages/opencode && bun test path/to/file.test.ts # one file +cd packages/opencode && bun test --test-name-pattern "pattern" # filtered tests +cd packages/opencode && bun run test:dag-core # DAG scheduling/state-machine coverage gate +cd packages/opencode && bun run test:httpapi # HTTP API contract exerciser (3 modes) + +# Build & codegen +./packages/opencode/script/build.ts --single # standalone binary → packages/opencode/dist//bin/opencode +./packages/sdk/js/script/build.ts # regenerate the JS SDK from the OpenAPI spec (after HTTP route changes) +bun run generate # root: regen SDK + openapi.json + format (wrapper of the above) +``` + +**`bun typecheck` (`tsgo --noEmit`) is the real gate.** `bun run build` uses esbuild +and transpiles only — a green build can still ship a missing import or non-existent API. +Never invoke `tsc` directly. + +**Lint ratchet:** `bun lint` runs `oxlint --max-warnings=4852`. The threshold only ever +tightens — new warnings fail CI and the pre-commit hook. When you fix existing warnings, +lower the number in the root `package.json` `lint` script to match (rationale recorded in +the `_lint_ratchet_note` field there). `oxlint` is `typeAware: true`. + +Pre-commit (husky) runs `lint` + `typecheck`. `post-checkout`/`pre-push` hooks also exist. + +## Architecture + +### Monorepo layout (Bun workspaces + Turborepo) + +`packages/core` (`@opencode-ai/core`) is the framework layer: pure domain logic, the +plugin/SDK, schema, storage, event system, and the **pure half of the DAG engine**. +`packages/opencode` (`opencode`) is the application: the CLI/server entrypoint, session +runtime, HTTP server, and the **execution half of the DAG engine**. `core` has no +dependency on `opencode`; the arrow points the other way. + +Key packages: + +| Package | Role | +|---|---| +| `packages/core` | Domain primitives, storage, schema, events, **pure DAG state machine/projector/store** | +| `packages/opencode` | CLI + headless server, session runtime, **DAG execution/loop/spawn/admission/recovery** | +| `packages/tui` | Terminal UI (SolidJS + opentui), incl. the DAG inspector (`src/feature-plugins/system/dag-inspector.tsx`) | +| `packages/app` · `packages/web` · `packages/desktop` | Web components / web app / Electron wrapper | +| `packages/sdk/js` | `@opencode-ai/sdk` — **generated** from the server's OpenAPI spec (`src/v2/gen`) | +| `packages/plugin` · `packages/schema` · `packages/protocol` · `packages/client` | Plugin SDK, event/schema definitions, wire protocol, server client | + +### The DAG engine is split across two packages (the non-obvious part) + +The workflow engine is the fork's reason for existing. It is deliberately divided: + +- **`packages/core/src/dag`** — *pure, side-effect-free*: declared state-machine transition + tables (`core/transitions.ts`), dependency graph + cycle/dangling validation + (`core/graph.ts`), wave-based scheduler (`core/scheduling.ts`), replan fragment merge + (`core/replan.ts`), and the **event projector** (`projector.ts`) that writes the SQLite + read model *inside* the event-publish transaction. History is event replay, not a log + table. `store.ts` / `sql.ts` are the persistence boundary. +- **`packages/opencode/src/dag`** — *effectful execution*: the workflow service (`dag.ts`, + `workflows.ts`), the execution loop (`runtime/loop.ts`), spawning real child sessions per + node (`runtime/spawn.ts`, same path as the `task` tool), deep-mode admission Q&A + (`admission.ts`), the `design`/`diff` review lifecycle with implementation-fingerprint + contracts (`review-lifecycle.ts`), lazy evidence-based crash recovery + (`runtime/recovery.ts`), and prompt-template resolution (`templates/`). + +A node never names its own model — the graph declares which nodes are *critical* and +`.opencode/dag.jsonc` decides what model runs each tier (`advanced` / `standard`). +Agents drive workflows through a single `workflow` tool; humans observe/control via the +TUI DAG inspector or the `GET/POST /dag*` HTTP routes. + +### Effect-TS is the composition backbone + +The codebase is built on `effect` 4.0.0-beta. Services are `Context.Tag`s wired through +`Layer`s. Two parallel composition systems coexist and **do not share wiring**: + +1. `X.defaultLayer` / `AppLayer` — the primary Effect layer graph. +2. `LayerNode` (`.node` exports, `LayerNode.buildLayer`) — a separate node-based system. + +Both demand **self-contained layers**: a `defaultLayer` must `Layer.provide` every +dependency its body `yield*`s. `Layer.provideMerge(self, layer)` builds `layer` in +isolation, and `Layer.mergeAll` does not cross-provide siblings — so a layer that quietly +assumes an ambient service will compile clean and crash at runtime in a different entry +point. Optional/heavyweight cross-deps (Provider, MCP, HttpClient) are resolved lazily via +`Effect.serviceOption(Tag)` at the call site. See AGENTS.md "Extending the Codebase" for +the full invariant list — the build will not catch violations of these. + +### Configuration & data files (all under `.opencode/`) + +| Path | Purpose | +|---|---| +| `.opencode/dag.jsonc` | Model tiers + `thinking_depth` for DAG child sessions (global counterpart in opencode config dir) | +| `.opencode/workflows/*.yaml` | Project-local saved workflow specs; curated workflows live in the config repository | +| `.opencode/dag-prompts/*.md` | Project-local node prompt templates referenced by `prompt_template.id` | +| `.opencode/command/*.md` | Custom slash commands (`commit`, `issues`, `changelog`, `translate`, `learn`, …) | +| `.opencode/opencode.jsonc` | Main app config | + +Curated *global* workflows live in a separate repo, [`LeXwDeX/opencode-dag-config`](https://github.com/LeXwDeX/opencode-dag-config); config-only changes belong there, not here. `dag.jsonc` and the workflow library are read lazily — edits apply to the next workflow start without a restart. + +### Spec-driven & domain docs + +- **`openspec/`** — spec-driven change proposals. `openspec/changes//` holds + `proposal.md` / `design.md` / `tasks.md` / `specs/`; `openspec/specs/` holds the + established capability specs. Active proposals (e.g. `harden-goal-state-machine`, + `internalize-dag-block-capabilities`) define in-flight work. +- **`CONTEXT-MAP.md` → `CONTEXT.md`** — multi-context domain docs. `CONTEXT-MAP.md` is the + index; read the linked `CONTEXT.md`(s) relevant to the area before working in it. A DAG + `CONTEXT.md` does not yet exist. +- **`docs/agents/`** — issue-tracker workflow, triage labels, domain-doc conventions. + +## Critical, non-obvious rules + +These compile clean but bite at runtime or in CI — the build will not catch them: + +- **Regenerate the SDK after touching any HTTP API route.** `packages/sdk/js` is generated + from the server's OpenAPI spec; a stale SDK breaks the TUI at runtime (calling a client + method that doesn't exist) in a way typecheck can't catch. After route changes run + `./packages/sdk/js/script/build.ts`. CI's `Check generated SDK` step + (`bun run check:generated` = regen + `git diff --exit-code -- src/v2/gen`) enforces this. +- **Changing an HTTP route's request/response shape** requires updating its scenario in + `test/server/httpapi-exercise/index.ts`; `bun run test:httpapi --fail-on-missing` fails otherwise. +- **Don't hand-duplicate SDK types in TUI/plugin code** — re-export the generated type so a + server schema change surfaces as a typecheck error instead of silent drift. +- **Every event type the TUI consumes** must be `define()`d in `packages/schema` and listed + in `Event manifest.Definitions`, or the generated event union won't contain it. Ephemeral + push events (e.g. `dag.workflow.summary.updated`) stay OUT of the durable manifest — emit + via `GlobalBus`, never persist, design consumers to tolerate missed events (re-fetch on bootstrap). +- **Adding a service other services see:** find every consumer's `.node` list (not just its + `defaultLayer`) and add the new service's node there. A missing wire compiles clean and + fails silently (feature no-ops) rather than erroring. +- **Mixed license:** upstream code is MIT; the DAG engine + (`packages/core/src/dag/**`, `packages/opencode/src/dag/**`) is AGPL-3.0-or-later. Exact + boundaries are in `NOTICE`. Don't move AGPL code into MIT-licensed paths or vice versa. + +## Git workflow (summary — full rules in AGENTS.md) + +`feat/fix` branches → PR (Typecheck gate) → `dev` (fast integration, push runs full tests) → +PR (full gate: Typecheck + Unit + E2E on linux+windows) → `main` → manual release. Direct +pushes to `main`/`dev` are blocked by GitHub Rulesets. Branch names: `{type}/{short-name}` +(`feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `release`, `hotfix`), enforced by +Ruleset. Commits/PR titles: conventional `type(scope): summary`. All PRs must reference an +existing issue (`Fixes #N`). Curated DAG configs are owned by the `opencode-dag-config` repo. From 0a1f1d31aee93373d326209d95e10abc2c5715bd Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:18:48 +0800 Subject: [PATCH 03/11] chore(repo): remove legacy issue forms --- .github/ISSUE_TEMPLATE/bug-report.yml | 66 ---------------------- .github/ISSUE_TEMPLATE/config.yml | 5 -- .github/ISSUE_TEMPLATE/feature-request.yml | 19 ------- 3 files changed, 90 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml deleted file mode 100644 index da82bde2e1..0000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Bug report -description: Report an issue that should be fixed (avoid pasting giant AI generated summaries or your issue may be closed/ignored) -body: - - type: textarea - id: description - attributes: - label: Description - description: Describe the bug you encountered - placeholder: What happened? - validations: - required: true - - - type: input - id: plugins - attributes: - label: Plugins - description: What plugins are you using? - validations: - required: false - - - type: input - id: opencode-version - attributes: - label: OpenCode version - description: What version of OpenCode are you using? - validations: - required: false - - - type: textarea - id: reproduce - attributes: - label: Steps to reproduce - description: How can we reproduce this issue? - placeholder: | - 1. - 2. - 3. - validations: - required: false - - - type: textarea - id: screenshot-or-link - attributes: - label: Screenshot and/or share link - description: Run `/share` to get a share link, or attach a screenshot - placeholder: Paste link or drag and drop screenshot here - validations: - required: false - - - type: input - id: os - attributes: - label: Operating System - description: what OS are you using? - placeholder: e.g., macOS 26.0.1, Ubuntu 22.04, Windows 11 - validations: - required: false - - - type: input - id: terminal - attributes: - label: Terminal - description: what terminal are you using? - placeholder: e.g., iTerm2, Ghostty, Alacritty, Windows Terminal - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 9501a1be65..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: 💬 Discord Community - url: https://discord.gg/opencode - about: For support, troubleshooting, how-to questions, and real-time discussion. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 42f1d3c51a..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: 🚀 Feature Request -description: Suggest an idea, feature, or enhancement -title: "[FEATURE]:" - -body: - - type: checkboxes - id: verified - attributes: - label: Feature hasn't been suggested before. - options: - - label: I have verified this feature I'm about to request hasn't been suggested before. - required: true - - - type: textarea - attributes: - label: Describe the enhancement you want to request - description: What do you want to change or add? What are the benefits of implementing this? Try to be detailed so we can understand your request better :) - validations: - required: true From 674fa4a265f361b2cebbb198e0ecbb9c863dbb55 Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:19:50 +0800 Subject: [PATCH 04/11] fix(ci): provide node 24 for native installs --- .github/actions/setup-bun/action.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index e0ac9a8c0c..4451fa0aed 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -12,6 +12,13 @@ inputs: runs: using: "composite" steps: + # node-gyp@latest (invoked via bunx for native install scripts) requires Node >=22; + # some runner images ship an older system Node on PATH + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" + - name: Get baseline download URL id: bun-url shell: bash From cb74cc26065cd5858827d3668b5848e4818b5459 Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:19:16 +0800 Subject: [PATCH 05/11] test(provider): cover internal aggregator endpoints --- docs/examples/internal-llm-aggregator.jsonc | 60 +++++++++++++++ .../opencode/test/provider/provider.test.ts | 76 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 docs/examples/internal-llm-aggregator.jsonc diff --git a/docs/examples/internal-llm-aggregator.jsonc b/docs/examples/internal-llm-aggregator.jsonc new file mode 100644 index 0000000000..e37503179d --- /dev/null +++ b/docs/examples/internal-llm-aggregator.jsonc @@ -0,0 +1,60 @@ +{ + // 聚合平台按其暴露的标准协议分别配置端点。 + // 这不是网络白名单;普通公网与封闭内网使用同一套 Provider 机制。 + "provider": { + "internal-openai": { + "name": "Internal OpenAI Protocol", + "npm": "@ai-sdk/openai", + "env": ["LLM_AGGREGATOR_API_KEY"], + "options": { + "baseURL": "https://llm-api.example.internal/openai/v1" + }, + "models": { + "gpt-5": { + "name": "GPT-5 via Internal Aggregator", + "tool_call": true, + "limit": { + "context": 400000, + "output": 128000 + } + } + } + }, + "internal-anthropic": { + "name": "Internal Anthropic Protocol", + "npm": "@ai-sdk/anthropic", + "env": ["LLM_AGGREGATOR_API_KEY"], + "options": { + "baseURL": "https://llm-api.example.internal/anthropic" + }, + "models": { + "claude-sonnet-4-20250514": { + "name": "Claude Sonnet 4 via Internal Aggregator", + "tool_call": true, + "limit": { + "context": 200000, + "output": 64000 + } + } + } + }, + "internal-gemini": { + "name": "Internal Gemini Protocol", + "npm": "@ai-sdk/google", + "env": ["LLM_AGGREGATOR_API_KEY"], + "options": { + "baseURL": "https://llm-api.example.internal/gemini/v1beta" + }, + "models": { + "gemini-2.5-pro": { + "name": "Gemini 2.5 Pro via Internal Aggregator", + "tool_call": true, + "limit": { + "context": 1048576, + "output": 65536 + } + } + } + } + } +} diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index e2eb4dc89a..84ae9fddee 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -3,6 +3,7 @@ import { mkdir, unlink } from "fs/promises" import path from "path" import { Effect, Layer } from "effect" import { ModelsDev } from "@opencode-ai/core/models-dev" +import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { FSUtil } from "@opencode-ai/core/fs-util" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Global } from "@opencode-ai/core/global" @@ -16,6 +17,7 @@ import { import { markPluginDependenciesReady } from "../fixture/plugin" import { Auth } from "@/auth" import { Config } from "@/config/config" +import { ConfigParse } from "@/config/parse" import { Env } from "../../src/env" import { Plugin } from "../../src/plugin/index" import { Provider } from "@/provider/provider" @@ -83,6 +85,34 @@ const paid = (providers: Record (language as { config: { baseURL: string } }).config.baseURL +const aggregatorExamplePath = path.join(import.meta.dir, "../../../../docs/examples/internal-llm-aggregator.jsonc") +const aggregatorExample = ConfigParse.schema( + ConfigV1.Info, + ConfigParse.jsonc(await Bun.file(aggregatorExamplePath).text(), aggregatorExamplePath), + aggregatorExamplePath, +) + +const emptyModelCatalogLayer = Layer.succeed( + ModelsDev.Service, + ModelsDev.Service.of({ + get: () => Effect.succeed({}), + refresh: () => Effect.dieMessage("models.dev refresh must not be called by the aggregator contract"), + }), +) + +const aggregatorProviderLayer = Provider.layer.pipe( + Layer.provide(FSUtil.defaultLayer), + Layer.provide(Env.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(Auth.defaultLayer), + Layer.provide(Plugin.defaultLayer), + Layer.provide(emptyModelCatalogLayer), + Layer.provide(RuntimeFlags.layer({})), +) +const aggregator = testEffect( + Layer.mergeAll(aggregatorProviderLayer, Env.defaultLayer, Plugin.defaultLayer, emptyModelCatalogLayer), +) + const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer)) const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true })) @@ -394,6 +424,52 @@ it.instance( }, ) +aggregator.instance( + "internal aggregator example resolves three protocol SDKs without models.dev", + Effect.gen(function* () { + yield* set("LLM_AGGREGATOR_API_KEY", "internal-test-key") + const provider = yield* Provider.Service + const providers = yield* provider.list() + const modelsDev = yield* ModelsDev.Service + + expect(yield* modelsDev.get()).toEqual({}) + + yield* Effect.forEach( + [ + { + providerID: ProviderV2.ID.make("internal-openai"), + modelID: ModelV2.ID.make("gpt-5"), + npm: "@ai-sdk/openai", + baseURL: "https://llm-api.example.internal/openai/v1", + }, + { + providerID: ProviderV2.ID.make("internal-anthropic"), + modelID: ModelV2.ID.make("claude-sonnet-4-20250514"), + npm: "@ai-sdk/anthropic", + baseURL: "https://llm-api.example.internal/anthropic", + }, + { + providerID: ProviderV2.ID.make("internal-gemini"), + modelID: ModelV2.ID.make("gemini-2.5-pro"), + npm: "@ai-sdk/google", + baseURL: "https://llm-api.example.internal/gemini/v1beta", + }, + ], + (item) => + Effect.gen(function* () { + expect(providers[item.providerID]).toBeDefined() + expect(providers[item.providerID].key).toBe("internal-test-key") + expect(providers[item.providerID].options.baseURL).toBe(item.baseURL) + expect(providers[item.providerID].models[item.modelID].api.npm).toBe(item.npm) + + const model = yield* provider.getModel(item.providerID, item.modelID) + expect(yield* provider.getLanguage(model)).toBeDefined() + }), + ) + }), + { config: aggregatorExample }, +) + it.instance( "model cost defaults to zero when not specified", Effect.gen(function* () { From dae9a9e86d2949cbb6f3613411d35d0da413081d Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:22:14 +0800 Subject: [PATCH 06/11] fixup! test(provider): cover internal aggregator endpoints --- packages/opencode/test/provider/provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 84ae9fddee..9f5de9c2aa 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -96,7 +96,7 @@ const emptyModelCatalogLayer = Layer.succeed( ModelsDev.Service, ModelsDev.Service.of({ get: () => Effect.succeed({}), - refresh: () => Effect.dieMessage("models.dev refresh must not be called by the aggregator contract"), + refresh: () => Effect.die(new Error("models.dev refresh must not be called by the aggregator contract")), }), ) From 04629f11f095567138343bbfacc3afe81db7e715 Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:22:58 +0800 Subject: [PATCH 07/11] feat(core): add verified runtime assets --- CORRESPONDING_SOURCE.md | 21 + LICENSE-SCOPE.json | 44 ++ LICENSE-SCOPE.schema.json | 54 +++ NOTICE | 29 +- bun.lock | 1 + packages/core/package.json | 2 + packages/core/src/dag/core/graph.ts | 3 + packages/core/src/dag/core/replan.ts | 3 + .../core/src/dag/core/required-validator.ts | 3 + packages/core/src/dag/core/scheduling.ts | 3 + packages/core/src/dag/core/transitions.ts | 3 + packages/core/src/dag/core/types.ts | 3 + packages/core/src/dag/projector.ts | 3 + packages/core/src/dag/sql.ts | 3 + packages/core/src/dag/store.ts | 3 + packages/core/src/ripgrep/binary.ts | 133 +---- packages/core/src/runtime-asset/LICENSE | 235 +++++++++ .../core/src/runtime-asset/MODIFICATIONS.md | 32 ++ .../core/src/runtime-asset/SPDX-HEADER.txt | 2 + .../core/src/runtime-asset/catalog/ripgrep.ts | 89 ++++ packages/core/src/runtime-asset/index.ts | 456 ++++++++++++++++++ .../core/test/corresponding-source.test.ts | 77 +++ packages/core/test/license-scope.test.ts | 114 +++++ packages/core/test/ripgrep-binary.test.ts | 57 +++ .../core/test/runtime-asset-cache.test.ts | 168 +++++++ packages/core/test/runtime-asset.test.ts | 159 ++++++ packages/desktop/electron-builder.config.ts | 5 + packages/desktop/package.json | 11 +- packages/desktop/scripts/prebuild.ts | 9 + packages/desktop/scripts/runtime-assets.ts | 92 ++++ .../desktop/scripts/verify-runtime-assets.ts | 10 + packages/desktop/src/main/server.ts | 1 + packages/desktop/test/runtime-assets.test.ts | 25 + packages/opencode/script/prefetch-ripgrep.ts | 223 +++------ packages/opencode/src/dag/admission.ts | 3 + packages/opencode/src/dag/config.ts | 3 + packages/opencode/src/dag/dag.ts | 3 + packages/opencode/src/dag/model.ts | 3 + packages/opencode/src/dag/review-lifecycle.ts | 3 + packages/opencode/src/dag/runtime/capture.ts | 3 + packages/opencode/src/dag/runtime/eval.ts | 3 + packages/opencode/src/dag/runtime/loop.ts | 3 + packages/opencode/src/dag/runtime/recovery.ts | 3 + packages/opencode/src/dag/runtime/spawn.ts | 3 + .../src/dag/runtime/summary-publisher.ts | 3 + .../opencode/src/dag/templates/resolve.ts | 3 + .../opencode/src/dag/templates/sanitize.ts | 3 + packages/opencode/src/dag/workflows.ts | 3 + .../routes/instance/httpapi/groups/dag.ts | 3 + .../routes/instance/httpapi/handlers/dag.ts | 3 + packages/opencode/src/tool/workflow.ts | 3 + packages/schema/src/dag-event.ts | 3 + .../src/feature-plugins/sidebar/dag-panel.tsx | 3 + .../system/dag-inspector-utils.ts | 3 + .../feature-plugins/system/dag-inspector.tsx | 3 + script/check-license-scope.ts | 198 ++++++++ script/corresponding-source.ts | 168 +++++++ 57 files changed, 2220 insertions(+), 285 deletions(-) create mode 100644 CORRESPONDING_SOURCE.md create mode 100644 LICENSE-SCOPE.json create mode 100644 LICENSE-SCOPE.schema.json create mode 100644 packages/core/src/runtime-asset/LICENSE create mode 100644 packages/core/src/runtime-asset/MODIFICATIONS.md create mode 100644 packages/core/src/runtime-asset/SPDX-HEADER.txt create mode 100644 packages/core/src/runtime-asset/catalog/ripgrep.ts create mode 100644 packages/core/src/runtime-asset/index.ts create mode 100644 packages/core/test/corresponding-source.test.ts create mode 100644 packages/core/test/license-scope.test.ts create mode 100644 packages/core/test/ripgrep-binary.test.ts create mode 100644 packages/core/test/runtime-asset-cache.test.ts create mode 100644 packages/core/test/runtime-asset.test.ts create mode 100644 packages/desktop/scripts/runtime-assets.ts create mode 100644 packages/desktop/scripts/verify-runtime-assets.ts create mode 100644 packages/desktop/test/runtime-assets.test.ts create mode 100644 script/check-license-scope.ts create mode 100644 script/corresponding-source.ts diff --git a/CORRESPONDING_SOURCE.md b/CORRESPONDING_SOURCE.md new file mode 100644 index 0000000000..5fa8a1f644 --- /dev/null +++ b/CORRESPONDING_SOURCE.md @@ -0,0 +1,21 @@ +# Corresponding source + +Every OpenCode-GraphAgent binary release that contains AGPL-covered code is +paired with a source archive generated from the same Git commit by: + +```sh +bun run ./script/corresponding-source.ts --version --output +``` + +The archive contains all tracked source and build inputs, including +`package.json`, `bun.lock`, build scripts, `LICENSE`, `NOTICE`, and +`LICENSE-SCOPE.json`. The adjacent JSON manifest records the exact commit, +commit timestamp, required build inputs, archive filename, and SHA-256 digest. + +Install the Bun version declared by the root `packageManager` field, restore +dependencies from `bun.lock`, and run the package-specific build documented in +the repository. External signing keys, certificates, credentials, and ordinary +system tools are not part of the source archive. + +For an internal deployment, publish the archive, JSON manifest, and checksum +next to the binaries or at the source URL configured for that deployment. diff --git a/LICENSE-SCOPE.json b/LICENSE-SCOPE.json new file mode 100644 index 0000000000..dbda7c0f9a --- /dev/null +++ b/LICENSE-SCOPE.json @@ -0,0 +1,44 @@ +{ + "$schema": "./LICENSE-SCOPE.schema.json", + "schemaVersion": 1, + "defaultLicense": { + "spdx": "MIT", + "licenseFile": "LICENSE", + "copyright": "Copyright (c) 2025 opencode" + }, + "scopes": [ + { + "id": "dag-workflow-engine", + "name": "DAG workflow engine", + "status": "active", + "spdx": "AGPL-3.0-or-later", + "copyright": "Copyright (c) 2026 LeXwDeX", + "licenseFiles": ["packages/core/src/dag/LICENSE", "packages/opencode/src/dag/LICENSE"], + "paths": [ + "packages/core/src/dag/", + "packages/opencode/src/dag/", + "packages/opencode/src/tool/workflow.ts", + "packages/schema/src/dag-event.ts", + "packages/tui/src/feature-plugins/system/dag-inspector.tsx", + "packages/tui/src/feature-plugins/system/dag-inspector-utils.ts", + "packages/tui/src/feature-plugins/sidebar/dag-panel.tsx", + "packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts", + "packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts" + ] + }, + { + "id": "portable-runtime", + "name": "Portable desktop runtime", + "status": "active", + "spdx": "AGPL-3.0-or-later", + "copyright": "Copyright (c) 2026 LeXwDeX", + "licenseFiles": ["packages/core/src/runtime-asset/LICENSE"], + "paths": [ + "packages/core/src/runtime-asset/", + "packages/opencode/script/prefetch-ripgrep.ts", + "packages/desktop/scripts/runtime-assets.ts", + "packages/desktop/scripts/verify-runtime-assets.ts" + ] + } + ] +} diff --git a/LICENSE-SCOPE.schema.json b/LICENSE-SCOPE.schema.json new file mode 100644 index 0000000000..6460640daf --- /dev/null +++ b/LICENSE-SCOPE.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/LeXwDeX/opencode-graph-agent/blob/main/LICENSE-SCOPE.schema.json", + "title": "OpenCode-GraphAgent license scope manifest", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "defaultLicense", "scopes"], + "properties": { + "$schema": { "type": "string" }, + "schemaVersion": { "const": 1 }, + "defaultLicense": { "$ref": "#/$defs/license" }, + "scopes": { + "type": "array", + "items": { "$ref": "#/$defs/scope" }, + "minItems": 1 + } + }, + "$defs": { + "license": { + "type": "object", + "additionalProperties": false, + "required": ["spdx", "licenseFile", "copyright"], + "properties": { + "spdx": { "type": "string", "minLength": 1 }, + "licenseFile": { "type": "string", "minLength": 1 }, + "copyright": { "type": "string", "minLength": 1 } + } + }, + "scope": { + "type": "object", + "additionalProperties": false, + "required": ["id", "name", "status", "spdx", "copyright", "licenseFiles", "paths"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "name": { "type": "string", "minLength": 1 }, + "status": { "enum": ["active", "planned"] }, + "spdx": { "const": "AGPL-3.0-or-later" }, + "copyright": { "type": "string", "minLength": 1 }, + "licenseFiles": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1, + "uniqueItems": true + }, + "paths": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1, + "uniqueItems": true + } + } + } + } +} diff --git a/NOTICE b/NOTICE index 1d067dbb89..a0d1755d9f 100644 --- a/NOTICE +++ b/NOTICE @@ -7,6 +7,10 @@ an AI coding agent. It is not affiliated with or endorsed by the OpenCode team. License boundaries ------------------ +The machine-readable source of truth for these boundaries is +`./LICENSE-SCOPE.json`. The human-readable paths below use the same values and +are checked for consistency in CI. + 1. Upstream opencode code (the vast majority of this repository) License: MIT @@ -17,14 +21,15 @@ License boundaries are minor patches to upstream files (bug fixes, localization fixes, hook system, tool improvements), remains under the upstream MIT license. -2. DAG workflow engine (self-developed by the fork author) +2. Self-developed AGPL modules License: GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) Text: ./packages/core/src/dag/LICENSE ./packages/opencode/src/dag/LICENSE + ./packages/core/src/runtime-asset/LICENSE Copyright (c) 2026 LeXwDeX - Covered directories and files: + 2.1 DAG workflow engine (active) - packages/core/src/dag/ DAG core: state machine, dependency graph, scheduling, event projection, @@ -42,11 +47,23 @@ License boundaries - packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts - packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts DAG HTTP API routes - - .opencode/dag-prompts/ DAG node prompt templates + 2.2 Portable desktop runtime (active) + + - packages/core/src/runtime-asset/ Runtime asset discovery, bundled + assets, verified cache, internal + mirrors, and public fallback + - packages/opencode/script/prefetch-ripgrep.ts + Verified ripgrep release prefetch + for distributable artifacts + - packages/desktop/scripts/runtime-assets.ts + - packages/desktop/scripts/verify-runtime-assets.ts + Required desktop runtime asset + preparation and package gate - The AGPL applies to these files and to derivative works of them, including - network-server deployments. Using the rest of the repository without the - DAG engine is governed by the MIT license alone. + The AGPL applies to these paths and to derivative works of them, including + network-server deployments. Files outside the scopes recorded in + `LICENSE-SCOPE.json` remain under their existing licenses unless they carry + an explicit license notice that says otherwise. When a file under an AGPL-covered directory imports MIT-licensed upstream modules, the upstream modules remain MIT; only the AGPL-covered files and diff --git a/bun.lock b/bun.lock index 2c045542f4..a86571ba07 100644 --- a/bun.lock +++ b/bun.lock @@ -372,6 +372,7 @@ "@actions/artifact": "4.0.0", "@lydell/node-pty": "catalog:", "@opencode-ai/app": "workspace:*", + "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", "@sentry/solid": "catalog:", "@sentry/vite-plugin": "catalog:", diff --git a/packages/core/package.json b/packages/core/package.json index ad78990294..d30579d4c8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,6 +18,8 @@ "exports": { "./session/runner": "./src/session/runner/index.ts", "./system-context": "./src/system-context/index.ts", + "./runtime-asset": "./src/runtime-asset/index.ts", + "./runtime-asset/catalog/*": "./src/runtime-asset/catalog/*.ts", "./dag/core/*": "./src/dag/core/*.ts", "./dag/*": "./src/dag/*.ts", "./*": "./src/*.ts" diff --git a/packages/core/src/dag/core/graph.ts b/packages/core/src/dag/core/graph.ts index 31cb026d56..baa6368d30 100644 --- a/packages/core/src/dag/core/graph.ts +++ b/packages/core/src/dag/core/graph.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG scheduling core — dependency graph. * diff --git a/packages/core/src/dag/core/replan.ts b/packages/core/src/dag/core/replan.ts index 08d839d83f..caca0e24b2 100644 --- a/packages/core/src/dag/core/replan.ts +++ b/packages/core/src/dag/core/replan.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG scheduling core — replan merge planning (D11, simplified model). * diff --git a/packages/core/src/dag/core/required-validator.ts b/packages/core/src/dag/core/required-validator.ts index c6f67ccf2d..551ed8b839 100644 --- a/packages/core/src/dag/core/required-validator.ts +++ b/packages/core/src/dag/core/required-validator.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG scheduling core — required-node validation. * diff --git a/packages/core/src/dag/core/scheduling.ts b/packages/core/src/dag/core/scheduling.ts index 7a988a8b34..9e17755ef2 100644 --- a/packages/core/src/dag/core/scheduling.ts +++ b/packages/core/src/dag/core/scheduling.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + import { DependencyGraph } from "./graph" export type SchedulingNodeStatus = "pending" | "running" | "satisfied" | "unsatisfied" | "skipped" diff --git a/packages/core/src/dag/core/transitions.ts b/packages/core/src/dag/core/transitions.ts index 6051098dfe..62137fa8da 100644 --- a/packages/core/src/dag/core/transitions.ts +++ b/packages/core/src/dag/core/transitions.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG scheduling core — transition-to-event mappings + status aggregation. * diff --git a/packages/core/src/dag/core/types.ts b/packages/core/src/dag/core/types.ts index f391dd6555..e74655d6c8 100644 --- a/packages/core/src/dag/core/types.ts +++ b/packages/core/src/dag/core/types.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG scheduling core — status enums, transition tables, and error types. * diff --git a/packages/core/src/dag/projector.ts b/packages/core/src/dag/projector.ts index 49b901b58d..ba9d76097a 100644 --- a/packages/core/src/dag/projector.ts +++ b/packages/core/src/dag/projector.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as DagProjector from "./projector" import { and, eq, inArray, sql } from "drizzle-orm" diff --git a/packages/core/src/dag/sql.ts b/packages/core/src/dag/sql.ts index eb70ca4008..9e6b9d80d0 100644 --- a/packages/core/src/dag/sql.ts +++ b/packages/core/src/dag/sql.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + import { sqliteTable, text, integer, index, primaryKey, uniqueIndex } from "drizzle-orm/sqlite-core" import { ProjectTable } from "../project/sql" import { SessionTable } from "../session/sql" diff --git a/packages/core/src/dag/store.ts b/packages/core/src/dag/store.ts index 711e3a3f19..52cb17e338 100644 --- a/packages/core/src/dag/store.ts +++ b/packages/core/src/dag/store.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as DagStore from "./store" import { and, asc, count, desc, eq, gt, inArray, or } from "drizzle-orm" diff --git a/packages/core/src/ripgrep/binary.ts b/packages/core/src/ripgrep/binary.ts index 9ac5d42e87..8e5b94f889 100644 --- a/packages/core/src/ripgrep/binary.ts +++ b/packages/core/src/ripgrep/binary.ts @@ -1,27 +1,9 @@ -import path from "path" -import { Context, Effect, Layer, Stream } from "effect" -import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" -import { ChildProcess } from "effect/unstable/process" -import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import { CrossSpawnSpawner } from "../cross-spawn-spawner" +import { Context, Effect, Layer } from "effect" import { LayerNode } from "../effect/layer-node" -import { httpClient } from "../effect/layer-node-platform" -import { FSUtil } from "../fs-util" -import { Global } from "../global" -import { which } from "../util/which" +import { RuntimeAsset } from "../runtime-asset" +import { RipgrepAsset } from "../runtime-asset/catalog/ripgrep" export namespace RipgrepBinary { - const VERSION = "15.1.0" - const PLATFORM = { - "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, - "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, - "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" }, - "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" }, - "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" }, - "ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" }, - "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }, - } as const - interface Interface { readonly filepath: Effect.Effect } @@ -31,110 +13,25 @@ export namespace RipgrepBinary { export const layer = Layer.effect( Service, Effect.gen(function* () { - const fs = yield* FSUtil.Service - const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient) - const spawner = yield* ChildProcessSpawner - - const run = Effect.fnUntraced(function* (command: string, args: string[]) { - const handle = yield* spawner.spawn(ChildProcess.make(command, args, { extendEnv: true, stdin: "ignore" })) - const [stdout, stderr, code] = yield* Effect.all( - [ - Stream.mkString(Stream.decodeText(handle.stdout)), - Stream.mkString(Stream.decodeText(handle.stderr)), - handle.exitCode, - ], - { concurrency: "unbounded" }, - ) - return { stdout, stderr, code } - }, Effect.scoped) - - const extract = Effect.fnUntraced(function* ( - archive: string, - config: (typeof PLATFORM)[keyof typeof PLATFORM], - target: string, - ) { - const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" }) - - if (config.extension === "zip") { - // Windows only (all win32 platforms in the PLATFORM table use zip). - // Resolve bsdtar by absolute path instead of relying on PATH: on - // GitHub Windows runners, Git for Windows' GNU tar - // (C:\Program Files\Git\usr\bin\tar.exe) shadows System32\tar.exe - // (bsdtar) in PATH, and GNU tar treats `C:\Users\...` as an SSH-style - // `host:path`, producing "Cannot connect to C: resolve failed". - // System32 bsdtar natively understands Windows drive paths. - // SystemRoot is virtually always set on Windows; fall back to the - // literal name only if it is somehow absent (cross-spawn will then - // do PATH lookup, which still works on stock Windows without Git). - const systemRoot = process.env.SystemRoot - const tarExe = systemRoot ? path.join(systemRoot, "System32", "tar.exe") : "tar" - const result = yield* run(tarExe, ["-xf", archive, "-C", dir]) - if (result.code !== 0) - throw new Error( - result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`, - ) - } - - if (config.extension === "tar.gz") { - const result = yield* run("tar", ["-xzf", archive, "-C", dir]) - if (result.code !== 0) - throw new Error( - result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`, - ) - } - - const extracted = path.join( - dir, - `ripgrep-${VERSION}-${config.platform}`, - process.platform === "win32" ? "rg.exe" : "rg", - ) - if (!(yield* fs.isFile(extracted))) throw new Error(`ripgrep archive did not contain executable: ${extracted}`) - - yield* fs.copyFile(extracted, target) - if (process.platform !== "win32") yield* fs.chmod(target, 0o755) - }, Effect.scoped) + const runtime = yield* RuntimeAsset.Service return Service.of({ filepath: yield* Effect.cached( - Effect.gen(function* () { - const system = yield* Effect.sync(() => which(process.platform === "win32" ? "rg.exe" : "rg")) - if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system - - const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) - if (yield* fs.isFile(target).pipe(Effect.orDie)) return target - - const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM - const config = PLATFORM[platformKey] - if (!config) throw new Error(`unsupported platform for ripgrep: ${platformKey}`) - - const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}` - const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}` - const archive = path.join(Global.Path.bin, filename) - - yield* Effect.logInfo("downloading ripgrep", { url }) - yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie) - const bytes = yield* HttpClientRequest.get(url).pipe( - http.execute, - Effect.flatMap((response) => response.arrayBuffer), - Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))), - ) - if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`) - - yield* fs.writeWithDirs(archive, new Uint8Array(bytes)) - yield* extract(archive, config, target) - yield* fs.remove(archive, { force: true }).pipe(Effect.ignore) - return target - }), + runtime + .resolve(RipgrepAsset.descriptor) + .pipe( + Effect.flatMap((result) => + result._tag === "Available" + ? Effect.succeed(result.path) + : Effect.fail(new Error(`ripgrep is unavailable: ${result.reason}`)), + ), + ), ), }) }), ) - export const defaultLayer = layer.pipe( - Layer.provide(FetchHttpClient.layer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(CrossSpawnSpawner.defaultLayer), - ) + export const defaultLayer = layer.pipe(Layer.provide(RuntimeAsset.defaultLayer)) - export const node = LayerNode.make(layer, [FSUtil.node, httpClient, CrossSpawnSpawner.node]) + export const node = LayerNode.make(layer, [RuntimeAsset.node]) } diff --git a/packages/core/src/runtime-asset/LICENSE b/packages/core/src/runtime-asset/LICENSE new file mode 100644 index 0000000000..0c97efd25b --- /dev/null +++ b/packages/core/src/runtime-asset/LICENSE @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/packages/core/src/runtime-asset/MODIFICATIONS.md b/packages/core/src/runtime-asset/MODIFICATIONS.md new file mode 100644 index 0000000000..a208447dbc --- /dev/null +++ b/packages/core/src/runtime-asset/MODIFICATIONS.md @@ -0,0 +1,32 @@ + + + +# RuntimeAsset modifications + +This module is self-developed for the OpenCode-GraphAgent fork and is licensed +under AGPL-3.0-or-later. Its full license text is in `LICENSE`. + +Every TypeScript source file in this directory starts with the two lines from +`SPDX-HEADER.txt`. Code copied or adapted from an upstream MIT file must retain +the upstream copyright and MIT permission notice; it must not be relabeled as +solely AGPL without a provenance review. + +Substantial changes are traceable through Git history and release metadata. +Each release source archive records the exact commit and build inputs. When a +change cannot be understood from its commit and path history, add a dated entry +below with the affected paths and a factual summary. + +## Entries + +- 2026-08-09: Reserved the module boundary for portable runtime asset + resolution, integrity verification, caching, and source selection. +- 2026-08-09: Added the public descriptor, policy, provenance, typed failure, + candidate resolution, Effect service, self-contained default layer, and + LayerNode interfaces. +- 2026-08-09: Added managed system/package/cache/mirror/public candidates, + pinned download verification, archive extraction, immutable cache metadata, + atomic publication, and process-local concurrent download deduplication. +- 2026-08-09: Added the pinned ripgrep 15.1.0 seven-platform catalog and + migrated the legacy binary service to the RuntimeAsset interface. +- 2026-08-09: Added desktop prebuild preparation, package verification, + electron-builder resource embedding, and packaged sidecar asset discovery. diff --git a/packages/core/src/runtime-asset/SPDX-HEADER.txt b/packages/core/src/runtime-asset/SPDX-HEADER.txt new file mode 100644 index 0000000000..a9cef60e15 --- /dev/null +++ b/packages/core/src/runtime-asset/SPDX-HEADER.txt @@ -0,0 +1,2 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/packages/core/src/runtime-asset/catalog/ripgrep.ts b/packages/core/src/runtime-asset/catalog/ripgrep.ts new file mode 100644 index 0000000000..a1f563759d --- /dev/null +++ b/packages/core/src/runtime-asset/catalog/ripgrep.ts @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { RuntimeAsset } from "../index" + +export namespace RipgrepAsset { + export const version = "15.1.0" + const release = `https://github.com/BurntSushi/ripgrep/releases/download/${version}` + + const target = (input: { + os: NodeJS.Platform + arch: string + platform: string + archive: "tar.gz" | "zip" + sha256: string + }): RuntimeAsset.Target => { + const executable = input.os === "win32" ? "rg.exe" : "rg" + const artifact = `ripgrep-${version}-${input.platform}.${input.archive}` + return { + os: input.os, + arch: input.arch, + executable, + artifact, + archive: input.archive, + entry: `ripgrep-${version}-${input.platform}/${executable}`, + sha256: input.sha256, + public: `${release}/${artifact}`, + } + } + + // Digests are the SHA-256 values published on the official GitHub 15.1.0 + // release assets. Keep the version, filename, entry, and digest together. + export const descriptor: RuntimeAsset.Descriptor = { + id: "ripgrep", + version, + required: true, + targets: [ + target({ + os: "darwin", + arch: "arm64", + platform: "aarch64-apple-darwin", + archive: "tar.gz", + sha256: "378e973289176ca0c6054054ee7f631a065874a352bf43f0fa60ef079b6ba715", + }), + target({ + os: "darwin", + arch: "x64", + platform: "x86_64-apple-darwin", + archive: "tar.gz", + sha256: "64811cb24e77cac3057d6c40b63ac9becf9082eedd54ca411b475b755d334882", + }), + target({ + os: "linux", + arch: "arm64", + platform: "aarch64-unknown-linux-gnu", + archive: "tar.gz", + sha256: "2b661c6ef508e902f388e9098d9c4c5aca72c87b55922d94abdba830b4dc885e", + }), + target({ + os: "linux", + arch: "x64", + platform: "x86_64-unknown-linux-musl", + archive: "tar.gz", + sha256: "1c9297be4a084eea7ecaedf93eb03d058d6faae29bbc57ecdaf5063921491599", + }), + target({ + os: "win32", + arch: "arm64", + platform: "aarch64-pc-windows-msvc", + archive: "zip", + sha256: "00d931fb5237c9696ca49308818edb76d8eb6fc132761cb2a1bd616b2df02f8e", + }), + target({ + os: "win32", + arch: "ia32", + platform: "i686-pc-windows-msvc", + archive: "zip", + sha256: "725be85a1e8f92878a548f40ee4f6df64bc93b809586462b3c6d884e1de1e83a", + }), + target({ + os: "win32", + arch: "x64", + platform: "x86_64-pc-windows-msvc", + archive: "zip", + sha256: "124510b94b6baa3380d051fdf4650eaa80a302c876d611e9dba0b2e18d87493a", + }), + ], + } +} diff --git a/packages/core/src/runtime-asset/index.ts b/packages/core/src/runtime-asset/index.ts new file mode 100644 index 0000000000..0a5b330d14 --- /dev/null +++ b/packages/core/src/runtime-asset/index.ts @@ -0,0 +1,456 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { existsSync } from "fs" +import { chmod, copyFile, mkdir, mkdtemp, rename, rm } from "fs/promises" +import path from "path" +import { Context, Effect, Layer } from "effect" +import { LayerNode } from "../effect/layer-node" +import { Global } from "../global" +import { which } from "../util/which" + +export namespace RuntimeAsset { + export const sources = ["system", "packaged", "cache", "mirror", "public"] as const + + export type Source = (typeof sources)[number] + export type Platform = { + readonly os: NodeJS.Platform + readonly arch: string + } + export type Target = Platform & { + readonly executable: string + readonly artifact?: string + readonly entry?: string + readonly sha256?: string + readonly executableSha256?: string + readonly archive?: "raw" | "tar.gz" | "zip" + readonly mirror?: string + readonly public?: string + } + export type Descriptor = { + readonly id: string + readonly version: string + readonly required: boolean + readonly targets: readonly Target[] + } + export type Policy = { + readonly sources?: readonly Source[] + } + export type Attempt = { + readonly source: Source + readonly reason: string + } + export type Available = { + readonly _tag: "Available" + readonly id: string + readonly version: string + readonly path: string + readonly source: Source + readonly platform: Platform + readonly sha256?: string + } + export type Unavailable = { + readonly _tag: "Unavailable" + readonly id: string + readonly version: string + readonly required: false + readonly platform: Platform + readonly reason: "unsupported-platform" | "sources-exhausted" + readonly attempts: readonly Attempt[] + } + export type Resolution = Available | Unavailable + export type CandidateRequest = { + readonly descriptor: Descriptor + readonly target: Target + readonly source: Source + } + export type CandidateResult = { + readonly path: string + } + export type Candidate = (request: CandidateRequest) => Effect.Effect + export type Candidates = Record + export type Fetch = (url: string, init: RequestInit) => Promise + export type ManagedInput = { + readonly platform: Platform + readonly cacheDirectory: string + readonly packagedDirectory?: string + readonly mirrorBaseURL?: string + readonly timeoutMs?: number + readonly fetch?: Fetch + } + export type Interface = { + readonly resolve: (descriptor: Descriptor, policy?: Policy) => Effect.Effect + } + + export class CandidateUnavailable extends Error { + readonly _tag = "CandidateUnavailable" + readonly reason: string + + constructor(input: { readonly reason: string }) { + super(input.reason) + this.reason = input.reason + } + } + + export class RequiredAssetUnavailable extends Error { + readonly _tag = "RequiredAssetUnavailable" + readonly id: string + readonly version: string + readonly platform: Platform + readonly reason: "unsupported-platform" | "sources-exhausted" + readonly attempts: readonly Attempt[] + + constructor(input: { + readonly id: string + readonly version: string + readonly platform: Platform + readonly reason: "unsupported-platform" | "sources-exhausted" + readonly attempts: readonly Attempt[] + }) { + super(`Required runtime asset is unavailable: ${input.id}@${input.version} (${input.reason})`) + this.id = input.id + this.version = input.version + this.platform = input.platform + this.reason = input.reason + this.attempts = input.attempts + } + } + + export class Service extends Context.Service()("@opencode/RuntimeAsset") {} + + export function make(input: { readonly platform: Platform; readonly candidates: Candidates }): Interface { + const finish = ( + descriptor: Descriptor, + reason: "unsupported-platform" | "sources-exhausted", + attempts: readonly Attempt[], + ): Effect.Effect => { + if (descriptor.required) { + return Effect.fail( + new RequiredAssetUnavailable({ + id: descriptor.id, + version: descriptor.version, + platform: input.platform, + reason, + attempts, + }), + ) + } + return Effect.succeed({ + _tag: "Unavailable", + id: descriptor.id, + version: descriptor.version, + required: false, + platform: input.platform, + reason, + attempts, + }) + } + + const resolve = (descriptor: Descriptor, policy?: Policy) => { + const target = descriptor.targets.find( + (candidate) => candidate.os === input.platform.os && candidate.arch === input.platform.arch, + ) + if (!target) return finish(descriptor, "unsupported-platform", []) + + const attempt = ( + pending: readonly Source[], + attempts: readonly Attempt[], + ): Effect.Effect => { + const source = pending[0] + if (!source) return finish(descriptor, "sources-exhausted", attempts) + return input.candidates[source]({ descriptor, target, source }).pipe( + Effect.map( + (result): Available => ({ + _tag: "Available", + id: descriptor.id, + version: descriptor.version, + path: result.path, + source, + platform: input.platform, + ...(target.sha256 ? { sha256: target.sha256 } : {}), + }), + ), + Effect.catchTag("CandidateUnavailable", (error) => + Effect.suspend(() => attempt(pending.slice(1), [...attempts, { source, reason: redact(error.reason) }])), + ), + ) + } + + return attempt(policy?.sources ?? sources, []) + } + + return { resolve } + } + + export function managed(input: ManagedInput) { + return make({ platform: input.platform, candidates: managedCandidates(input) }) + } + + export const layer = (input: { readonly platform: Platform; readonly candidates: Candidates }) => + Layer.succeed(Service, Service.of(make(input))) + + export const defaultLayer = Layer.succeed( + Service, + Service.of( + managed({ + platform: { os: process.platform, arch: process.arch }, + cacheDirectory: process.env.OPENCODE_RUNTIME_ASSET_CACHE ?? path.join(Global.Path.bin, "runtime-assets"), + packagedDirectory: process.env.OPENCODE_RUNTIME_ASSETS_DIR, + mirrorBaseURL: process.env.OPENCODE_RUNTIME_ASSET_MIRROR, + }), + ), + ) + + export const node = LayerNode.make(defaultLayer, []) + + function redact(reason: string) { + return reason.replace(/https?:\/\/[^\s)\]}]+/g, (value) => { + if (!URL.canParse(value)) return value + const url = new URL(value) + url.username = "" + url.password = "" + Array.from(url.searchParams.keys()) + .filter((key) => + /^(?:api[_-]?key|access[_-]?token|authorization|password|secret|sig(?:nature)?|token)$/i.test(key), + ) + .forEach((key) => url.searchParams.set(key, "REDACTED")) + return url.toString() + }) + } +} + +function managedCandidates(input: RuntimeAsset.ManagedInput): RuntimeAsset.Candidates { + const inFlight = new Map>() + const local = + (root: string | undefined, source: "packaged" | "cache"): RuntimeAsset.Candidate => + (request) => + Effect.tryPromise({ + try: () => { + if (!root) throw new Error(`${source} runtime asset directory is not configured`) + return source === "cache" ? verifiedCache(root, request) : verifiedPackaged(root, request) + }, + catch: unavailable, + }) + const network = + (source: "mirror" | "public"): RuntimeAsset.Candidate => + (request) => + Effect.tryPromise({ + try: () => { + const key = assetDirectory(input.cacheDirectory, request) + const active = inFlight.get(key) + if (active) return active + const pending = download(input, request, sourceURL(input, request, source)).finally(() => + inFlight.delete(key), + ) + inFlight.set(key, pending) + return pending + }, + catch: unavailable, + }) + + return { + system: (request) => + Effect.tryPromise({ + try: async () => { + const found = which(request.target.executable) + if (!found || !(await Bun.file(found).exists())) { + throw new Error(`system executable is unavailable: ${request.target.executable}`) + } + return { path: found } + }, + catch: unavailable, + }), + packaged: local(input.packagedDirectory, "packaged"), + cache: local(input.cacheDirectory, "cache"), + mirror: network("mirror"), + public: network("public"), + } +} + +async function verifiedPackaged(root: string, request: RuntimeAsset.CandidateRequest) { + const executable = assetPath(root, request) + if (!(await Bun.file(executable).exists())) throw new Error(`packaged executable is unavailable: ${executable}`) + if (request.target.executableSha256 && (await digestFile(executable)) !== request.target.executableSha256) { + throw new Error(`packaged executable digest mismatch: ${executable}`) + } + return { path: executable } +} + +async function verifiedCache(root: string, request: RuntimeAsset.CandidateRequest) { + const executable = assetPath(root, request) + const metadataFile = path.join(path.dirname(executable), "metadata.json") + if (!(await Bun.file(executable).exists()) || !(await Bun.file(metadataFile).exists())) { + throw new Error(`verified cache entry is unavailable: ${executable}`) + } + const value: unknown = await Bun.file(metadataFile).json() + if (!isRecord(value) || value.schemaVersion !== 1) throw new Error(`cache metadata is invalid: ${metadataFile}`) + if (request.target.sha256 && value.archiveSha256 !== request.target.sha256) { + throw new Error(`cache archive digest mismatch: ${executable}`) + } + if (typeof value.executableSha256 !== "string" || (await digestFile(executable)) !== value.executableSha256) { + throw new Error(`cache executable digest mismatch: ${executable}`) + } + return { path: executable } +} + +async function download( + input: RuntimeAsset.ManagedInput, + request: RuntimeAsset.CandidateRequest, + url: string, +): Promise { + if (!request.target.sha256 || !/^[a-f0-9]{64}$/i.test(request.target.sha256)) { + throw new Error(`network runtime asset requires a SHA-256 digest: ${request.descriptor.id}`) + } + const archiveSha256 = request.target.sha256.toLowerCase() + await mkdir(input.cacheDirectory, { recursive: true }) + const response = await (input.fetch ?? globalThis.fetch)(url, { + signal: AbortSignal.timeout(input.timeoutMs ?? 30_000), + }) + if (!response.ok) throw new Error(`download failed (${response.status}): ${url}`) + const bytes = new Uint8Array(await response.arrayBuffer()) + if (!bytes.byteLength) throw new Error(`download returned an empty asset: ${url}`) + if (digestBytes(bytes) !== archiveSha256) { + throw new Error(`download digest mismatch: ${url}`) + } + + const destination = assetDirectory(input.cacheDirectory, request) + await mkdir(path.dirname(destination), { recursive: true }) + const temporary = await mkdtemp(path.join(path.dirname(destination), `.${path.basename(destination)}-`)) + return (async () => { + const executable = path.join(temporary, executableName(request.target.executable)) + await materialize(bytes, request.target, temporary, executable) + if (process.platform !== "win32") await chmod(executable, 0o755) + const executableSha256 = await digestFile(executable) + if (request.target.executableSha256 && request.target.executableSha256 !== executableSha256) { + throw new Error(`downloaded executable digest mismatch: ${url}`) + } + await Bun.write( + path.join(temporary, "metadata.json"), + JSON.stringify( + { + schemaVersion: 1, + id: request.descriptor.id, + version: request.descriptor.version, + platform: request.target.os, + arch: request.target.arch, + archiveSha256, + executableSha256, + }, + null, + 2, + ) + "\n", + ) + await rm(destination, { recursive: true, force: true }) + await rename(temporary, destination) + return { path: path.join(destination, executableName(request.target.executable)) } + })().finally(() => rm(temporary, { recursive: true, force: true })) +} + +async function materialize(bytes: Uint8Array, target: RuntimeAsset.Target, temporary: string, executable: string) { + if ((target.archive ?? "raw") === "raw") { + await Bun.write(executable, bytes) + return + } + if (!target.entry) throw new Error(`archive entry is required for ${target.artifact ?? target.executable}`) + const entry = safeRelative(target.entry, "archive entry") + const archive = path.join(temporary, artifactName(target)) + const extracted = path.join(temporary, "extracted") + await mkdir(extracted, { recursive: true }) + await Bun.write(archive, bytes) + await command( + process.platform === "win32" && process.env.SystemRoot + ? path.join(process.env.SystemRoot, "System32", "tar.exe") + : "tar", + [target.archive === "tar.gz" ? "-xzf" : "-xf", archive, "-C", extracted], + ) + const source = path.resolve(extracted, entry) + if (source !== extracted && !source.startsWith(`${extracted}${path.sep}`)) + throw new Error(`archive entry escapes root: ${entry}`) + if (!(await Bun.file(source).exists())) throw new Error(`archive entry is missing: ${entry}`) + await copyFile(source, executable) +} + +function sourceURL( + input: RuntimeAsset.ManagedInput, + request: RuntimeAsset.CandidateRequest, + source: "mirror" | "public", +) { + const direct = request.target[source] + if (direct) return direct + if (source === "public" || !input.mirrorBaseURL) throw new Error(`${source} URL is not configured`) + if (!URL.canParse(input.mirrorBaseURL)) throw new Error(`runtime asset mirror URL is invalid: ${input.mirrorBaseURL}`) + const url = new URL(input.mirrorBaseURL) + url.pathname = [ + url.pathname.replace(/\/$/, ""), + request.descriptor.id, + request.descriptor.version, + `${request.target.os}-${request.target.arch}`, + artifactName(request.target), + ] + .map((segment) => segment.split("/").map(encodeURIComponent).join("/")) + .join("/") + return url.toString() +} + +function assetPath(root: string, request: RuntimeAsset.CandidateRequest) { + return path.join(assetDirectory(root, request), executableName(request.target.executable)) +} + +function assetDirectory(root: string, request: RuntimeAsset.CandidateRequest) { + const id = safeSegment(request.descriptor.id, "asset id") + const version = safeSegment(request.descriptor.version, "asset version") + const platform = safeSegment(`${request.target.os}-${request.target.arch}`, "asset platform") + const digest = request.target.sha256?.slice(0, 16).toLowerCase() ?? "unverified" + return path.join(root, id, version, `${platform}-${digest}`) +} + +function artifactName(target: RuntimeAsset.Target) { + return safeSegment(target.artifact ?? target.executable, "artifact filename") +} + +function executableName(executable: string) { + return safeSegment(executable, "executable filename") +} + +function safeSegment(value: string, name: string) { + if (!value || value === "." || value === ".." || path.basename(value) !== value) { + throw new Error(`${name} must be one path segment: ${value}`) + } + return value +} + +function safeRelative(value: string, name: string) { + const normalized = path.normalize(value) + if (path.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path.sep}`)) { + throw new Error(`${name} must stay inside the archive: ${value}`) + } + return normalized +} + +async function command(executable: string, args: string[]) { + const child = Bun.spawn([executable, ...args], { stdout: "pipe", stderr: "pipe" }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + if (exitCode !== 0) { + throw new Error(`${executable} ${args.join(" ")} failed (${exitCode}): ${stderr.trim() || stdout.trim()}`) + } +} + +function unavailable(error: unknown) { + return new RuntimeAsset.CandidateUnavailable({ reason: error instanceof Error ? error.message : String(error) }) +} + +async function digestFile(file: string) { + return digestBytes(new Uint8Array(await Bun.file(file).arrayBuffer())) +} + +function digestBytes(bytes: Uint8Array) { + return new Bun.CryptoHasher("sha256").update(bytes).digest("hex") +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} diff --git a/packages/core/test/corresponding-source.test.ts b/packages/core/test/corresponding-source.test.ts new file mode 100644 index 0000000000..08637b208b --- /dev/null +++ b/packages/core/test/corresponding-source.test.ts @@ -0,0 +1,77 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { mkdtemp, rm, unlink } from "fs/promises" +import os from "os" +import path from "path" +import { createCorrespondingSource, verifyCorrespondingSourceArtifacts } from "../../../script/corresponding-source" + +const roots: string[] = [] + +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) + +describe("corresponding source", () => { + test("archives the exact committed tree with deterministic source metadata", async () => { + const root = await repository() + const result = await createCorrespondingSource({ + root, + output: path.join(root, "artifacts"), + version: "1.2.3-dev.4", + requiredFiles: ["LICENSE", "NOTICE", "bun.lock", "package.json"], + }) + const manifest = await Bun.file(result.manifest).json() + const entries = (await command(root, ["tar", "-tzf", result.archive])).split("\n") + + expect(result.sha256).toHaveLength(64) + expect(await Bun.file(result.checksum).text()).toBe(`${result.sha256} ${path.basename(result.archive)}\n`) + expect(manifest.commit).toBe(result.commit) + expect(manifest.archive.sha256).toBe(result.sha256) + expect(manifest.dependencyLocks).toEqual(["bun.lock"]) + expect(entries).toContain(`opencode-graphagent-1.2.3-dev.4-source-${result.commit.slice(0, 12)}/LICENSE`) + expect((await verifyCorrespondingSourceArtifacts(path.dirname(result.archive))).sha256).toBe(result.sha256) + }) + + test("rejects a release set with a missing source archive", async () => { + const root = await repository() + const result = await createCorrespondingSource({ + root, + output: path.join(root, "artifacts"), + version: "1.2.3", + requiredFiles: ["LICENSE", "NOTICE", "bun.lock", "package.json"], + }) + await unlink(result.archive) + + await expect(verifyCorrespondingSourceArtifacts(path.dirname(result.archive))).rejects.toThrow( + "Corresponding source archive is missing", + ) + }) +}) + +async function repository() { + const root = await mkdtemp(path.join(os.tmpdir(), "corresponding-source-")) + roots.push(root) + await Promise.all( + ["LICENSE", "NOTICE", "bun.lock", "package.json"].map((file) => Bun.write(path.join(root, file), `${file}\n`)), + ) + await git(root, ["init"]) + await git(root, ["config", "user.email", "test@example.com"]) + await git(root, ["config", "user.name", "Test User"]) + await git(root, ["add", "."]) + await git(root, ["commit", "-m", "test: source fixture"]) + return root +} + +async function git(root: string, args: string[]) { + return command(root, ["git", ...args]) +} + +async function command(root: string, args: string[]) { + const child = Bun.spawn(args, { cwd: root, stdout: "pipe", stderr: "pipe" }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + if (exitCode !== 0) throw new Error(`${args.join(" ")} failed: ${stderr}`) + return stdout.trim() +} diff --git a/packages/core/test/license-scope.test.ts b/packages/core/test/license-scope.test.ts new file mode 100644 index 0000000000..caf08044be --- /dev/null +++ b/packages/core/test/license-scope.test.ts @@ -0,0 +1,114 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { existsSync } from "fs" +import { mkdir, mkdtemp, rm } from "fs/promises" +import os from "os" +import path from "path" +import { z } from "zod" +import { checkLicenseScope } from "../../../script/check-license-scope" + +const root = path.join(import.meta.dir, "../../..") +const tempRoots: string[] = [] +const scopeSchema = z.object({ + id: z.string().min(1), + name: z.string().min(1), + status: z.enum(["active", "planned"]), + spdx: z.literal("AGPL-3.0-or-later"), + copyright: z.string().min(1), + licenseFiles: z.array(z.string().min(1)).min(1), + paths: z.array(z.string().min(1)).min(1), +}) +const manifestSchema = z.object({ + schemaVersion: z.literal(1), + defaultLicense: z.object({ + spdx: z.literal("MIT"), + licenseFile: z.string().min(1), + copyright: z.string().min(1), + }), + scopes: z.array(scopeSchema).min(1), +}) + +afterEach(async () => { + await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) + +describe("license scope manifest", () => { + test("uses MIT by default and AGPL only for explicit scopes", async () => { + const manifest = manifestSchema.parse(await Bun.file(path.join(root, "LICENSE-SCOPE.json")).json()) + + expect(existsSync(path.join(root, manifest.defaultLicense.licenseFile))).toBe(true) + expect(new Set(manifest.scopes.map((scope) => scope.id)).size).toBe(manifest.scopes.length) + expect(new Set(manifest.scopes.flatMap((scope) => scope.paths)).size).toBe( + manifest.scopes.flatMap((scope) => scope.paths).length, + ) + }) + + test("keeps NOTICE synchronized with every AGPL scope", async () => { + const manifest = manifestSchema.parse(await Bun.file(path.join(root, "LICENSE-SCOPE.json")).json()) + const notice = await Bun.file(path.join(root, "NOTICE")).text() + + for (const scope of manifest.scopes) { + expect(notice).toContain(scope.name) + expect(notice).toContain(scope.spdx) + expect(notice).toContain(scope.copyright) + for (const file of scope.licenseFiles) expect(existsSync(path.join(root, file))).toBe(true) + for (const file of scope.paths) expect(notice).toContain(file) + } + }) + + test("accepts the repository license boundary", async () => { + expect(await checkLicenseScope(root)).toEqual([]) + }) + + test("rejects an AGPL source without SPDX headers", async () => { + const fixture = await createFixture({ header: false, noticePath: true }) + expect((await checkLicenseScope(fixture)).map((issue) => issue.code)).toEqual([ + "missing-spdx-copyright", + "missing-spdx-license", + ]) + }) + + test("rejects an AGPL path missing from NOTICE", async () => { + const fixture = await createFixture({ header: true, noticePath: false }) + expect((await checkLicenseScope(fixture)).map((issue) => issue.code)).toEqual(["notice-missing-value"]) + }) +}) + +async function createFixture(input: { header: boolean; noticePath: boolean }) { + const root = await mkdtemp(path.join(os.tmpdir(), "license-scope-")) + tempRoots.push(root) + await mkdir(path.join(root, "src", "module"), { recursive: true }) + const copyright = "Copyright (c) 2026 LeXwDeX" + await Promise.all([ + Bun.write(path.join(root, "LICENSE"), "MIT\n"), + Bun.write(path.join(root, "AGPL.txt"), "AGPL-3.0-or-later\n"), + Bun.write( + path.join(root, "NOTICE"), + ["Fixture module", "AGPL-3.0-or-later", copyright, "AGPL.txt", input.noticePath ? "src/module/" : ""].join("\n"), + ), + Bun.write( + path.join(root, "LICENSE-SCOPE.json"), + JSON.stringify({ + schemaVersion: 1, + defaultLicense: { spdx: "MIT", licenseFile: "LICENSE", copyright: "Upstream" }, + scopes: [ + { + id: "fixture-module", + name: "Fixture module", + status: "active", + spdx: "AGPL-3.0-or-later", + copyright, + licenseFiles: ["AGPL.txt"], + paths: ["src/module/"], + }, + ], + }), + ), + Bun.write( + path.join(root, "src", "module", "index.ts"), + input.header + ? `// SPDX-FileCopyrightText: 2026 LeXwDeX\n// SPDX-License-Identifier: AGPL-3.0-or-later\n` + : "export const value = true\n", + ), + ]) + return root +} diff --git a/packages/core/test/ripgrep-binary.test.ts b/packages/core/test/ripgrep-binary.test.ts new file mode 100644 index 0000000000..ca95f7d95d --- /dev/null +++ b/packages/core/test/ripgrep-binary.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Layer } from "effect" +import { RipgrepBinary } from "@opencode-ai/core/ripgrep/binary" +import { RuntimeAsset } from "@opencode-ai/core/runtime-asset" +import { RipgrepAsset } from "@opencode-ai/core/runtime-asset/catalog/ripgrep" + +describe("RipgrepBinary", () => { + RuntimeAsset.sources.forEach((selected) => { + test(`falls back through RuntimeAsset candidates to ${selected}`, async () => { + const attempts: RuntimeAsset.Source[] = [] + const candidate = + (source: RuntimeAsset.Source): RuntimeAsset.Candidate => + () => { + attempts.push(source) + return source === selected + ? Effect.succeed({ path: `/${source}/rg` }) + : Effect.fail(new RuntimeAsset.CandidateUnavailable({ reason: `${source} unavailable` })) + } + const candidates: RuntimeAsset.Candidates = { + system: candidate("system"), + packaged: candidate("packaged"), + cache: candidate("cache"), + mirror: candidate("mirror"), + public: candidate("public"), + } + const layer = RipgrepBinary.layer.pipe( + Layer.provide( + RuntimeAsset.layer({ + platform: { os: "linux", arch: "x64" }, + candidates, + }), + ), + ) + const filepath = await Effect.runPromise( + Effect.gen(function* () { + return yield* (yield* RipgrepBinary.Service).filepath + }).pipe(Effect.provide(layer)), + ) + + expect(filepath).toBe(`/${selected}/rg`) + expect(attempts).toEqual(RuntimeAsset.sources.slice(0, RuntimeAsset.sources.indexOf(selected) + 1)) + }) + }) + + test("pins official archive metadata for every supported target", () => { + expect(RipgrepAsset.descriptor).toMatchObject({ id: "ripgrep", version: "15.1.0", required: true }) + expect(RipgrepAsset.descriptor.targets).toHaveLength(7) + expect( + RipgrepAsset.descriptor.targets.every( + (target) => + target.public?.includes(`/15.1.0/${target.artifact}`) && + target.entry?.endsWith(`/${target.executable}`) && + /^[a-f0-9]{64}$/.test(target.sha256 ?? ""), + ), + ).toBe(true) + }) +}) diff --git a/packages/core/test/runtime-asset-cache.test.ts b/packages/core/test/runtime-asset-cache.test.ts new file mode 100644 index 0000000000..7c9888ecb7 --- /dev/null +++ b/packages/core/test/runtime-asset-cache.test.ts @@ -0,0 +1,168 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { mkdir, mkdtemp, rm } from "fs/promises" +import os from "os" +import path from "path" +import { Effect } from "effect" +import { RuntimeAsset } from "@opencode-ai/core/runtime-asset" + +const cleanups: Array<() => void | Promise> = [] + +afterEach(async () => { + await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())) +}) + +describe("RuntimeAsset managed candidates", () => { + test("rejects a download with the wrong SHA-256 before publishing cache files", async () => { + const root = await temporaryRoot() + const cache = path.join(root, "cache") + const runtime = RuntimeAsset.managed({ + platform: currentPlatform(), + cacheDirectory: cache, + fetch: () => Promise.resolve(new Response("corrupt")), + }) + + const result = await Effect.runPromise( + runtime.resolve(rawDescriptor("https://assets.example/tool", "0".repeat(64)), { sources: ["public"] }), + ) + + expect(result._tag).toBe("Unavailable") + expect(await Array.fromAsync(new Bun.Glob("**/tool").scan({ cwd: cache, onlyFiles: true }))).toEqual([]) + }) + + test("verifies, extracts, atomically caches, and reuses a tar.gz asset", async () => { + const root = await temporaryRoot() + const archive = await tarFixture(root) + const bytes = await Bun.file(archive).bytes() + const requests = { count: 0 } + const runtime = RuntimeAsset.managed({ + platform: currentPlatform(), + cacheDirectory: path.join(root, "cache"), + fetch: () => { + requests.count++ + return Promise.resolve(new Response(bytes)) + }, + }) + const asset = archiveDescriptor("https://assets.example/fixture.tar.gz", sha256(bytes)) + + const downloaded = await Effect.runPromise(runtime.resolve(asset, { sources: ["public"] })) + const cached = await Effect.runPromise(runtime.resolve(asset, { sources: ["cache"] })) + + expect(downloaded).toMatchObject({ _tag: "Available", source: "public" }) + expect(cached).toMatchObject({ _tag: "Available", source: "cache" }) + expect(downloaded._tag === "Available" ? await Bun.file(downloaded.path).text() : "").toBe("fixture executable\n") + expect(cached._tag === "Available" ? cached.path : "").toBe(downloaded._tag === "Available" ? downloaded.path : "") + expect(requests.count).toBe(1) + }) + + test("deduplicates concurrent downloads and never exposes a partial executable", async () => { + const root = await temporaryRoot() + const cache = path.join(root, "cache") + const requested = Promise.withResolvers() + const release = Promise.withResolvers() + const requests = { count: 0 } + const bytes = new TextEncoder().encode("executable") + const runtime = RuntimeAsset.managed({ + platform: currentPlatform(), + cacheDirectory: cache, + fetch: async () => { + requests.count++ + requested.resolve() + await release.promise + return new Response(bytes) + }, + }) + const asset = rawDescriptor("https://assets.example/tool", sha256(bytes)) + + const first = Effect.runPromise(runtime.resolve(asset, { sources: ["public"] })) + const second = Effect.runPromise(runtime.resolve(asset, { sources: ["public"] })) + await requested.promise + expect(await Array.fromAsync(new Bun.Glob("**/tool").scan({ cwd: cache, onlyFiles: true }))).toEqual([]) + release.resolve() + const results = await Promise.all([first, second]) + + expect(results.every((result) => result._tag === "Available")).toBe(true) + expect(requests.count).toBe(1) + expect(results[0]).toEqual(results[1]) + }) + + test("rejects a cache entry whose executable digest no longer matches metadata", async () => { + const root = await temporaryRoot() + const bytes = new TextEncoder().encode("original") + const runtime = RuntimeAsset.managed({ + platform: currentPlatform(), + cacheDirectory: path.join(root, "cache"), + fetch: () => Promise.resolve(new Response(bytes)), + }) + const asset = rawDescriptor("https://assets.example/tool", sha256(bytes)) + const downloaded = await Effect.runPromise(runtime.resolve(asset, { sources: ["public"] })) + if (downloaded._tag !== "Available") throw new Error("fixture download failed") + await Bun.write(downloaded.path, "tampered") + + const result = await Effect.runPromise(runtime.resolve(asset, { sources: ["cache"] })) + + expect(result._tag).toBe("Unavailable") + expect(result._tag === "Unavailable" ? result.attempts[0]?.reason : "").toContain("digest") + }) +}) + +function currentPlatform(): RuntimeAsset.Platform { + return { os: process.platform, arch: process.arch } +} + +function rawDescriptor(url: string, digest: string): RuntimeAsset.Descriptor { + return { + id: "fixture-raw", + version: "1.0.0", + required: false, + targets: [ + { + ...currentPlatform(), + executable: "tool", + artifact: "tool", + archive: "raw", + public: url, + sha256: digest, + }, + ], + } +} + +function archiveDescriptor(url: string, digest: string): RuntimeAsset.Descriptor { + return { + id: "fixture-archive", + version: "1.0.0", + required: false, + targets: [ + { + ...currentPlatform(), + executable: "tool", + artifact: "fixture.tar.gz", + archive: "tar.gz", + entry: "bin/tool", + public: url, + sha256: digest, + }, + ], + } +} + +async function temporaryRoot() { + const root = await mkdtemp(path.join(os.tmpdir(), "runtime-asset-")) + cleanups.push(() => rm(root, { recursive: true, force: true })) + return root +} + +async function tarFixture(root: string) { + const source = path.join(root, "source") + const archive = path.join(root, "fixture.tar.gz") + await mkdir(path.join(source, "bin"), { recursive: true }) + await Bun.write(path.join(source, "bin", "tool"), "fixture executable\n") + const child = Bun.spawn(["tar", "-czf", archive, "-C", source, "."], { stdout: "pipe", stderr: "pipe" }) + const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]) + if (exitCode !== 0) throw new Error(`tar fixture failed: ${stderr}`) + return archive +} + +function sha256(bytes: Uint8Array) { + return new Bun.CryptoHasher("sha256").update(bytes).digest("hex") +} diff --git a/packages/core/test/runtime-asset.test.ts b/packages/core/test/runtime-asset.test.ts new file mode 100644 index 0000000000..4322d68389 --- /dev/null +++ b/packages/core/test/runtime-asset.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Result } from "effect" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { RuntimeAsset } from "@opencode-ai/core/runtime-asset" + +describe("RuntimeAsset", () => { + test("uses the deterministic source order and reports provenance", async () => { + const attempts: RuntimeAsset.Source[] = [] + const runtime = RuntimeAsset.make({ + platform: { os: "linux", arch: "x64" }, + candidates: candidates({ + system: () => { + attempts.push("system") + return Effect.fail(new RuntimeAsset.CandidateUnavailable({ reason: "not installed" })) + }, + packaged: () => { + attempts.push("packaged") + return Effect.succeed({ path: "/app/assets/rg" }) + }, + cache: () => { + attempts.push("cache") + return Effect.succeed({ path: "/cache/rg" }) + }, + }), + }) + + const result = await Effect.runPromise(runtime.resolve(descriptor(false))) + + expect(result).toMatchObject({ + _tag: "Available", + path: "/app/assets/rg", + source: "packaged", + id: "ripgrep", + version: "15.1.0", + platform: { os: "linux", arch: "x64" }, + }) + expect(attempts).toEqual(["system", "packaged"]) + }) + + test("selects only the target matching the current platform", async () => { + const runtime = RuntimeAsset.make({ + platform: { os: "linux", arch: "x64" }, + candidates: candidates({ + system: (request) => Effect.succeed({ path: `/system/${request.target.executable}` }), + }), + }) + + const result = await Effect.runPromise(runtime.resolve(descriptor(false))) + + expect(result).toMatchObject({ _tag: "Available", path: "/system/rg", sha256: "linux-digest" }) + }) + + test("does not call mirror or public adapters when network sources are disabled", async () => { + const attempts: RuntimeAsset.Source[] = [] + const candidate = + (source: RuntimeAsset.Source): RuntimeAsset.Candidate => + () => { + attempts.push(source) + return source === "mirror" || source === "public" + ? Effect.succeed({ path: `/network/${source}/rg` }) + : Effect.fail(new RuntimeAsset.CandidateUnavailable({ reason: `${source} unavailable` })) + } + const runtime = RuntimeAsset.make({ + platform: { os: "linux", arch: "x64" }, + candidates: candidates({ + system: candidate("system"), + packaged: candidate("packaged"), + cache: candidate("cache"), + mirror: candidate("mirror"), + public: candidate("public"), + }), + }) + + const result = await Effect.runPromise( + runtime.resolve(descriptor(false), { sources: ["system", "packaged", "cache"] }), + ) + + expect(result._tag).toBe("Unavailable") + expect(attempts).toEqual(["system", "packaged", "cache"]) + }) + + test("returns typed unavailability for optional assets and fails required assets", async () => { + const runtime = RuntimeAsset.make({ + platform: { os: "linux", arch: "x64" }, + candidates: candidates({}), + }) + + const optional = await Effect.runPromise(runtime.resolve(descriptor(false))) + const required = await Effect.runPromise(runtime.resolve(descriptor(true)).pipe(Effect.result)) + + expect(optional).toMatchObject({ _tag: "Unavailable", id: "ripgrep", required: false }) + expect(optional._tag === "Unavailable" ? optional.attempts : []).toHaveLength(RuntimeAsset.sources.length) + expect(Result.isFailure(required)).toBe(true) + if (Result.isFailure(required)) { + expect(required.failure).toMatchObject({ _tag: "RequiredAssetUnavailable", id: "ripgrep" }) + } + }) + + test("redacts credentials and sensitive query values from diagnostics", async () => { + const runtime = RuntimeAsset.make({ + platform: { os: "linux", arch: "x64" }, + candidates: candidates({ + mirror: () => + Effect.fail( + new RuntimeAsset.CandidateUnavailable({ + reason: "download failed: https://user:secret@mirror.internal/rg?token=sensitive&file=rg", + }), + ), + }), + }) + + const result = await Effect.runPromise(runtime.resolve(descriptor(false), { sources: ["mirror"] })) + const reason = result._tag === "Unavailable" ? result.attempts[0]?.reason : "" + + expect(reason).toContain("mirror.internal/rg") + expect(reason).toContain("file=rg") + expect(reason).not.toContain("user") + expect(reason).not.toContain("secret") + expect(reason).not.toContain("sensitive") + }) + + test("builds both defaultLayer and LayerNode without ambient services", async () => { + const resolve = Effect.gen(function* () { + const runtime = yield* RuntimeAsset.Service + return yield* runtime.resolve(descriptor(false)) + }) + + const direct = await Effect.runPromise(resolve.pipe(Effect.provide(RuntimeAsset.defaultLayer))) + const node = await Effect.runPromise(resolve.pipe(Effect.provide(LayerNode.buildLayer(RuntimeAsset.node)))) + + expect(node).toEqual(direct) + }) +}) + +function descriptor(required: boolean): RuntimeAsset.Descriptor { + return { + id: "ripgrep", + version: "15.1.0", + required, + targets: [ + { os: "darwin", arch: "arm64", executable: "rg", sha256: "darwin-digest" }, + { os: "linux", arch: "x64", executable: "rg", sha256: "linux-digest" }, + ], + } +} + +function candidates(overrides: Partial): RuntimeAsset.Candidates { + const unavailable = + (source: RuntimeAsset.Source): RuntimeAsset.Candidate => + () => + Effect.fail(new RuntimeAsset.CandidateUnavailable({ reason: `${source} candidate is not configured` })) + return { + system: overrides.system ?? unavailable("system"), + packaged: overrides.packaged ?? unavailable("packaged"), + cache: overrides.cache ?? unavailable("cache"), + mirror: overrides.mirror ?? unavailable("mirror"), + public: overrides.public ?? unavailable("public"), + } +} diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts index 23afe811c2..16ba64a93e 100644 --- a/packages/desktop/electron-builder.config.ts +++ b/packages/desktop/electron-builder.config.ts @@ -59,6 +59,11 @@ const getBase = (appId: string): Configuration => ({ to: "native/", filter: ["index.js", "index.d.ts", "build/Release/mac_window.node", "swift-build/**"], }, + { + from: "resources/runtime-assets/", + to: "runtime-assets/", + filter: ["**/*"], + }, ], mac: { category: "public.app-category.developer-tools", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 5a239863a8..7de87be5e9 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -16,10 +16,12 @@ "prebuild": "bun ./scripts/prebuild.ts", "build": "electron-vite build", "preview": "electron-vite preview", - "package": "electron-builder --config electron-builder.config.ts", - "package:mac": "electron-builder --mac --config electron-builder.config.ts", - "package:win": "electron-builder --win --config electron-builder.config.ts", - "package:linux": "electron-builder --linux --config electron-builder.config.ts", + "verify:runtime-assets": "bun ./scripts/verify-runtime-assets.ts", + "package": "bun run verify:runtime-assets && electron-builder --config electron-builder.config.ts", + "package:mac": "bun run verify:runtime-assets && electron-builder --mac --config electron-builder.config.ts", + "package:win": "bun run verify:runtime-assets && electron-builder --win --config electron-builder.config.ts", + "package:linux": "bun run verify:runtime-assets && electron-builder --linux --config electron-builder.config.ts", + "test": "bun test --only-failures", "native:build": "bun install --cwd native" }, "main": "./out/main/index.js", @@ -36,6 +38,7 @@ "devDependencies": { "@actions/artifact": "4.0.0", "@lydell/node-pty": "catalog:", + "@opencode-ai/core": "workspace:*", "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", "@sentry/solid": "catalog:", diff --git a/packages/desktop/scripts/prebuild.ts b/packages/desktop/scripts/prebuild.ts index 79b0e30afc..38ec25236b 100644 --- a/packages/desktop/scripts/prebuild.ts +++ b/packages/desktop/scripts/prebuild.ts @@ -1,10 +1,19 @@ #!/usr/bin/env bun import { $ } from "bun" +import path from "path" +import { prepareDesktopRuntimeAssets } from "./runtime-assets" import { resolveChannel } from "./utils" const channel = resolveChannel() await $`bun ./scripts/copy-icons.ts ${channel}` await $`bun ./scripts/copy-metainfo.ts ${channel}` +const runtimeAsset = await prepareDesktopRuntimeAssets({ + directory: path.resolve(import.meta.dir, "..", "resources", "runtime-assets"), + mirrorBaseURL: process.env.OPENCODE_RUNTIME_ASSET_MIRROR, + publicFallback: process.env.OPENCODE_RUNTIME_ASSET_DISABLE_PUBLIC !== "true", +}) +console.log(`[runtime-assets] prepared ${runtimeAsset.id}@${runtimeAsset.version}: ${runtimeAsset.path}`) + await $`cd ../opencode && bun script/build-node.ts` diff --git a/packages/desktop/scripts/runtime-assets.ts b/packages/desktop/scripts/runtime-assets.ts new file mode 100644 index 0000000000..621b4a39b7 --- /dev/null +++ b/packages/desktop/scripts/runtime-assets.ts @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + +import path from "path" +import { Effect } from "effect" +import { RuntimeAsset } from "@opencode-ai/core/runtime-asset" +import { RipgrepAsset } from "@opencode-ai/core/runtime-asset/catalog/ripgrep" + +export type DesktopRuntimeAssetsInput = { + readonly directory: string + readonly platform?: RuntimeAsset.Platform + readonly mirrorBaseURL?: string + readonly publicFallback?: boolean + readonly fetch?: RuntimeAsset.Fetch +} + +export async function prepareDesktopRuntimeAssets(input: DesktopRuntimeAssetsInput) { + const platform = input.platform ?? { os: process.platform, arch: process.arch } + const runtime = RuntimeAsset.managed({ + platform, + cacheDirectory: input.directory, + ...(input.mirrorBaseURL ? { mirrorBaseURL: input.mirrorBaseURL } : {}), + ...(input.fetch ? { fetch: input.fetch } : {}), + }) + const sources = RuntimeAsset.sources.filter( + (source) => + source === "cache" || + (source === "mirror" && !!input.mirrorBaseURL) || + (source === "public" && input.publicFallback !== false), + ) + const result = await Effect.runPromise(runtime.resolve(RipgrepAsset.descriptor, { sources })) + if (result._tag !== "Available") throw new Error(`required desktop runtime asset is unavailable: ${result.id}`) + await Bun.write( + path.join(input.directory, "manifest.json"), + JSON.stringify( + { + schemaVersion: 1, + assets: [ + { + id: result.id, + version: result.version, + os: platform.os, + arch: platform.arch, + path: path.relative(input.directory, result.path).replaceAll("\\", "/"), + source: result.source, + sha256: result.sha256, + }, + ], + }, + null, + 2, + ) + "\n", + ) + return verifyDesktopRuntimeAssets({ directory: input.directory, platform }) +} + +export async function verifyDesktopRuntimeAssets(input: { + readonly directory: string + readonly platform?: RuntimeAsset.Platform +}) { + const platform = input.platform ?? { os: process.platform, arch: process.arch } + const runtime = RuntimeAsset.managed({ platform, cacheDirectory: input.directory }) + const result = await Effect.runPromise( + runtime.resolve(RipgrepAsset.descriptor, { + sources: ["cache"], + }), + ) + if (result._tag !== "Available") throw new Error(`required desktop runtime asset is unavailable: ${result.id}`) + const executable = Bun.file(result.path) + if (!(await executable.exists()) || executable.size === 0) { + throw new Error(`required desktop runtime asset is empty: ${result.path}`) + } + const manifestFile = path.join(input.directory, "manifest.json") + const value: unknown = await Bun.file(manifestFile).json() + if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.assets)) { + throw new Error(`desktop runtime asset manifest is invalid: ${manifestFile}`) + } + const entry = value.assets.find( + (value) => + isRecord(value) && + value.id === RipgrepAsset.descriptor.id && + value.version === RipgrepAsset.descriptor.version && + value.os === platform.os && + value.arch === platform.arch, + ) + if (!entry) throw new Error(`desktop runtime asset manifest is missing ripgrep for ${platform.os}-${platform.arch}`) + return result +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} diff --git a/packages/desktop/scripts/verify-runtime-assets.ts b/packages/desktop/scripts/verify-runtime-assets.ts new file mode 100644 index 0000000000..a10225027a --- /dev/null +++ b/packages/desktop/scripts/verify-runtime-assets.ts @@ -0,0 +1,10 @@ +#!/usr/bin/env bun +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + +import path from "path" +import { verifyDesktopRuntimeAssets } from "./runtime-assets" + +const directory = path.resolve(import.meta.dir, "..", "resources", "runtime-assets") +const result = await verifyDesktopRuntimeAssets({ directory }) +console.log(`[runtime-assets] verified ${result.id}@${result.version}: ${result.path}`) diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index b213dbc82a..9a793ca5e9 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -213,6 +213,7 @@ function createSidecarEnv(): Record { ) delete env.DEBUG if (process.platform === "linux") delete env.LD_PRELOAD + if (app.isPackaged) env.OPENCODE_RUNTIME_ASSETS_DIR = join(process.resourcesPath, "runtime-assets") if (!app.isPackaged) env.OPENCODE_DISABLE_CHANNEL_DB = "1" return env } diff --git a/packages/desktop/test/runtime-assets.test.ts b/packages/desktop/test/runtime-assets.test.ts new file mode 100644 index 0000000000..784a2545bb --- /dev/null +++ b/packages/desktop/test/runtime-assets.test.ts @@ -0,0 +1,25 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { mkdtemp, rm } from "fs/promises" +import os from "os" +import path from "path" +import { verifyDesktopRuntimeAssets } from "../scripts/runtime-assets" + +const roots: string[] = [] + +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) + +describe("desktop runtime assets", () => { + test("fails packaging verification when required ripgrep is missing", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "desktop-runtime-assets-")) + roots.push(directory) + + await expect( + verifyDesktopRuntimeAssets({ + directory, + platform: { os: "linux", arch: "x64" }, + }), + ).rejects.toThrow("Required runtime asset is unavailable: ripgrep@15.1.0") + }) +}) diff --git a/packages/opencode/script/prefetch-ripgrep.ts b/packages/opencode/script/prefetch-ripgrep.ts index fa955be124..28925a5dbb 100644 --- a/packages/opencode/script/prefetch-ripgrep.ts +++ b/packages/opencode/script/prefetch-ripgrep.ts @@ -1,173 +1,82 @@ #!/usr/bin/env bun -// -// Prefetch the ripgrep binary into every dist/opencode-/bin output produced -// by build.ts, so air-gapped users never hit the runtime download in -// packages/opencode/src/file/ripgrep.ts. -// -// Strategy: -// 1. Scan dist/opencode-* directories (created by build.ts). -// 2. Derive the upstream ripgrep PLATFORM key from each directory name. -// Several variants (baseline / musl) share the same rg binary, so we -// cache one download per rg-key under dist/.rg-cache/. -// 3. Extract rg (or rg.exe) into each dist/opencode-/bin/, where -// opencode's which("rg") step picks it up before any network fallback. -// -// Usage: -// bun run script/prefetch-ripgrep.ts # all platforms in dist/ -// bun run script/prefetch-ripgrep.ts --only # one directory only -// bun run script/prefetch-ripgrep.ts --version 15.1.0 -// -// Exit codes: -// 0 success / 1 fatal error / 2 no matching dist directories found. +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later -import { $ } from "bun" import fs from "fs" import path from "path" -import { fileURLToPath } from "url" +import { Effect } from "effect" +import { RuntimeAsset } from "@opencode-ai/core/runtime-asset" +import { RipgrepAsset } from "@opencode-ai/core/runtime-asset/catalog/ripgrep" -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const pkgDir = path.resolve(__dirname, "..") - -// Keep in sync with packages/opencode/src/file/ripgrep.ts -const DEFAULT_VERSION = "15.1.0" -const PLATFORM = { - "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, - "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, - "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" }, - "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" }, - "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" }, - "ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" }, - "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }, -} as const - -type RgKey = keyof typeof PLATFORM - -const args = process.argv.slice(2) -const versionFlag = args.indexOf("--version") -const version = versionFlag >= 0 ? args[versionFlag + 1] : DEFAULT_VERSION -const onlyFlag = args.indexOf("--only") -const only = onlyFlag >= 0 ? args[onlyFlag + 1] : undefined - -const distDir = path.join(pkgDir, "dist") -if (!fs.existsSync(distDir)) { - console.error(`[prefetch-ripgrep] no dist/ directory at ${distDir}; build first.`) - process.exit(2) +const packageDirectory = path.resolve(import.meta.dir, "..") +const arguments_ = process.argv.slice(2) +const versionIndex = arguments_.indexOf("--version") +const requestedVersion = versionIndex === -1 ? undefined : arguments_[versionIndex + 1] +if (requestedVersion && requestedVersion !== RipgrepAsset.version) { + throw new Error(`ripgrep version is pinned to ${RipgrepAsset.version}; requested ${requestedVersion}`) } +const onlyIndex = arguments_.indexOf("--only") +const only = onlyIndex === -1 ? undefined : arguments_[onlyIndex + 1] +if (onlyIndex !== -1 && !only) throw new Error("--only requires a dist directory name") -const dirs = fs - .readdirSync(distDir, { withFileTypes: true }) - .filter((e) => e.isDirectory() && e.name.startsWith("opencode-")) - .map((e) => e.name) - .filter((name) => (only ? name === only : true)) - -if (dirs.length === 0) { - console.error(`[prefetch-ripgrep] no opencode-* directories under ${distDir}.`) +const distDirectory = path.join(packageDirectory, "dist") +if (!fs.existsSync(distDirectory)) { + console.error(`[prefetch-ripgrep] no dist/ directory at ${distDirectory}; build first.`) process.exit(2) } -const cacheDir = path.join(distDir, ".rg-cache") -fs.mkdirSync(cacheDir, { recursive: true }) - -/** - * Map a build.ts output directory like "opencode-windows-x64-baseline" to the - * ripgrep PLATFORM key. The `baseline` / `musl` modifiers do not affect rg. - */ -function deriveRgKey(dirName: string): RgKey | undefined { - // dirName = opencode--[-modifier...] - const parts = dirName.split("-") - if (parts.length < 3) return undefined - const [, osTok, archTok] = parts - const os = osTok === "windows" ? "win32" : osTok - const arch = archTok - const key = `${arch}-${os}` as RgKey - return key in PLATFORM ? key : undefined +const directories = fs + .readdirSync(distDirectory, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && entry.name.startsWith("opencode-")) + .map((entry) => entry.name) + .filter((name) => !only || name === only) +if (!directories.length) { + console.error(`[prefetch-ripgrep] no opencode-* directories under ${distDirectory}.`) + process.exit(2) } -async function fetchArchive(rgKey: RgKey): Promise { - const cfg = PLATFORM[rgKey] - const filename = `ripgrep-${version}-${cfg.platform}.${cfg.extension}` - const cached = path.join(cacheDir, filename) - if (fs.existsSync(cached) && fs.statSync(cached).size > 0) { - console.log(`[prefetch-ripgrep] cache hit: ${filename}`) - return cached - } - const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}` - console.log(`[prefetch-ripgrep] downloading ${url}`) - const res = await fetch(url, { redirect: "follow" }) - if (!res.ok) throw new Error(`download failed ${res.status} ${res.statusText}: ${url}`) - const buf = new Uint8Array(await res.arrayBuffer()) - if (buf.byteLength === 0) throw new Error(`empty archive: ${url}`) - fs.writeFileSync(cached, buf) - return cached -} +const cacheDirectory = path.join(distDirectory, ".rg-cache") +const runtimes = new Map() +const sources: RuntimeAsset.Source[] = process.env.OPENCODE_RUNTIME_ASSET_MIRROR + ? ["cache", "mirror", "public"] + : ["cache", "public"] +const results = await Promise.all( + directories.map(async (directory) => { + const platform = derivePlatform(directory) + if (!platform) return { directory, status: "skipped" as const } + const key = `${platform.os}-${platform.arch}` + const runtime = + runtimes.get(key) ?? + RuntimeAsset.managed({ + platform, + cacheDirectory, + mirrorBaseURL: process.env.OPENCODE_RUNTIME_ASSET_MIRROR, + }) + runtimes.set(key, runtime) + console.log(`[prefetch-ripgrep] ${directory} -> ${key}`) -async function extractRg(archive: string, rgKey: RgKey, targetBinDir: string): Promise { - const cfg = PLATFORM[rgKey] - const rgName = cfg.extension === "zip" ? "rg.exe" : "rg" - const targetPath = path.join(targetBinDir, rgName) - if (fs.existsSync(targetPath)) { - console.log(`[prefetch-ripgrep] rg already present at ${path.relative(pkgDir, targetPath)}`) - return - } - fs.mkdirSync(targetBinDir, { recursive: true }) - // Extract to a sibling temp dir, then copy. - // On Windows, Git Bash's cygwin tar treats `D:\foo` as a remote `host:path`, - // so spawn the Windows-native tar.exe (libarchive, accepts native paths and - // handles both .tar.gz and .zip) directly via Bun.spawnSync — bypassing the - // shell entirely. On POSIX, just use system tar the same way. - const tmp = path.join(cacheDir, `extract-${rgKey}`) - fs.rmSync(tmp, { recursive: true, force: true }) - fs.mkdirSync(tmp, { recursive: true }) - const tarBin = - process.platform === "win32" && fs.existsSync("C:\\Windows\\System32\\tar.exe") - ? "C:\\Windows\\System32\\tar.exe" - : "tar" - const proc = Bun.spawnSync({ - cmd: [tarBin, "-xf", archive, "-C", tmp], - stdout: "pipe", - stderr: "pipe", - }) - if (proc.exitCode !== 0) { - throw new Error( - `tar extract failed (exit ${proc.exitCode}) for ${archive}\nstderr: ${proc.stderr?.toString() ?? ""}`, - ) - } - // ripgrep archives extract to ripgrep--/rg(.exe) - const subdirs = fs.readdirSync(tmp, { withFileTypes: true }).filter((e) => e.isDirectory()) - let extractedRg: string | undefined - for (const d of subdirs) { - const candidate = path.join(tmp, d.name, rgName) - if (fs.existsSync(candidate)) { - extractedRg = candidate - break - } - } - if (!extractedRg) { - throw new Error(`rg binary not found inside archive ${archive}`) - } - fs.copyFileSync(extractedRg, targetPath) - if (cfg.extension !== "zip") fs.chmodSync(targetPath, 0o755) - fs.rmSync(tmp, { recursive: true, force: true }) - console.log(`[prefetch-ripgrep] wrote ${path.relative(pkgDir, targetPath)}`) -} + const resolved = await Effect.runPromise(runtime.resolve(RipgrepAsset.descriptor, { sources })) + if (resolved._tag !== "Available") throw new Error(`ripgrep unavailable for ${key}: ${resolved.reason}`) + const bin = path.join(distDirectory, directory, "bin") + const target = path.join(bin, path.basename(resolved.path)) + fs.mkdirSync(bin, { recursive: true }) + fs.copyFileSync(resolved.path, target) + if (platform.os !== "win32") fs.chmodSync(target, 0o755) + console.log(`[prefetch-ripgrep] wrote ${path.relative(packageDirectory, target)} from ${resolved.source}`) + return { directory, status: "injected" as const } + }), +) -let injected = 0 -const skipped: string[] = [] -for (const dirName of dirs) { - const rgKey = deriveRgKey(dirName) - if (!rgKey) { - skipped.push(`${dirName} (unsupported platform)`) - continue - } - console.log(`[prefetch-ripgrep] ${dirName} -> ${rgKey}`) - const archive = await fetchArchive(rgKey) - const binDir = path.join(distDir, dirName, "bin") - await extractRg(archive, rgKey, binDir) - injected++ -} +const injected = results.filter((result) => result.status === "injected") +const skipped = results.filter((result) => result.status === "skipped") +console.log(`[prefetch-ripgrep] done: ${injected.length} directory(ies) injected, ${skipped.length} skipped.`) +skipped.forEach((result) => console.log(` - skip ${result.directory} (unsupported platform)`)) -console.log(`[prefetch-ripgrep] done: ${injected} directory(ies) injected, ${skipped.length} skipped.`) -if (skipped.length) { - for (const s of skipped) console.log(` - skip ${s}`) +function derivePlatform(directory: string): RuntimeAsset.Platform | undefined { + const parts = directory.split("-") + const os = parts[1] === "windows" ? "win32" : parts[1] + const arch = parts[2] + if (os !== "darwin" && os !== "linux" && os !== "win32") return + if (!arch || !RipgrepAsset.descriptor.targets.some((target) => target.os === os && target.arch === arch)) return + return { os, arch } } diff --git a/packages/opencode/src/dag/admission.ts b/packages/opencode/src/dag/admission.ts index fad431b196..68736d9f3c 100644 --- a/packages/opencode/src/dag/admission.ts +++ b/packages/opencode/src/dag/admission.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as DagAdmission from "./admission" import { Schema } from "effect" diff --git a/packages/opencode/src/dag/config.ts b/packages/opencode/src/dag/config.ts index 7be8c88300..78d330b8b2 100644 --- a/packages/opencode/src/dag/config.ts +++ b/packages/opencode/src/dag/config.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG defaults config — `dag.jsonc` beside the opencode config. * diff --git a/packages/opencode/src/dag/dag.ts b/packages/opencode/src/dag/dag.ts index ab03a1915d..d38dced3fb 100644 --- a/packages/opencode/src/dag/dag.ts +++ b/packages/opencode/src/dag/dag.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as Dag from "./dag" import { LayerNode } from "@opencode-ai/core/effect/layer-node" diff --git a/packages/opencode/src/dag/model.ts b/packages/opencode/src/dag/model.ts index 14224d151c..4a9612ffdb 100644 --- a/packages/opencode/src/dag/model.ts +++ b/packages/opencode/src/dag/model.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as DagModel from "./model" export type Ref = { diff --git a/packages/opencode/src/dag/review-lifecycle.ts b/packages/opencode/src/dag/review-lifecycle.ts index 5ee59bc9c4..b33c19dece 100644 --- a/packages/opencode/src/dag/review-lifecycle.ts +++ b/packages/opencode/src/dag/review-lifecycle.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as DagReviewLifecycle from "./review-lifecycle" import type { NodeConfig, WorkflowConfig } from "./dag" diff --git a/packages/opencode/src/dag/runtime/capture.ts b/packages/opencode/src/dag/runtime/capture.ts index ab15b09f87..8c0ec0572c 100644 --- a/packages/opencode/src/dag/runtime/capture.ts +++ b/packages/opencode/src/dag/runtime/capture.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG structured-output schema registry + validation. * diff --git a/packages/opencode/src/dag/runtime/eval.ts b/packages/opencode/src/dag/runtime/eval.ts index 2f9298f888..be1570cb9f 100644 --- a/packages/opencode/src/dag/runtime/eval.ts +++ b/packages/opencode/src/dag/runtime/eval.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG conditional-node evaluation + input_mapping resolution (task 2.16). * diff --git a/packages/opencode/src/dag/runtime/loop.ts b/packages/opencode/src/dag/runtime/loop.ts index ba06226b9c..5da1c80553 100644 --- a/packages/opencode/src/dag/runtime/loop.ts +++ b/packages/opencode/src/dag/runtime/loop.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as DagLoop from "./loop" import { Cause, Effect, Layer, Context, Stream, Semaphore, Fiber, Option, DateTime, Clock } from "effect" diff --git a/packages/opencode/src/dag/runtime/recovery.ts b/packages/opencode/src/dag/runtime/recovery.ts index a9bbc0848a..ff6a135860 100644 --- a/packages/opencode/src/dag/runtime/recovery.ts +++ b/packages/opencode/src/dag/runtime/recovery.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG crash recovery — EventV2-driven, no separate recovery table/scan. * diff --git a/packages/opencode/src/dag/runtime/spawn.ts b/packages/opencode/src/dag/runtime/spawn.ts index aa66f05966..5926e6011f 100644 --- a/packages/opencode/src/dag/runtime/spawn.ts +++ b/packages/opencode/src/dag/runtime/spawn.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG node spawn — reuses the `task` tool's spawn path. * diff --git a/packages/opencode/src/dag/runtime/summary-publisher.ts b/packages/opencode/src/dag/runtime/summary-publisher.ts index 091d3d7672..618d375762 100644 --- a/packages/opencode/src/dag/runtime/summary-publisher.ts +++ b/packages/opencode/src/dag/runtime/summary-publisher.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as DagSummaryPublisher from "./summary-publisher" import { Cause, Effect, Exit, Layer, Scope, Context } from "effect" diff --git a/packages/opencode/src/dag/templates/resolve.ts b/packages/opencode/src/dag/templates/resolve.ts index fc0b1c3fd4..a68356f486 100644 --- a/packages/opencode/src/dag/templates/resolve.ts +++ b/packages/opencode/src/dag/templates/resolve.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * DAG prompt-template resolver. * diff --git a/packages/opencode/src/dag/templates/sanitize.ts b/packages/opencode/src/dag/templates/sanitize.ts index 39d7964b24..536349d9a8 100644 --- a/packages/opencode/src/dag/templates/sanitize.ts +++ b/packages/opencode/src/dag/templates/sanitize.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * Prompt-injection sanitizer for DAG template input. * diff --git a/packages/opencode/src/dag/workflows.ts b/packages/opencode/src/dag/workflows.ts index b8b98d2ede..27ca8bc9fd 100644 --- a/packages/opencode/src/dag/workflows.ts +++ b/packages/opencode/src/dag/workflows.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * Workflow library — reusable start specs discovered by name. * diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts index 0f857c2285..34a18a8785 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/dag.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../middleware/authorization" diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts index f85c23fc89..2a73c1afcc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/dag.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" diff --git a/packages/opencode/src/tool/workflow.ts b/packages/opencode/src/tool/workflow.ts index f1a87358b0..a4b87ac69c 100644 --- a/packages/opencode/src/tool/workflow.ts +++ b/packages/opencode/src/tool/workflow.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + import { Tool } from "./tool" import { CommandPlugin } from "@opencode-ai/core/plugin/command" import { Effect, Option, Schema } from "effect" diff --git a/packages/schema/src/dag-event.ts b/packages/schema/src/dag-event.ts index a2a93832c0..d21987c6f5 100644 --- a/packages/schema/src/dag-event.ts +++ b/packages/schema/src/dag-event.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + export * as DagEvent from "./dag-event" import { Schema } from "effect" diff --git a/packages/tui/src/feature-plugins/sidebar/dag-panel.tsx b/packages/tui/src/feature-plugins/sidebar/dag-panel.tsx index b2832241a5..52ab53156c 100644 --- a/packages/tui/src/feature-plugins/sidebar/dag-panel.tsx +++ b/packages/tui/src/feature-plugins/sidebar/dag-panel.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** @jsxImportSource @opentui/solid */ import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" import type { DagNode, DagWorkflowSummary } from "@opencode-ai/sdk/v2" diff --git a/packages/tui/src/feature-plugins/system/dag-inspector-utils.ts b/packages/tui/src/feature-plugins/system/dag-inspector-utils.ts index 7f5b58cf34..0fc612f54d 100644 --- a/packages/tui/src/feature-plugins/system/dag-inspector-utils.ts +++ b/packages/tui/src/feature-plugins/system/dag-inspector-utils.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** Pure topology helpers for the DAG inspector. Extracted for unit testing, * mirroring the diff-viewer-file-tree-utils pattern in this directory. */ diff --git a/packages/tui/src/feature-plugins/system/dag-inspector.tsx b/packages/tui/src/feature-plugins/system/dag-inspector.tsx index 9639a9458e..6a672515ea 100644 --- a/packages/tui/src/feature-plugins/system/dag-inspector.tsx +++ b/packages/tui/src/feature-plugins/system/dag-inspector.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** @jsxImportSource @opentui/solid */ import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" import type { BuiltinTuiPlugin } from "../builtins" diff --git a/script/check-license-scope.ts b/script/check-license-scope.ts new file mode 100644 index 0000000000..0ea390cd9d --- /dev/null +++ b/script/check-license-scope.ts @@ -0,0 +1,198 @@ +import { existsSync } from "fs" +import path from "path" + +type LicenseScope = { + id: string + name: string + status: "active" | "planned" + spdx: "AGPL-3.0-or-later" + copyright: string + licenseFiles: string[] + paths: string[] +} + +type LicenseScopeManifest = { + schemaVersion: 1 + defaultLicense: { + spdx: "MIT" + licenseFile: string + copyright: string + } + scopes: LicenseScope[] +} + +export type LicenseScopeIssue = { + code: string + path?: string + message: string +} + +export async function checkLicenseScope(root: string) { + const manifest = parseManifest(await Bun.file(path.join(root, "LICENSE-SCOPE.json")).json()) + const notice = await Bun.file(path.join(root, "NOTICE")).text() + const allPaths = manifest.scopes.flatMap((scope) => scope.paths) + const duplicateIssues: LicenseScopeIssue[] = + new Set(allPaths).size === allPaths.length + ? [] + : [{ code: "duplicate-path", message: "LICENSE-SCOPE.json contains duplicate covered paths" }] + const defaultIssues = existsSync(path.join(root, manifest.defaultLicense.licenseFile)) + ? [] + : [ + { + code: "missing-default-license", + path: manifest.defaultLicense.licenseFile, + message: `Default MIT license file is missing: ${manifest.defaultLicense.licenseFile}`, + }, + ] + const scopeIssues = manifest.scopes.flatMap((scope) => { + const unsafePaths = [...scope.licenseFiles, ...scope.paths].filter( + (file) => path.isAbsolute(file) || file.split("/").includes(".."), + ) + const noticeValues = [scope.name, scope.spdx, scope.copyright, ...scope.licenseFiles, ...scope.paths] + return [ + ...unsafePaths.map((file) => ({ + code: "unsafe-path", + path: file, + message: `License scope path must be repository-relative: ${file}`, + })), + ...scope.licenseFiles + .filter((file) => !existsSync(path.join(root, file))) + .map((file) => ({ + code: "missing-license-file", + path: file, + message: `License text is missing: ${file}`, + })), + ...noticeValues + .filter((value) => !notice.includes(value)) + .map((value) => ({ + code: "notice-missing-value", + path: value, + message: `NOTICE does not contain license scope value: ${value}`, + })), + ...(scope.status === "active" + ? scope.paths + .filter((file) => !existsSync(path.join(root, file))) + .map((file) => ({ + code: "missing-active-path", + path: file, + message: `Active AGPL path is missing: ${file}`, + })) + : []), + ] + }) + const sourceIssues = ( + await Promise.all( + manifest.scopes + .filter((scope) => scope.status === "active") + .flatMap((scope) => scope.paths.map((file) => sourceFiles(root, file))) + .map(async (files) => + Promise.all( + (await files).map(async (file) => { + const header = (await Bun.file(file).text()).split("\n").slice(0, 8).join("\n") + const display = path.relative(root, file).split(path.sep).join("/") + return [ + ...(!header.includes("SPDX-FileCopyrightText: 2026 LeXwDeX") + ? [ + { + code: "missing-spdx-copyright", + path: display, + message: `AGPL source is missing its SPDX copyright header: ${display}`, + }, + ] + : []), + ...(!header.includes("SPDX-License-Identifier: AGPL-3.0-or-later") + ? [ + { + code: "missing-spdx-license", + path: display, + message: `AGPL source is missing its SPDX license header: ${display}`, + }, + ] + : []), + ] + }), + ), + ), + ) + ).flat(2) + + return [...duplicateIssues, ...defaultIssues, ...scopeIssues, ...sourceIssues] +} + +async function sourceFiles(root: string, file: string) { + const target = path.join(root, file) + if (!file.endsWith("/")) return /\.tsx?$/.test(file) && existsSync(target) ? [target] : [] + if (!existsSync(target)) return [] + return (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: target, absolute: true, onlyFiles: true }))).filter( + (entry) => /\.tsx?$/.test(entry), + ) +} + +function parseManifest(value: unknown): LicenseScopeManifest { + const manifest = record(value, "license scope manifest") + if (manifest.schemaVersion !== 1) throw new Error("LICENSE-SCOPE.json schemaVersion must be 1") + const defaultLicense = record(manifest.defaultLicense, "defaultLicense") + if (defaultLicense.spdx !== "MIT") throw new Error("The default repository license must remain MIT") + if (!Array.isArray(manifest.scopes)) throw new Error("LICENSE-SCOPE.json scopes must be an array") + + return { + schemaVersion: 1, + defaultLicense: { + spdx: "MIT", + licenseFile: string(defaultLicense.licenseFile, "defaultLicense.licenseFile"), + copyright: string(defaultLicense.copyright, "defaultLicense.copyright"), + }, + scopes: manifest.scopes.map((value, index) => { + const scope = record(value, `scopes[${index}]`) + if (scope.status !== "active" && scope.status !== "planned") { + throw new Error(`scopes[${index}].status must be active or planned`) + } + if (scope.spdx !== "AGPL-3.0-or-later") { + throw new Error(`scopes[${index}].spdx must be AGPL-3.0-or-later`) + } + return { + id: string(scope.id, `scopes[${index}].id`), + name: string(scope.name, `scopes[${index}].name`), + status: scope.status, + spdx: "AGPL-3.0-or-later", + copyright: string(scope.copyright, `scopes[${index}].copyright`), + licenseFiles: strings(scope.licenseFiles, `scopes[${index}].licenseFiles`), + paths: strings(scope.paths, `scopes[${index}].paths`), + } + }), + } +} + +function record(value: unknown, name: string): Record { + if (!isRecord(value)) throw new Error(`${name} must be an object`) + return value +} + +function string(value: unknown, name: string) { + if (typeof value !== "string" || !value) throw new Error(`${name} must be a non-empty string`) + return value +} + +function strings(value: unknown, name: string) { + if (!Array.isArray(value) || !value.length || !value.every(isString)) { + throw new Error(`${name} must be a non-empty string array`) + } + return value.filter(isString) +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} + +function isString(value: unknown): value is string { + return typeof value === "string" && !!value +} + +if (import.meta.main) { + const issues = await checkLicenseScope(path.join(import.meta.dir, "..")) + if (issues.length) { + console.error(issues.map((issue) => `[${issue.code}] ${issue.message}`).join("\n")) + process.exit(1) + } + console.log("License scope check passed") +} diff --git a/script/corresponding-source.ts b/script/corresponding-source.ts new file mode 100644 index 0000000000..a179119e86 --- /dev/null +++ b/script/corresponding-source.ts @@ -0,0 +1,168 @@ +import { existsSync } from "fs" +import { mkdir } from "fs/promises" +import path from "path" + +export const requiredSourceFiles = [ + "LICENSE", + "NOTICE", + "LICENSE-SCOPE.json", + "LICENSE-SCOPE.schema.json", + "CORRESPONDING_SOURCE.md", + "package.json", + "bun.lock", + "script/corresponding-source.ts", + "packages/opencode/script/build.ts", + "packages/desktop/scripts/prebuild.ts", + "packages/desktop/electron-builder.config.ts", +] as const + +export type CorrespondingSourceInput = { + root: string + output: string + version: string + ref?: string + allowDirty?: boolean + requiredFiles?: readonly string[] +} + +export async function createCorrespondingSource(input: CorrespondingSourceInput) { + if (!input.version.trim()) throw new Error("Corresponding source version is required") + + const root = path.resolve(input.root) + const requiredFiles = input.requiredFiles ?? requiredSourceFiles + if (requiredFiles.some((file) => path.isAbsolute(file) || file.split("/").includes(".."))) { + throw new Error("Corresponding source paths must be repository-relative") + } + + const commit = await git(root, ["rev-parse", "--verify", `${input.ref ?? "HEAD"}^{commit}`]) + if (!input.allowDirty && (await git(root, ["status", "--porcelain", "--untracked-files=no"]))) { + throw new Error("Tracked working tree changes must be committed before creating corresponding source") + } + await Promise.all(requiredFiles.map((file) => git(root, ["cat-file", "-e", `${commit}:${file}`]))) + + const output = path.resolve(input.output) + const version = input.version.replace(/[^0-9A-Za-z._-]/g, "-") + const base = `opencode-graphagent-${version}-source-${commit.slice(0, 12)}` + const archive = path.join(output, `${base}.tar.gz`) + await mkdir(output, { recursive: true }) + await command(root, ["git", "archive", "--format=tar.gz", `--prefix=${base}/`, `--output=${archive}`, commit]) + + const sha256 = await digest(archive) + const sourceDateEpoch = Number(await git(root, ["show", "-s", "--format=%ct", commit])) + const manifest = path.join(output, `${base}.source.json`) + await Bun.write( + manifest, + JSON.stringify( + { + schemaVersion: 1, + artifactKind: "agpl-corresponding-source", + project: "OpenCode-GraphAgent", + version: input.version, + commit, + generatedAt: new Date(sourceDateEpoch * 1000).toISOString(), + sourceDateEpoch, + archive: { + file: path.basename(archive), + sha256, + }, + requiredFiles, + dependencyLocks: ["bun.lock"], + licenseScope: "LICENSE-SCOPE.json", + rebuildInstructions: "CORRESPONDING_SOURCE.md", + }, + null, + 2, + ) + "\n", + ) + const checksum = path.join(output, `${base}.sha256`) + await Bun.write(checksum, `${sha256} ${path.basename(archive)}\n`) + + return { archive, manifest, checksum, commit, sha256 } +} + +export async function verifyCorrespondingSourceArtifacts(directory: string) { + const root = path.resolve(directory) + const manifests = await Array.fromAsync( + new Bun.Glob("*-source-*.source.json").scan({ cwd: root, absolute: true, onlyFiles: true }), + ) + if (manifests.length !== 1) { + throw new Error(`Expected exactly one corresponding source manifest in ${root}, found ${manifests.length}`) + } + + const value: unknown = await Bun.file(manifests[0]).json() + if (!isRecord(value) || !isRecord(value.archive)) throw new Error("Corresponding source manifest is malformed") + if (typeof value.archive.file !== "string" || path.basename(value.archive.file) !== value.archive.file) { + throw new Error("Corresponding source archive filename is invalid") + } + if (typeof value.archive.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.archive.sha256)) { + throw new Error("Corresponding source SHA-256 is invalid") + } + + const archive = path.join(root, value.archive.file) + const checksum = manifests[0].replace(/\.source\.json$/, ".sha256") + if (!existsSync(archive)) throw new Error(`Corresponding source archive is missing: ${value.archive.file}`) + if (!existsSync(checksum)) throw new Error(`Corresponding source checksum is missing: ${path.basename(checksum)}`) + const actual = await digest(archive) + if (actual !== value.archive.sha256) throw new Error(`Corresponding source digest mismatch for ${value.archive.file}`) + if ((await Bun.file(checksum).text()) !== `${actual} ${value.archive.file}\n`) { + throw new Error(`Corresponding source checksum file does not match ${value.archive.file}`) + } + return { manifest: manifests[0], archive, checksum, sha256: actual } +} + +async function git(root: string, args: string[]) { + return command(root, ["git", ...args]) +} + +async function command(root: string, args: string[]) { + const child = Bun.spawn(args, { + cwd: root, + stdout: "pipe", + stderr: "pipe", + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + if (exitCode !== 0) throw new Error(`${args.join(" ")} failed (${exitCode}): ${stderr.trim() || stdout.trim()}`) + return stdout.trim() +} + +async function digest(file: string) { + const hasher = new Bun.CryptoHasher("sha256") + for await (const chunk of Bun.file(file).stream()) hasher.update(chunk) + return hasher.digest("hex") +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value) +} + +async function main() { + const verifyIndex = Bun.argv.indexOf("--verify") + if (verifyIndex !== -1) { + const directory = Bun.argv[verifyIndex + 1] + if (!directory) throw new Error("Usage: corresponding-source.ts --verify ") + console.log(JSON.stringify(await verifyCorrespondingSourceArtifacts(directory), null, 2)) + return + } + + const versionIndex = Bun.argv.indexOf("--version") + const outputIndex = Bun.argv.indexOf("--output") + const version = versionIndex === -1 ? undefined : Bun.argv[versionIndex + 1] + if (!version) throw new Error("Usage: corresponding-source.ts --version [--output ]") + const output = + outputIndex === -1 ? path.join(import.meta.dir, "..", "dist", "corresponding-source") : Bun.argv[outputIndex + 1] + if (!output) throw new Error("--output requires a directory") + + const result = await createCorrespondingSource({ + root: path.join(import.meta.dir, ".."), + output, + version, + allowDirty: Bun.argv.includes("--allow-dirty"), + }) + console.log(JSON.stringify(result, null, 2)) +} + +if (import.meta.main) await main() From a406dcc54859e00982b6320881969df501adf8df Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:26:05 +0800 Subject: [PATCH 08/11] docs: align claude guide with domain map --- CLAUDE.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f3b1950516..4b6352ab73 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,8 +140,7 @@ Curated *global* workflows live in a separate repo, [`LeXwDeX/opencode-dag-confi established capability specs. Active proposals (e.g. `harden-goal-state-machine`, `internalize-dag-block-capabilities`) define in-flight work. - **`CONTEXT-MAP.md` → `CONTEXT.md`** — multi-context domain docs. `CONTEXT-MAP.md` is the - index; read the linked `CONTEXT.md`(s) relevant to the area before working in it. A DAG - `CONTEXT.md` does not yet exist. + index; read the linked `CONTEXT.md`(s) relevant to the area before working in it. - **`docs/agents/`** — issue-tracker workflow, triage labels, domain-doc conventions. ## Critical, non-obvious rules From ae1b391f444fe9d2814a3f4330e6893b23101b44 Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:27:29 +0800 Subject: [PATCH 09/11] chore(dag): cover authoring files in license gate --- packages/opencode/src/dag/authoring.ts | 3 +++ packages/opencode/src/dag/blocks.ts | 3 +++ packages/opencode/src/dag/validation.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/packages/opencode/src/dag/authoring.ts b/packages/opencode/src/dag/authoring.ts index 18fcceee85..94667ed338 100644 --- a/packages/opencode/src/dag/authoring.ts +++ b/packages/opencode/src/dag/authoring.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * The only source-to-prepared-graph seam for workflow authoring. * diff --git a/packages/opencode/src/dag/blocks.ts b/packages/opencode/src/dag/blocks.ts index ecc66d0775..f1c7989892 100644 --- a/packages/opencode/src/dag/blocks.ts +++ b/packages/opencode/src/dag/blocks.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + import { Schema } from "effect" import type { NodeConfig } from "./dag" diff --git a/packages/opencode/src/dag/validation.ts b/packages/opencode/src/dag/validation.ts index ace5701901..c7bb864463 100644 --- a/packages/opencode/src/dag/validation.ts +++ b/packages/opencode/src/dag/validation.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LeXwDeX +// SPDX-License-Identifier: AGPL-3.0-or-later + /** * Workflow spec validation authority. * From 55aa8b37896cdcca5aa559facd8113c36007ac81 Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:36:21 +0800 Subject: [PATCH 10/11] fix(runtime): keep lint at baseline --- packages/core/src/runtime-asset/index.ts | 1 - packages/core/test/corresponding-source.test.ts | 8 ++++++-- packages/core/test/runtime-asset-cache.test.ts | 2 +- packages/desktop/test/runtime-assets.test.ts | 16 ++++++++++------ packages/opencode/script/prefetch-ripgrep.ts | 6 ++++-- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/core/src/runtime-asset/index.ts b/packages/core/src/runtime-asset/index.ts index 0a5b330d14..84db518cd3 100644 --- a/packages/core/src/runtime-asset/index.ts +++ b/packages/core/src/runtime-asset/index.ts @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2026 LeXwDeX // SPDX-License-Identifier: AGPL-3.0-or-later -import { existsSync } from "fs" import { chmod, copyFile, mkdir, mkdtemp, rename, rm } from "fs/promises" import path from "path" import { Context, Effect, Layer } from "effect" diff --git a/packages/core/test/corresponding-source.test.ts b/packages/core/test/corresponding-source.test.ts index 08637b208b..9889db0376 100644 --- a/packages/core/test/corresponding-source.test.ts +++ b/packages/core/test/corresponding-source.test.ts @@ -41,9 +41,13 @@ describe("corresponding source", () => { }) await unlink(result.archive) - await expect(verifyCorrespondingSourceArtifacts(path.dirname(result.archive))).rejects.toThrow( - "Corresponding source archive is missing", + const failure = await verifyCorrespondingSourceArtifacts(path.dirname(result.archive)).then( + () => undefined, + (error: unknown) => error, ) + expect(failure).toBeInstanceOf(Error) + if (!(failure instanceof Error)) throw new Error("expected source verification to fail") + expect(failure.message).toContain("Corresponding source archive is missing") }) }) diff --git a/packages/core/test/runtime-asset-cache.test.ts b/packages/core/test/runtime-asset-cache.test.ts index 7c9888ecb7..5fdf7abd3a 100644 --- a/packages/core/test/runtime-asset-cache.test.ts +++ b/packages/core/test/runtime-asset-cache.test.ts @@ -8,7 +8,7 @@ import { RuntimeAsset } from "@opencode-ai/core/runtime-asset" const cleanups: Array<() => void | Promise> = [] afterEach(async () => { - await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())) + await Promise.all(cleanups.splice(0).map(async (cleanup) => cleanup())) }) describe("RuntimeAsset managed candidates", () => { diff --git a/packages/desktop/test/runtime-assets.test.ts b/packages/desktop/test/runtime-assets.test.ts index 784a2545bb..1c0171db7b 100644 --- a/packages/desktop/test/runtime-assets.test.ts +++ b/packages/desktop/test/runtime-assets.test.ts @@ -15,11 +15,15 @@ describe("desktop runtime assets", () => { const directory = await mkdtemp(path.join(os.tmpdir(), "desktop-runtime-assets-")) roots.push(directory) - await expect( - verifyDesktopRuntimeAssets({ - directory, - platform: { os: "linux", arch: "x64" }, - }), - ).rejects.toThrow("Required runtime asset is unavailable: ripgrep@15.1.0") + const failure = await verifyDesktopRuntimeAssets({ + directory, + platform: { os: "linux", arch: "x64" }, + }).then( + () => undefined, + (error: unknown) => error, + ) + expect(failure).toBeInstanceOf(Error) + if (!(failure instanceof Error)) throw new Error("expected runtime asset verification to fail") + expect(failure.message).toContain("Required runtime asset is unavailable: ripgrep@15.1.0") }) }) diff --git a/packages/opencode/script/prefetch-ripgrep.ts b/packages/opencode/script/prefetch-ripgrep.ts index 28925a5dbb..cda0fc89de 100644 --- a/packages/opencode/script/prefetch-ripgrep.ts +++ b/packages/opencode/script/prefetch-ripgrep.ts @@ -76,7 +76,9 @@ function derivePlatform(directory: string): RuntimeAsset.Platform | undefined { const parts = directory.split("-") const os = parts[1] === "windows" ? "win32" : parts[1] const arch = parts[2] - if (os !== "darwin" && os !== "linux" && os !== "win32") return - if (!arch || !RipgrepAsset.descriptor.targets.some((target) => target.os === os && target.arch === arch)) return + if (os !== "darwin" && os !== "linux" && os !== "win32") return undefined + if (!arch || !RipgrepAsset.descriptor.targets.some((target) => target.os === os && target.arch === arch)) { + return undefined + } return { os, arch } } From 8a2fe91bbddc826831b5926ef135b4826929de6d Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:49:00 +0800 Subject: [PATCH 11/11] fix(dag): make YAML authoring explicit --- .../plugin/command/orchestration-policy.md | 22 +++-- .../src/plugin/command/workflow-blocks.md | 80 ++++++++++++++++++- packages/core/src/plugin/command/workflow.md | 22 ++--- packages/core/test/plugin/command.test.ts | 49 +++++++++--- .../test/dag/workflow-authoring.test.ts | 30 +++++++ 5 files changed, 165 insertions(+), 38 deletions(-) diff --git a/packages/core/src/plugin/command/orchestration-policy.md b/packages/core/src/plugin/command/orchestration-policy.md index b51985896f..f0c557f52f 100644 --- a/packages/core/src/plugin/command/orchestration-policy.md +++ b/packages/core/src/plugin/command/orchestration-policy.md @@ -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 @@ -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 diff --git a/packages/core/src/plugin/command/workflow-blocks.md b/packages/core/src/plugin/command/workflow-blocks.md index 0e06745d42..59e29148ce 100644 --- a/packages/core/src/plugin/command/workflow-blocks.md +++ b/packages/core/src/plugin/command/workflow-blocks.md @@ -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. @@ -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. diff --git a/packages/core/src/plugin/command/workflow.md b/packages/core/src/plugin/command/workflow.md index c802c0a880..4429b37c7d 100644 --- a/packages/core/src/plugin/command/workflow.md +++ b/packages/core/src/plugin/command/workflow.md @@ -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. @@ -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 @@ -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. diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index 328c5424c4..63627074e7 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -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") @@ -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", ) @@ -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`") @@ -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( diff --git a/packages/opencode/test/dag/workflow-authoring.test.ts b/packages/opencode/test/dag/workflow-authoring.test.ts index 1df8901e03..0adb983a39 100644 --- a/packages/opencode/test/dag/workflow-authoring.test.ts +++ b/packages/opencode/test/dag/workflow-authoring.test.ts @@ -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" @@ -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()