From ddfa2e1fc8a4554a32807f689c83bcd454a57d74 Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 19 Aug 2026 19:38:44 +0800 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20post-review=20cleanup=20=E2=80=94?= =?UTF-8?q?=20docs=20drift,=20CONTEXT.md=20section=20order,=20review=20nit?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-axis review of today's 62-commit range (both axes PASS, info findings): - AGENTS.md gate prose/table updated to Typecheck + Unit Tests (linux) (#370 drift) - CONTEXT.md: Conventions section moved after the full Invariants list — the #348 edit had demoted 5 original Invariants (incl. the engine-enforced gated-checkpoint obligation) under the new header - memory-search: identical if-branches folded back to || - blocks.ts: BLK-02 collision check extracted from an IIFE into aggregatorEvidenceMapping per the Style Guide - #340's serve-mode behavioral e2e documented as a covered-by-composition delta (e2e-loop behavior tests + the wiring probe for the failure class) Closes #374 --- .specgit.yaml | 7 ++- AGENTS.md | 8 ++-- packages/opencode/src/dag/CONTEXT.md | 8 ++-- packages/opencode/src/dag/blocks.ts | 52 +++++++++++---------- packages/opencode/src/tool/memory-search.ts | 6 +-- 5 files changed, 40 insertions(+), 41 deletions(-) diff --git a/.specgit.yaml b/.specgit.yaml index e33e7aaef..e1eb8e907 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -1,8 +1,7 @@ version: 1 -delivery: issue348 +delivery: issue375 context: kind: branch - branch: feat/348-issue348 + branch: feat/375-issue375 issues: - - 348 -pr: 373 + - 375 diff --git a/AGENTS.md b/AGENTS.md index 517450225..a8dd70ee6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,22 +4,22 @@ ## Git Workflow (铁律) ``` -feat/**, fix/** ──PR(Typecheck 门禁)──▶ dev ──push 触发全量测试──▶ +feat/**, fix/** ──PR(Typecheck + Unit Tests 门禁)──▶ dev ──push 触发全量测试──▶ dev ──手动 release-fork──▶ prerelease 测试版 dev ──PR(全量测试门禁)──▶ main ──手动 release-fork──▶ 正式版 ``` -**分层门禁**:`dev` 是快速集成层(仅 Typecheck),`main` 是正式质量门禁(Typecheck + 全量 Unit Tests + E2E)。所有改动通过 PR 流转,禁止直推 `main` 和 `dev`(由 GitHub Rulesets 强制)。 +**分层门禁**:`dev` 是快速集成层(Typecheck + Unit Tests (linux);E2E 不阻塞),`main` 是正式质量门禁(Typecheck + 全量 Unit Tests + E2E)。所有改动通过 PR 流转,禁止直推 `main` 和 `dev`(由 GitHub Rulesets 强制)。 | Branch | 直推 | PR 门禁 | CI 触发 | Purpose | |--------|------|---------|---------|---------| | `{type}/**` | ✅ 允许 | — | ❌ 不跑 | 开发分支,频繁变更 | -| `dev` | ❌ 禁止 | PR 必须通过 **Typecheck** | ✅ push 触发 Typecheck + 全量测试 | 快速集成层 | +| `dev` | ❌ 禁止 | PR 必须通过 **Typecheck + Unit Tests (linux)** | ✅ push 触发 Typecheck + 全量测试 | 快速集成层 | | `main` | ❌ 禁止 | PR 必须通过 **Typecheck + Unit Tests + E2E (linux + windows)** | ✅ push 触发全量 | 正式质量门禁 + 发版 | **流程**: 1. 从 `main` 切出 `feat/**` 或 `fix/**` 分支开发 -2. PR → `dev`(Typecheck 门禁,快速合并) +2. PR → `dev`(Typecheck + Unit Tests (linux) 门禁,快速合并) 3. push 到 `dev` 自动触发全量测试验证 4. 从 `dev` 手动 `release-fork` → 产出 **prerelease** 测试版 5. PR `dev` → `main`(全量测试门禁:Typecheck + Unit Tests + E2E) diff --git a/packages/opencode/src/dag/CONTEXT.md b/packages/opencode/src/dag/CONTEXT.md index 7f2824dd3..34f5871da 100644 --- a/packages/opencode/src/dag/CONTEXT.md +++ b/packages/opencode/src/dag/CONTEXT.md @@ -27,16 +27,16 @@ Workflow Orchestration turns one user objective into one durable DAG. Its model- - Parsing, file-only compatibility, strict action decoding, Block compilation, and profile diagnostics are not reimplemented by callers. - `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. - -## Conventions - -- One user objective has at most one live DAG; route expansion stays inside that DAG (issue #348: a modeling convention enforced by orchestrator guidance — workflow-routing and orchestration-policy — not by the engine; `dag.create` and the workflow tool's start accept a session with a live workflow. The runtime tolerates the violation with bounded consequences: the wake model aggregates across workflows and a goal is blocked by any DAG lease. When two live DAGs share one workspace, the plan block's disjoint-write-set discipline does NOT carry across workflows — authors must keep concurrent workflows on disjoint worktrees or serialize them). - 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. - Dependents of a reporting checkpoint must be gated on its output; authoring rejects ungated shapes at start/validate AND at replan/extend fragment actions, and the runtime replan/extend mutation seam re-checks the merged graph (exempting checkpoints already terminal in the durable graph — they are settled and immutable, the spawn-before-verdict race is past; runtime create remains deliberately unchanged). A gated checkpoint must declare `output_schema` (authoring obligation). +## Conventions + +- One user objective has at most one live DAG; route expansion stays inside that DAG (issue #348: a modeling convention enforced by orchestrator guidance — workflow-routing and orchestration-policy — not by the engine; `dag.create` and the workflow tool's start accept a session with a live workflow. The runtime tolerates the violation with bounded consequences: the wake model aggregates across workflows and a goal is blocked by any DAG lease. When two live DAGs share one workspace, the plan block's disjoint-write-set discipline does NOT carry across workflows — authors must keep concurrent workflows on disjoint worktrees or serialize them). + ## Boundaries - `WorkflowAuthoring` owns source interpretation and authoring diagnostics. diff --git a/packages/opencode/src/dag/blocks.ts b/packages/opencode/src/dag/blocks.ts index aa2e1bfb5..fc010a014 100644 --- a/packages/opencode/src/dag/blocks.ts +++ b/packages/opencode/src/dag/blocks.ts @@ -262,30 +262,7 @@ function compileBlock( contract: AGGREGATOR_CONTRACT, required: true, reportToParent: false, - inputMapping: Object.fromEntries( - (() => { - // #349/BLK-02: writer ids may mix hyphens and underscores - // ("foo-bar" vs "foo_bar") whose -→_ normalization collides on - // the same mapping key — Object.fromEntries would silently drop - // one writer's evidence (and its files escape the aggregator's - // overlap detection). Reject the shape at compile time. - const seen = new Map() - for (const writerID of aggregation.writerIDs) { - const key = writerID.replace(/-/g, "_") - const prior = seen.get(key) - if (prior !== undefined) { - throw new Error( - `Parallel implementation writers "${prior}" and "${writerID}" normalize to the same input-mapping key "${key}" — their aggregator evidence keys would collide. Rename one of the writers so the ids differ beyond hyphens vs underscores`, - ) - } - seen.set(key, writerID) - } - return aggregation.writerIDs.flatMap((writerID: string) => [ - [`${writerID.replace(/-/g, "_")}_changed_files`, `${writerID}.output.changed_files`], - [`${writerID.replace(/-/g, "_")}_summary`, `${writerID}.output.summary`], - ]) - })(), - ), + inputMapping: aggregatorEvidenceMapping(aggregation.writerIDs), outputSchema: IMPLEMENTATION_SCHEMA, }), ...lanes, @@ -449,6 +426,33 @@ interface WriterAggregation { verificationID: string } +/** + * The aggregator's per-writer evidence mapping. #349/BLK-02: writer ids may + * mix hyphens and underscores ("foo-bar" vs "foo_bar") whose -→_ normalization + * collides on the same mapping key — Object.fromEntries would silently drop + * one writer's evidence (and its files escape the aggregator's overlap + * detection), so the shape is rejected at compile time. + */ +function aggregatorEvidenceMapping(writerIDs: string[]): Record { + const seen = new Map() + for (const writerID of writerIDs) { + const key = writerID.replace(/-/g, "_") + const prior = seen.get(key) + if (prior !== undefined) { + throw new Error( + `Parallel implementation writers "${prior}" and "${writerID}" normalize to the same input-mapping key "${key}" — their aggregator evidence keys would collide. Rename one of the writers so the ids differ beyond hyphens vs underscores`, + ) + } + seen.set(key, writerID) + } + return Object.fromEntries( + writerIDs.flatMap((writerID: string) => [ + [`${writerID.replace(/-/g, "_")}_changed_files`, `${writerID}.output.changed_files`], + [`${writerID.replace(/-/g, "_")}_summary`, `${writerID}.output.summary`], + ]), + ) +} + function aggregateParallelWriters(blocks: WorkflowBlock[]) { const aggregations = new Map() for (const block of blocks) { diff --git a/packages/opencode/src/tool/memory-search.ts b/packages/opencode/src/tool/memory-search.ts index c2af0e33f..4574f0cf2 100644 --- a/packages/opencode/src/tool/memory-search.ts +++ b/packages/opencode/src/tool/memory-search.ts @@ -33,11 +33,7 @@ export const MemorySearchTool = Tool.define( const memory = Option.getOrUndefined(yield* Effect.serviceOption(Memory.Service)) const sessions = Option.getOrUndefined(yield* Effect.serviceOption(Session.Service)) - // #350: when the service exists but Memory is inert, say why instead - // of a bare "unavailable" — the reason tells the user what to do - // (e.g. run /init, then /memory on). - if (!memory) return unavailable() - if (!sessions) return unavailable() + if (!memory || !sessions) return unavailable() const current = yield* sessions.get(ctx.sessionID).pipe(Effect.option) if (Option.isNone(current) || current.value.parentID) return unavailable() From ecdc300ecd6b20ad70ccf564c2575ebca169b739 Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 19 Aug 2026 19:39:22 +0800 Subject: [PATCH 2/4] chore(specgit): record PR binding in delivery record --- .specgit.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.specgit.yaml b/.specgit.yaml index e1eb8e907..ca77f6b0d 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -5,3 +5,4 @@ context: branch: feat/375-issue375 issues: - 375 +pr: 375 From 319637e1c175fd858de9359ca3acb2bea3935cde Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 19 Aug 2026 20:09:52 +0800 Subject: [PATCH 3/4] chore(specgit): bind delivery to issue 374 (was mistakenly bound to the PR number) --- .specgit.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.specgit.yaml b/.specgit.yaml index ca77f6b0d..8131bc0b0 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -1,8 +1,7 @@ version: 1 -delivery: issue375 +delivery: issue374 context: kind: branch - branch: feat/375-issue375 + branch: feat/374-issue374 issues: - - 375 -pr: 375 + - 374 From 583b1d1c9c04f4ccdb00c8183de285a111e5ac32 Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 19 Aug 2026 20:10:44 +0800 Subject: [PATCH 4/4] chore(specgit): correct delivery record context (branch + PR binding) --- .specgit.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.specgit.yaml b/.specgit.yaml index 8131bc0b0..300153e43 100644 --- a/.specgit.yaml +++ b/.specgit.yaml @@ -2,6 +2,7 @@ version: 1 delivery: issue374 context: kind: branch - branch: feat/374-issue374 + branch: feat/375-issue375 issues: - 374 +pr: 375