From 0ad03a6eebc61d3c0559610fbacd22989b47b21f Mon Sep 17 00:00:00 2001 From: Matt Jackson Date: Sun, 8 Nov 2026 20:17:00 +0000 Subject: [PATCH 1/7] docs: plan proportionate OpenCode 2.2 adaptation Prepared by Snarkimedes under owner acceptance; preserves original Claude REVISE and corrected-plan READY evidence. Planning only; implementation and publication remain separately gated. --- docs/reviews/claude-opencode-22-plan-29k.md | 41 ++++ .../claude-opencode-22-plan-recheck-29k.md | 34 +++ .../changes/adapt-opencode-2-2/.openspec.yaml | 5 + openspec/changes/adapt-opencode-2-2/design.md | 53 +++++ .../changes/adapt-opencode-2-2/proposal.md | 31 +++ .../specs/work-continuity/spec.md | 30 +++ .../specs/workflow-access/spec.md | 222 ++++++++++++++++++ openspec/changes/adapt-opencode-2-2/tasks.md | 20 ++ 8 files changed, 436 insertions(+) create mode 100644 docs/reviews/claude-opencode-22-plan-29k.md create mode 100644 docs/reviews/claude-opencode-22-plan-recheck-29k.md create mode 100644 openspec/changes/adapt-opencode-2-2/.openspec.yaml create mode 100644 openspec/changes/adapt-opencode-2-2/design.md create mode 100644 openspec/changes/adapt-opencode-2-2/proposal.md create mode 100644 openspec/changes/adapt-opencode-2-2/specs/work-continuity/spec.md create mode 100644 openspec/changes/adapt-opencode-2-2/specs/workflow-access/spec.md create mode 100644 openspec/changes/adapt-opencode-2-2/tasks.md diff --git a/docs/reviews/claude-opencode-22-plan-29k.md b/docs/reviews/claude-opencode-22-plan-29k.md new file mode 100644 index 0000000..2bcc2e3 --- /dev/null +++ b/docs/reviews/claude-opencode-22-plan-29k.md @@ -0,0 +1,41 @@ +**Verdict: REVISE.** The plan needs one narrow correction plus an owner decision. Everything else is ready. + +## Standards findings +- No blocking issues. The plan stays planning-only. It maps to one implementation Bead (`tasks.md:3`), keeps actor, permission and Stop contracts unchanged, keeps Beads-owned behavior out of scope, and keeps publication and operator activation as separate gates (`design.md:47`, `tasks.md:19`). This matches `AGENTS.md`. +- Nit, not blocking: `design.md:5` points to `/opt/data/research/...`, which is outside the repo. AGENTS.md says committed docs own evidence, so cite the committed evidence location at acceptance. + +## Spec/Design findings + +**B1 (blocker): a model change after native compaction will reliably lose snapshots, but the plan describes this as an "opaque older window" case.** +- **Upstream evidence:** + - `ctx.session.context` → `store.context` → `SessionHistory.load(db, id, "latest")` (`store.ts:194`). + - With `"latest"`, `latestCompaction` stops at the newest completed compaction of any kind, including native (`history.ts:70`, `replayable` at `:49`). + - The runner reads history with `model.ref` instead (`llm.ts:137`). If a native window can't replay on the new model, it reads back from the previous compaction (`history.ts:220-223`). +- **Result:** after a native compaction on model A, a switch to model B sends the ordinary, durable, ID-bearing tagged user messages from before that compaction (`to-llm-message.ts:241-254`). `context()` does not return those records, so the planned lookup marks them "unavailable". These messages are plain retained transcript, not opaque windows. The plugin `SessionDomain` has no other history reader: it has `context` but not `messages` or `message.get` (`plugin/dist/promise/session.d.ts:186`). +- **Where the plan gets it wrong:** + - `work-continuity/spec.md:30` and `design.md:19` frame disclosure as limited to "opaque older windows". + - `workflow-access/spec.md:162` ("accessible durable snapshot") leaves "accessible" undefined. + - `tasks.md:13` asks qualification to cover "a model-change boundary" but gives no expected result. A predictable degradation could therefore be recorded as an adapter failure, or quietly passed. +- **Smallest correction:** + - State in `design.md:19` and `work-continuity/spec.md:30` that "accessible" means present in the native latest-boundary context. + - Name the case explicitly: snapshots from before a native compaction, replayed after a model change. + - Set the expected qualification outcome for that case to "disclosed unavailable". + - Have the owner accept that degradation (see decisions). No new store or reader is needed. + +## Material nonblocking notes +1. **What happens if the read fails is unspecified.** `load` decodes every row and fails the whole call on any `MessageDecodeError` (`history.ts:215-218`). Spec `:199-203` covers this in general. Add one line to `design.md:17`: if the context read rejects, treat all tagged messages in that request as unavailable (with the notice), and still deliver the request and the rest of the guidance. +2. **Order of steps.** `design.md:17` puts the location check before the lookup, but beta.5 currently expands before the location check (`src/host.ts:339-343`). Task 1.1 should mention this reordering so the regression test fixes it in place. +3. **The direct-tool reasoning is consistent with upstream.** When `execute` is wholly denied, eligible leaves become direct tools (`tool.ts:260-264`), and per-leaf wholly-disabled filtering still applies (`:257`). Spec `:33-36` and `:103-107` match this. The capability check in beta.5 (`host.ts:356`) is the right seam to change. +4. **ID matching depends on a published type.** `SessionRequest.messages: Array` (`session.d.ts:30`) has to expose `id` to the plugin. Task 1.1's SDK typecheck should assert this, not cast it. + +## Consequential owner decisions +- Accept the B1 degradation: after a native compaction followed by a model switch, pre-compaction workflow snapshots show an unavailable-guidance notice rather than being expanded. The alternative is to reopen the rejected side-store choice: `ctx.storage` exists, but `design.md:21` rejects durable side stores. I recommend accepting the degradation and documenting it. + +## Evidence +- **Inspected:** + - The six candidate files and `AGENTS.md`. + - Beta.5 `src/host.ts:226-410`. + - Upstream `history.ts` (load/boundary), `store.ts:194`, the native-session context path (`:446-448`), `host.ts:609`, `to-llm-message.ts:226-280`, `llm.ts` boundary usage and `tool.ts:251-320`. + - Plugin SDK `session.d.ts` (SessionDomain, SessionRequest), the schema user `metadata` field, and report greps. +- **Not inspected:** the canonical `openspec/specs/*`, the candidate-manifest hashes and the compaction-hook request construction. +- **Not executed:** any typecheck, test, OpenSpec validation, SDK fixture or installed-host run. That compaction hooks keep message IDs, and that the `id` field is visible in the published types, is inferred from source, not verified. diff --git a/docs/reviews/claude-opencode-22-plan-recheck-29k.md b/docs/reviews/claude-opencode-22-plan-recheck-29k.md new file mode 100644 index 0000000..a6d95b8 --- /dev/null +++ b/docs/reviews/claude-opencode-22-plan-recheck-29k.md @@ -0,0 +1,34 @@ +**Verdict: READY** for owner review of the corrected plan. The original REVISE stands as history. READY only means the plan is coherent. It does not show runtime behavior, does not replace the still-unrun 2.2.6 native/model qualification, and is not owner acceptance or permission to implement, release or close `opencode-beads-o66`. + +## B1: resolved +- **"Accessible" is now defined.** `design.md` defines it as "present in native latest-compaction context". It names the exact trigger: native compaction on model A, then a switch to model B that can't replay A's window, which restores plain older user messages to the request. It links the pinned boundary code (`history.ts#L45-L84`). This matches what I checked earlier (`history.ts:49,70,220-223`; `llm.ts:137`). +- **The expected result is now set.** The new scenario at `workflow-access/spec.md:205-209` and `work-continuity/spec.md:30` both expect a visible "unavailable" disclosure for affected historical instructions and reports. Accessible snapshots still expand. Host replay content and Beads data are not rewritten. +- **Qualification can now judge it.** Task 2.2 records this case as a "verified limitation", not a pass or a failure. The stop condition in `design.md` excludes this documented outcome, so it no longer triggers a design revision. +- **The owner's decision is recorded.** The design states that the owner chose disclosure plus a follow-up over a separate snapshot store. + +## Standards findings +- No blockers. +- The machine-local evidence path is gone, and the design now links pinned upstream sources instead. +- The follow-up is one Bead, separate from this change and not blocking it: `deficiency-record.json` shows it blocked, unassigned, `blocks_release:false`, `implementation_authorized:false`, and linked by `discovered-from` to `9za`. Its closure requires a released supported API plus a demonstrated recovery, not just an issue closure or PR merge. That fits the AGENTS.md split between OpenSpec, docs and Beads. +- Nothing creates a duplicate GitHub issue, a side store or a polling service. + +## Spec/Design findings +No correction introduced a blocker. +- **Nonblocking wording nit:** `workflow-access/spec.md:222` ("Scenario: User evaluates the documented limitation", last line) says "the published beta's narrower host support". Beta.5 supports a *different* host line (2.1), not a narrower one. Suggested wording: "the published beta's 2.1 host support". +- **Earlier nonblocking notes, now folded in:** + - What happens when the history read fails: disclose unavailable, leave the content alone, keep the other guidance, don't retry. + - Moving the location check before the lookup. + - Checking message IDs through published types, without casts. + - Task 1.1 now owns these and tests them. + - The proposal now names the native current-context API correctly. +- **The ADDED requirement is reasonable.** "Documented snapshot-recovery deficiency" makes README disclosure a requirement in the spec. The design already says docs and specs get updated when the deficiency is resolved, so it won't harden into a permanent rule. +- **The controlled-provider fixture should work** for the model-switch case. The compaction hook result can supply `native.messages` (`session.d.ts:42-43`), so a native window for model A can be produced without a second run against a real provider. This is inferred from the types, not executed. + +## Remaining owner decisions +- Accepting the complete corrected package, including the documented deficiency as a known limitation of the 2.0 adaptation. Your direction already says you'll take this disclosure-and-follow-up approach; formal acceptance is still pending. +- No other new decisions. + +## Evidence +- **Inspected:** `correction.patch`, `deficiency-record.json`, and a heading/grep check of the current `workflow-access` spec. The working tree matches the patched heading structure, including the scenario at :205 and the ADDED requirement at :211-222. +- **Not re-inspected, by your scoping:** upstream sources beyond my earlier review, canonical specs, the hashes in `corrected-manifest.json`, and `review.md`. +- **Not executed by me:** OpenSpec validation, typecheck, tests, the SDK fixtures, and the installed-host/model/compaction qualification. The validation and hash checks you cite are your own runs. diff --git a/openspec/changes/adapt-opencode-2-2/.openspec.yaml b/openspec/changes/adapt-opencode-2-2/.openspec.yaml new file mode 100644 index 0000000..c52689e --- /dev/null +++ b/openspec/changes/adapt-opencode-2-2/.openspec.yaml @@ -0,0 +1,5 @@ +schema: spec-driven +created: 2026-11-08 +goal: Adapt compact workflow transport and native tool capability guidance to + OpenCode2.2.6 while preserving scoped behavior; prepare targeted qualification + and stable delivery without implementation or publication. diff --git a/openspec/changes/adapt-opencode-2-2/design.md b/openspec/changes/adapt-opencode-2-2/design.md new file mode 100644 index 0000000..2608512 --- /dev/null +++ b/openspec/changes/adapt-opencode-2-2/design.md @@ -0,0 +1,53 @@ +# Design + +## Context + +The [proposal](proposal.md) changes two concrete host seams. It reuses assessment `opencode-beads-tzq` and independent Sol/Astra findings; decisive source evidence and its limits are summarized here so review does not depend on machine-local artifact paths. The planning base is `f8db6a3e9c5b1804e18adb44bf6ba98b0ace1cb5`; its only runtime-assessment successor change is repository-local configuration. + +Pinned OpenCode comparison: 2.1.3 `bb25a2f4a3f3f04d7c344bec0c0574ea3510b3b1` → 2.2.6 `dc2ffcf8bf26f9e669b6aa22b95f50e12be15e06`. The existing SDK fixtures pass after two test-only AI API updates, but bypass the broken history projection. They are not host qualification. + +## Decisions + +### Keep durable snapshots; resolve them by native message identity + +Continue writing version-1 `ocBeadsWorkflow` metadata through one ordinary prompt. During primary/compaction preparation, read `ctx.session.context({ sessionID })` once and match its durable user records to request user-message IDs. Expand only validated package snapshots into transient request copies; retain existing generalized-over-legacy precedence. Never reconstruct past instructions from current command definitions or mutate stored history. + +Use the actual plugin API, not the wider standalone client: published SessionDomain includes `context`, but does **not** expose `session.message.get`. Its context result contains durable SessionMessage records, including user metadata. The [host binding](https://github.com/anomalyco/opencode/blob/dc2ffcf8bf26f9e669b6aa22b95f50e12be15e06/packages/core/src/plugin/host.ts#L609) delegates to a read-only store-context load, not another model preparation. [User lowering](https://github.com/anomalyco/opencode/blob/dc2ffcf8bf26f9e669b6aa22b95f50e12be15e06/packages/core/src/session/runner/to-llm-message.ts#L241-L254) retains the ID. Verify this whole chain on the installed host. + +Use a request-local lookup only: one native current-context read, no per-message RPC loop, long-lived cache, pagination or full transcript scan. Move the existing location check ahead of snapshot lookup; check generation/cancellation afterward. Match only IDs exposed by published request types, without casts inventing an ID contract. Ordinary unmarked history remains ordinary input. Unresolvable workflow snapshots produce an explicit unavailable-guidance notice, not invented instructions or another prompt; recognizing a compact invocation for that diagnostic does not reconstruct its authority. If the read rejects, disclose unavailable snapshot guidance for identifiable workflow invocations, leave request content untouched and continue other applicable guidance rather than retrying admission or failing unrelated input. + +Here, **accessible** means a durable user record present in the native latest-compaction context. After native compaction on model A, switching to model B that cannot replay A's native window can restore ordinary pre-compaction user messages to B's request, while this reader omits their stored snapshots. This is not restricted to opaque legacy windows. Preserve host replay content, expand accessible snapshots, and explicitly disclose unavailable instructions/reports for the affected historical commands. The expected qualification outcome for that precise case is **disclosed unavailable**, not restored snapshots or a silently passing restoration check. See the pinned [history boundary selection](https://github.com/anomalyco/opencode/blob/dc2ffcf8bf26f9e669b6aa22b95f50e12be15e06/packages/core/src/session/history.ts#L45-L84). + +Treat this as an **unresolved plugin deficiency with an upstream blocker**, not a solved upstream quirk. The owner selected explicit disclosure and follow-up rather than parallel storage. Task 1.1 must add README coverage of trigger, missing historical instruction/diagnostic context, unaffected Beads data and upstream [#57424](https://github.com/anomalyco/opencode/issues/57424). `opencode-beads-o66` remains blocked and unassigned; it does not gate this adaptation or stable release and cannot close merely because the limitation is accepted or an upstream PR merges. Revisit a released supported API, verify recovery, then update docs/specs. Prior [PR #49568](https://github.com/anomalyco/opencode/pull/49568) closed unmerged. No duplicate GitHub issue, polling service, backfill or side store is selected. + +Alternatives rejected: casting metadata cannot restore dropped data; generated visible instructions regress the accepted UX; synthetic inputs, pending queues and durable side stores duplicate host lifecycle. Native keyed instruction entries are promising for stable context, but session-wide state is not a drop-in historical invocation snapshot and is unnecessary for this repair. Keep current guarded prime injection. + +### Follow native tool availability + +Retain the eleven definitions and their shell-equivalent permissions. Derive guidance from the actual request catalog: prefer native composition with `execute`; otherwise use exposed direct Beads leaves; use direct shell for supported advanced/fallback work only when independently available. Registration alone proves neither request visibility nor permission. Do not expose shell inside code mode or force direct registration to emulate host fallback. + +The [2.2.6 snapshot](https://github.com/anomalyco/opencode/blob/dc2ffcf8bf26f9e669b6aa22b95f50e12be15e06/packages/core/src/tool.ts#L251-L318) supplies this fallback while retaining leaf permission filtering and actual invocation identity. + +### One target, bounded acceptance + +Use exact SDK/host 2.2.6 and proposed admission `>=2.2.6 <2.3`; Beads minimum remains 1.3.1. No 2.1 adapter or minimum/current host matrix. Update the removed AI Promise subpath and flattened `generate` API in tests. A projection-boundary regression must fail against beta.5 before the repair; hand-built metadata-bearing hook fixtures alone are insufficient. + +Use normal cheap repository gates and one frozen installed candidate in an isolated host scenario set. Cover compact action/explanation and diagnostics, retained legacy/current history, primary/compaction versus title/generate, composed/direct/denied tools, actual worker identity and Stop/unload. Reuse unchanged command-purpose and argv coverage rather than replaying all 30 workflows with a model. Host-owned summary/native retention must be exercised; do not demand verbatim summary retention or universal whole-step termination after a declined inner operation. + +## Edit boundary and stop conditions + +Expected production edits: `src/workflows.ts`, `src/host.ts`, `src/guidance.ts`, with a small adjacent context/type adjustment only if required. Existing tests/package smoke, manifest/admission declarations and current compatibility/install docs change with those seams. `src/continuity.ts`, guard/database behavior, agent prompts and upstream monitoring are preservation/verification surfaces, not redesign targets. + +Stop and revise the design if IDs present in the native latest-context read cannot resolve their snapshots, or the repair needs a new store, scheduler, upstream patch or transport interception. The specifically documented model-switch deficiency has the disclosed-unavailable outcome above, not a restoration guarantee. An unavailable provider/isolated runtime leaves qualification pending; it is not permission for auth/database copying, operator changes or another infrastructure project. + +## Risks and migration + +- Missing or stale history → disclose unavailable snapshots; never replay operations or recover authority by guessing. +- Lookup overhead → one read of native current context and a request-local ID map; no persistent cache or benchmark campaign. +- Native permission/compaction behavior differs → preserve original observations and record actual target outcomes, distinguishing host limitations from adapter failures. + +No Beads data migration, config rewrite or historical-message backfill is needed. Preserve existing exact beta/stable pins; a separately approved plugin 2.0.0 release can move `latest` after qualification and explain the new host floor. Existing installations are not upgraded by this work. Source acceptance includes canonical spec synchronization/archive; publication and operator activation remain separate gates. + +## Tooling + +Installed OpenSpec is 1.16.0; latest released 1.16.1 fixes malformed/closed-heading validation and version checking. This package uses ordinary recognized headings, so no tooling upgrade is required to draft it. Do not upgrade the CLI or regenerate repository integrations as part of this adaptation. Report inherited strict-validation warnings separately from new errors. diff --git a/openspec/changes/adapt-opencode-2-2/proposal.md b/openspec/changes/adapt-opencode-2-2/proposal.md new file mode 100644 index 0000000..1f16661 --- /dev/null +++ b/openspec/changes/adapt-opencode-2-2/proposal.md @@ -0,0 +1,31 @@ +# Adapt to OpenCode 2.2 + +## Why + +OpenCode 2.2.6 no longer forwards stored user metadata into model-request messages, breaking beta.5's compact workflow expansion if its version guard is widened. Native direct-tool fallback also makes its capability guidance incomplete. Repair these concrete seams without redesigning the integration. + +## What Changes + +- **BREAKING:** select OpenCode 2.2.6 as the qualification target and `>=2.2.6 <2.3` as the proposed admission window; retire 2.1 support rather than add a compatibility layer. +- Preserve compact invocations and stored instruction snapshots; resolve snapshots through the released native current-context API instead of relying on model-message metadata. +- Describe the actual request's composed, direct-typed and shell capabilities, retaining existing permissions and actor identity. +- Document snapshot recovery after native compaction/model switching as an unresolved plugin deficiency with upstream API blocker [#57424](https://github.com/anomalyco/opencode/issues/57424), tracked in `opencode-beads-o66`. Disclose it in the README; accepting it for a release does not mean fixing it. +- Update SDK/test APIs and add coverage of history-to-request lowering, followed by representative installed-host qualification. +- Prepare for stable plugin 2.0.0 with clear migration guidance and existing version pins left available. Publication and operator upgrades remain separately authorized. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `workflow-access`: host admission, durable snapshot expansion, diagnostics, user-visible deficiency disclosure and permission-aware native tool availability. +- `work-continuity`: describe retained summary/native replay against the selected host contract rather than 2.1-specific field names. + +## Impact + +Expected edits are in workflow/context wiring and capability guidance, their tests, SDK/admission declarations and current installation documentation. Keep all 30 commands, eleven typed operations, command semantics, resources, agents, attribution, cancellation and upstream-owned Beads behavior. + +Excluded: scheduled source monitoring (`opencode-beads-rou`), changes to Beads minimum 1.3.1, new operations, automatic checkpoints/continuation, persistent queues, host/provider patches, GUI work, and wholesale adoption of instruction-entry or idle APIs. This package authorizes planning only; no implementation, release or deployment. diff --git a/openspec/changes/adapt-opencode-2-2/specs/work-continuity/spec.md b/openspec/changes/adapt-opencode-2-2/specs/work-continuity/spec.md new file mode 100644 index 0000000..077ca7a --- /dev/null +++ b/openspec/changes/adapt-opencode-2-2/specs/work-continuity/spec.md @@ -0,0 +1,30 @@ +## MODIFIED Requirements + +### Requirement: Native trigger and semantic responsibility separation + +The selected integration SHALL distinguish awaited interception from observational events and semantic judgment from transport success. It SHALL document the selected native mechanism for each continuity behavior, preserve failed or uncertain persistence status, and avoid indiscriminate conversion of tool results or transcripts into durable memory. + +#### Scenario: Compaction starts with unsaved meaning + +- **WHEN** compaction occurs before a semantic checkpoint has been prepared +- **THEN** summary guidance does not claim that durable task context was saved +- **AND** an event subscriber is not treated as a compaction barrier or a guaranteed final-write mechanism + +#### Scenario: Compaction carries known references and gaps + +- **WHEN** the awaited compaction hook prepares summary or native-provider context +- **THEN** it carries acknowledged durable references and known unsaved or uncertain status without initiating persistence +- **AND** it does not wait for a same-session semantic checkpoint, queued write or permission escalation + +#### Scenario: User stops execution + +- **WHEN** an execution interruption or permission wait is observed +- **THEN** the integration does not automatically restart work or approve the blocked operation to manufacture a final checkpoint +- **AND** recovery uses earlier durable checkpoints and reports known gaps + +#### Scenario: Host retains and replays compacted history + +- **WHEN** summary/native history is recovered or forked on the selected 2.2 host, or its provider/model changes +- **THEN** the integration preserves host-owned native replay and summary retention without substituting a provider replacement window for a summary +- **AND** tool execution state and UI metadata are not treated as renamed provider replay payloads +- **AND** workflow snapshots present in native latest-compaction context follow workflow-access expansion; earlier ordinary user messages restored after a non-replayable native-window model switch receive explicit unavailable-guidance disclosure when their snapshots are absent from that context diff --git a/openspec/changes/adapt-opencode-2-2/specs/workflow-access/spec.md b/openspec/changes/adapt-opencode-2-2/specs/workflow-access/spec.md new file mode 100644 index 0000000..795f6dd --- /dev/null +++ b/openspec/changes/adapt-opencode-2-2/specs/workflow-access/spec.md @@ -0,0 +1,222 @@ +## MODIFIED Requirements + +### Requirement: Explicit native availability and fallback + +The integration SHALL target exact OpenCode 2.2.6 qualification and `>=2.2.6 <2.3` admission. It SHALL distinguish admission from measured compatibility, preserve existing version pins and refuse unsupported host registration with diagnostics. It SHALL NOT retain a 2.1 compatibility layer or silently migrate installations. Publication remains separately authorized. + +#### Scenario: A different host version is considered + +- **WHEN** a future admitted patch or a version outside the declared range is considered +- **THEN** an admitted patch is not described as tested without measured evidence, and an out-of-range host remains unsupported pending explicit contract review + +#### Scenario: Host cannot satisfy N1 + +- **WHEN** the host lacks the selected permission or lifecycle contract +- **THEN** the integration reports the unsupported path and does not enable privileged fallback execution or silently weaken guarantees + +#### Scenario: Experimental capability is considered + +- **WHEN** a released experimental mechanism could improve task flow or recovery +- **THEN** the packet records its semantics, qualification, maintenance and fallback consequences before adoption +- **AND** instability alone is neither approval nor automatic exclusion + +#### Scenario: Plugin reloads or a session changes location + +- **WHEN** registrations are unloaded or a session moves, forks or is deleted +- **THEN** the selected integration disposes or re-resolves affected state and observers without leaking task context across sessions or locations +- **AND** it preserves user overrides and does not claim cleanup guarantees a final durable write + +### Requirement: Restricted-agent typed operation boundary + +Typed tools and direct shell fallback SHALL respect shell visibility and operation-specific permissions using actual invocation identity, canonical resources and narrow saved grants. Guard holds, location, attribution and cancellation SHALL be rechecked after approval and before execution. Beads owns database/routing/consistency semantics; normal CLI equivalence does not promise filesystem confinement. + +#### Scenario: Agent wholly denies shell + +- **WHEN** a restricted agent's effective rules wholly deny shell +- **THEN** typed tools are absent from both its native code-mode inventory and direct-tool fallback, not merely denied after selection; native shell is never callable inside execute + +#### Scenario: Invoking agent differs from session default + +- **WHEN** a typed operation is called by an agent with more restrictive rules than the session's default agent +- **THEN** permission evaluation uses the invoking agent and does not broaden saved approvals to unrelated bd operations + +#### Scenario: Saved approval remains narrow + +- **WHEN** a typed operation requests approval with `save: []` +- **THEN** even an “always” response installs no new saved grant and guidance explains possible repeat prompts +- **AND** existing shell `bd *` grants may authorize equivalent typed operations, while configured denies retain precedence + +#### Scenario: User corrects or declines a typed operation + +- **WHEN** the host permission assertion returns user correction with feedback or explicit decline +- **THEN** the adapter propagates the assertion rejection unchanged and preserves correction feedback without executing the rejected target operation +- **AND** it does not acknowledge target success or automatically retry the operation +- **AND** the host owns outer code-mode decline/continuation behavior; qualification records actual target-host behavior without downstream repair or relabeling historical observations +- **AND** rejection of one operation does not promise whole-step termination; every later operation retains its own permission checks, and explicit Stop/Cancel remains a separate cancellation guarantee + +#### Scenario: External target or held workspace + +- **WHEN** the explicit invocation/location requires external-directory access or the existing guard withholds store access +- **THEN** the operation preserves that restriction and refuses before store-opening execution unless invocation context and the required OpenCode authority are established +- **AND** Beads retains ownership of its internal routing; no final-store attestation or additional internal-route confinement is promised +- **AND** normal intended routing is not a speculative blocker or a prerequisite for upstream routing-disable support or local repair logic +- **AND** unresolved OpenCode permission equivalence prevents the affected execution + +#### Scenario: Agent claims and hands off tracked work + +- **WHEN** a selected workflow writes, claims or renews tracked work, or delegates execution +- **THEN** it supplies the actual executor's explicit actor consistently and verifies audit author, account owner and assignee separately +- **AND** coordination or review does not silently transfer a claim, actor naming does not grant authority, and historical authorship is not rewritten + +### Requirement: Read-only native diagnostics + +The package SHALL expose effective discovery, compatibility, ownership conflicts and context/persistence status through `oc-beads:diagnostics` without opening a withheld store, changing configuration or exposing credentials. Diagnostics SHALL distinguish unavailable, failed, withheld and uncertain outcomes. + +#### Scenario: Discovery collision + +- **WHEN** a user customization prevents an owned entry from taking effect +- **THEN** diagnostics reports effective names/descriptions and detectable overrides while preserving native user-config precedence +- **AND** a description mismatch may identify an override but SHALL NOT be presented as proof of ownership; public owner identity and identical-description overrides remain unobservable and are disclosed +- **AND** command registration status reflects actual registration and lookup outcomes rather than an obsolete blanket hold + +#### Scenario: Integration health and issue overview are requested + +- **WHEN** a user invokes diagnostics or stats +- **THEN** diagnostics uses only the existing safe version/context probes and cached integration state without opening a store, while stats remains an issue-overview workflow using `bd status` (alias `bd stats`) +- **AND** diagnostic safety and observed qualification limits are preserved; the report identifies the loaded plugin package name/version from its own manifest alongside the observed host version, without inferring a unique build fingerprint + +#### Scenario: Compact diagnostic history + +- **WHEN** diagnostics is invoked with optional free text and native attachments/delivery +- **THEN** history shows the compact command and exact free text, while the dynamic report is captured once in a durable invocation snapshot and expanded only through primary/compaction hooks +- **AND** replay preserves the captured observation without regenerating diagnostics, duplicating instructions, mutating persisted input or expanding auxiliary requests + +#### Scenario: Diagnostics is overridden or unloaded + +- **WHEN** a user override is added or removed, or package diagnostics is unloaded while pending +- **THEN** native override precedence, package restoration after override removal and disposal are preserved, user-owned old status remains untouched, and unloaded work submits no late prompt + +### Requirement: Capability-aware actor and context guidance + +The integration SHALL render actual executor identity and request capabilities, deduplicate guidance while retaining authority, and retain full prime pending measurement. It SHALL preserve the original 29 workflow stems and selected advanced/admin workflows, replacing only package-owned status with diagnostics without an alias or user-override migration, with explicit links to 16 resources plus ADR. Durable references and freshness SHALL be operation-aware. + +#### Scenario: Execute is denied + +- **WHEN** execute is wholly denied for the actual request +- **THEN** composition is reported unavailable, while actually exposed direct Beads tools remain discoverable under their own permissions; direct shell is offered only if separately available and not held +- **AND** when composed tools, direct typed tools and shell are unavailable the limitation is reported without bypass or invented tool access + +#### Scenario: Typed and shell writes share an executor + +- **WHEN** a primary or delegated agent alternates typed and direct-shell writes +- **THEN** context and typed results expose the same actor derived from the actual request/invocation session and agent; attribution grants no permission, and permission checks separately use actual context.agent +- **AND** child executors keep their own identities, no model-controlled actor argument exists, and beads-task-agent remains an optional execution profile + +#### Scenario: A write changes cached state + +- **WHEN** a memory write may have landed, external state changes, or recovery needs current knowledge +- **THEN** relevant location context is refreshed or its uncertainty reported +- **AND** issue-write prime refresh is retained until supported prime content is verified independent of issue state +- **AND** acknowledged writes avoid redundant generic recovery cues, while uncertain results preserve targeted readback needs + +#### Scenario: Actor or capabilities change + +- **WHEN** a request changes agent, session or effective capabilities +- **THEN** rendered actor/capability guidance is recomputed without leaking cached identity or promising unobserved permissions +- **AND** deduplication preserves distinct local authority and the required scope, stop, ownership and honest-outcome rules + +#### Scenario: Semantic issue compaction is requested + +- **WHEN** compact guidance is invoked +- **THEN** it identifies semantic `bd admin compact`, distinguishes Dolt-history squashing with `bd compact` and conversation compaction, and clarifies ambiguous operation or authority before action; bare invocation is explanation only +- **AND** linked guidance prefers agent-driven analyze/apply; `--auto` external-AI mode and `--dolt` storage garbage collection each require separate explicit authority; ordinary compact approval selects neither +- **AND** any authorized admin compact invocation supplies explicit `--actor` because its local default is agent +- **AND** guidance discloses the conflicting upstream examples and discard/restore wording without promising recovery or executing administration to validate prose + +#### Scenario: Pre-compaction issue content is retrieved + +- **WHEN** restore guidance is invoked +- **THEN** it identifies `bd restore` as pre-compaction issue-content retrieval, display-only by default, with explicit authority required for `--apply` to write content back +- **AND** it does not imply database backup recovery or guaranteed reversibility + +### Requirement: Compact workflow history with effective instructions + +The integration SHALL present all 29 WORKFLOWS callbacks as compact native user invocations while preserving full effective action/authority instructions, command descriptions and installed guidance. Diagnostics SHALL retain its dynamic observed-report path. + +#### Scenario: Deliberately migrated workflow presentation + +- **WHEN** a user invokes any of the 29 WORKFLOWS callbacks +- **THEN** its visible entry is the compact command invocation with exact user free text rather than generated workflow instructions +- **AND** all 30 names/descriptions, per-command purposes, authority contracts and guidance resources remain unchanged + +#### Scenario: Normal native input admission + +- **WHEN** any WORKFLOWS command is invoked with bare text or literal details, files, agents, skills and queue or steer delivery +- **THEN** exactly one ordinary native prompt preserves those inputs and native prompt-hook edits +- **AND** visible text contains `/oc-beads:` and any exact untruncated literal details, while a durable versioned ocBeadsWorkflow v1 invocation snapshot retains the static instructions and installed resource paths +- **AND** single-line free text follows the command on the same line; multiline free text follows it on a new line, preserving its whitespace without generated headings or typed-argument parsing +- **AND** no synthetic pairing, retry, auxiliary model call or pending-command queue is introduced + +#### Scenario: Request expansion and replay + +- **WHEN** a retained tagged user message whose durable snapshot is present in the native latest-compaction context reaches primary or compaction context, including retained messages after replay or reload +- **THEN** its durable invocation snapshot is resolved through supported native session history and its request copy receives exactly one expansion, preserving content, foreign data, IDs and role without changing stored history or relying on model-message metadata +- **AND** outbound provider input contains no routing marker or serialized duplicate snapshot, with stable expansion across identical requests and current-turn instructions present +- **AND** missing, unknown or non-user metadata and unmarked historical messages remain untouched + +#### Scenario: Auxiliary context and summary retention + +- **WHEN** title or generate context uses workflow history +- **THEN** it receives no expanded action snapshot +- **AND** summary-path recent context remains compact, while existing compaction guidance asks for preservation of an active workflow’s requested outcome, unresolved inputs or explicit modes, and applicable authority limits without promising verbatim retention + +#### Scenario: Existing bounded sync execution + +- **WHEN** bare sync or an explicit explanation is requested +- **THEN** existing bounded action versus explanation behavior remains controlling, with actual executor identity and capabilities recomputed at execution +- **AND** ordinary synchronization uses supported `bd sync --no-adopt`, existing configuration, applicable sandbox and native permissions/holds, reporting real success, partial failure or blockers honestly +- **AND** no adoption, provisioning, --yes, force resolution, manual repair loop or historical replayed operation is inferred + +#### Scenario: Native lifecycle ownership + +- **WHEN** admission fails ambiguously, delivery is queued or steered, or cancellation, revert, reload or compaction occurs +- **THEN** native scheduling and lifecycle remain authoritative without plugin retry, re-admission or manufactured continuation +- **AND** pre-admission unload protection and existing overrides/disposal remain effective + +#### Scenario: Legacy snapshots and idempotent precedence + +- **WHEN** retained user history contains a valid version-1 ocBeadsSync snapshot, a valid version-1 ocBeadsWorkflow snapshot, or both +- **THEN** the generalized snapshot wins when both are valid, otherwise the valid snapshot is expanded +- **AND** recognized routing keys and serialized duplicate snapshots are excluded from provider input, expansion is stable across repeated requests, and malformed, unsupported, foreign and non-user metadata remains untouched in durable history +- **AND** new invocations write only ocBeadsWorkflow, without reconstructing old instructions from current command definitions or rewriting stored history + +#### Scenario: Existing workflow authority and resource contracts + +- **WHEN** any migrated workflow is invoked with action, help, guide, missing-input or explicit mode details +- **THEN** its existing purpose, common instructions, all installed skill/resource paths, required targets and acceptance ownership remain controlling with actual execution-derived actor/capabilities/native permission +- **AND** restore remains display-only absent --apply, compact --auto and --dolt each need separate authority, QuickStart uses current installed guidance, stats means issue-database overview, and no implicit administration, cascade, initialization or repair is inferred + +#### Scenario: Snapshot retrieval is unavailable + +- **WHEN** a retained workflow invocation cannot resolve its durable snapshot through the supported native history path +- **THEN** the request exposes the missing-guidance condition rather than silently inventing or reconstructing its instructions +- **AND** the integration does not re-admit the prompt, replay the operation or reinterpret stored text as new authority + +#### Scenario: Model switch restores older transcript beyond the readable context + +- **WHEN** native compaction on model A is followed by switching to model B that cannot replay A's native window, and the host restores earlier ordinary user messages whose snapshots are absent from the native latest-compaction context +- **THEN** affected historical workflow instructions or captured diagnostic reports are explicitly reported unavailable rather than claimed restored +- **AND** accessible current snapshots still expand normally; host-supplied replay content and Beads issues, notes and checkpoints are not rewritten or deleted + +## ADDED Requirements + +### Requirement: Documented snapshot-recovery deficiency + +The package SHALL document unavailable historical workflow-snapshot recovery as an unresolved plugin deficiency with an upstream API blocker. User-facing documentation SHALL explain its trigger, impact and limits and link the upstream request. Accepting the limitation for a release SHALL NOT be described as resolving the deficiency. + +#### Scenario: User evaluates the documented limitation + +- **WHEN** a user reads the README's applicable compatibility/limitations section +- **THEN** it describes native compaction followed by an incompatible model switch, missing historical workflow instructions or captured reports, and unaffected Beads tracker data +- **AND** it links OpenCode #57424 and identifies the unresolved local follow-up without requiring Beads access to understand the limitation +- **AND** unreleased adaptation behavior is distinguished from the published beta's narrower host support diff --git a/openspec/changes/adapt-opencode-2-2/tasks.md b/openspec/changes/adapt-opencode-2-2/tasks.md new file mode 100644 index 0000000..f7d3e2f --- /dev/null +++ b/openspec/changes/adapt-opencode-2-2/tasks.md @@ -0,0 +1,20 @@ +# Tasks + +Planning only. After owner acceptance, map this cohesive change to one implementation Bead, not one Bead per checkbox. Shared edit limits and stop conditions are in [design.md](design.md); do not begin from artifact-complete status alone. + +## 1. Adapt the two host seams + +- [ ] 1.1 In the existing workflow/host modules, pin SDK 2.2.6, align admission to `>=2.2.6 <2.3`, migrate the test AI import/client calls and resolve retained snapshots through the native current-context API. Move location validation before lookup and assert published message-ID typing without casts. Land the projection-boundary RED→GREEN regression, rejected-read/unavailable-snapshot behavior, current/legacy precedence, diagnostics captured once, exact input/files/delivery and primary/compaction-only replay tests together. Preserve request IDs/content and stored history; verify one request-local read rather than a per-message lookup/cache. Add README limitation coverage: precise model-switch trigger, missing historical instructions/reports, unaffected Beads data, upstream #57424 and unresolved `opencode-beads-o66`; distinguish unreleased source from published beta.5. Completion: focused tests pass on the actual installed SDK, no unsupported host registers and user-visible documentation describes the deficiency without claiming it fixed. +- [ ] 1.2 Update capability derivation and shared guidance for composed tools, native direct Beads leaves and independently available shell. Keep the eleven tool definitions, permissions and actor contract unchanged. Land representative tests/docs for execute allowed, execute denied with direct leaves available, and shell wholly denied; include an operation-specific rejection and Stop behavior. Completion: request-catalog fixtures demonstrate truthful availability without direct re-registration, bypass or shell-inside-execute advice. + +## 2. Verify one candidate and accept the change + +- [ ] 2.1 After 1.1–1.2, run typecheck, the existing full tests, packed-install smoke, strict change/global OpenSpec validation and diff hygiene. Compare command purposes/resources, CLI argv and agent definitions against the accepted baseline; explain only intended changes. Freeze one local package artifact with source identity/digest for 2.2. Completion: normal gates pass and the handoff lists actual checks, preserved surfaces and remaining live gates; no registry publication. +- [ ] 2.2 Qualify that same artifact on exact OpenCode 2.2.6 with isolated HOME/XDG/database, an isolated Beads fixture and an authorized provider route. Use one bounded scenario set: command/diagnostics through stored context and provider request, legacy/current snapshots, primary/compaction versus auxiliary exclusion, native composed/direct/denied operations, scoped worker identity and Stop/unload. Cover summary/native replay; specifically, after native compaction on A then switching to non-replayable B, pre-compaction plain transcript snapshots absent from latest context must be **disclosed unavailable**, while accessible snapshots expand. Record that as a verified limitation, not restored snapshots or a fixed deficiency. A controlled-provider fixture can exercise those model identities; do not add a second genuine-provider campaign. Reuse deterministic per-command/argv coverage; one representative genuine-model workflow is sufficient, not 30 model runs or a provider/variant matrix. Completion: retained evidence identifies passes, verified limitations, failures and unexecuted checks, without operator auth/data/config changes; unavailable provisioning leaves this gate pending. +- [ ] 2.3 Review the frozen delta and 2.1–2.2 evidence for contract correctness and proportionality; resolve demonstrated adapter defects only. Reconcile current docs and task markers from accepted evidence, leaving limitations/unexecuted checks explicit and `opencode-beads-o66` unresolved. Completion: owner receives the candidate, source/installed-host verdicts and concise stable-2.0 migration/release recommendation; source readiness is not publication, activation or deficiency-resolution approval. + +## Workflow follow-up + +- After acceptance, synchronize these deltas into canonical specs and archive using the normal tested lifecycle; preserve historical reviews and qualifications. +- Commit/push/merge, plugin 2.0.0 preparation/publication and moving npm `latest` require their own authority. Keep existing stable/beta pins available and document the new host floor; operator activation remains separate. +- Scheduled source monitoring stays in `opencode-beads-rou`; do not pull it into this change. From cd4630c7a4322a8a1439acb714274f6a89ff6c15 Mon Sep 17 00:00:00 2001 From: Matt Jackson Date: Sun, 8 Nov 2026 20:20:42 +0000 Subject: [PATCH 2/7] docs: record reviewed OpenCode 2.2 execution mapping --- docs/reviews/astra-opencode-22-mapping-65r.md | 17 +++++++++++++++++ openspec/changes/adapt-opencode-2-2/tasks.md | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docs/reviews/astra-opencode-22-mapping-65r.md diff --git a/docs/reviews/astra-opencode-22-mapping-65r.md b/docs/reviews/astra-opencode-22-mapping-65r.md new file mode 100644 index 0000000..a9db8ba --- /dev/null +++ b/docs/reviews/astra-opencode-22-mapping-65r.md @@ -0,0 +1,17 @@ +# READY — bounded spec-to-Bead mapping review + +Reviewed the actual exported records against accepted commit `0ad03a6eebc61d3c0559610fbacd22989b47b21f`, reading `tasks.md`, `design.md` and `proposal.md` directly from Git. HEAD matches that commit; the working tree was clean. This verdict concerns mapping only, not implementation readiness or dispatch authority. + +**Findings: no material gaps, duplicates or authority mistakes. No corrections required.** + +- **Exclusive coverage:** `opencode-beads-5rj.1` owns exactly 1.1, 1.2 and 2.1; `opencode-beads-5rj.2` owns exactly 2.2 and 2.3. All five accepted task IDs occur once across execution leaves, with no extras. The epic and review record have empty task membership. All five spec checkboxes remain unchecked. +- **Prerequisites and parentage:** both leaves identify `opencode-beads-5rj` as parent. Qualification's `blocks` dependency points to the source leaf, correctly making source its prerequisite; acceptance additionally requires the exact parent-accepted revision/artifact digest. The epic's `discovered-from` edge to `9za` and review's `discovered-from` edge to the epic are provenance, not execution blockers. Parent-child edges are likewise not blocker edges. No exported edge gates delivery on `o66` or `rou`. +- **Scope and acceptance:** source owns the two adapter seams, accompanying documentation/regressions, normal full gates and one frozen artifact. Continuity, Beads behavior and agent definitions remain preservation/verification surfaces. Qualification consumes that artifact and evidence, retains permission/actor/Stop coverage, bounds genuine-provider work and requires honest pass/limitation/failure/unexecuted reporting. The pinned tasks remain authoritative for detailed requirements, including task 2.3's migration/release recommendation; concise record summaries do not remove them. +- **Markers and closure:** parent-accepted evidence controls task markers. Epic closeout requires both children, actual host qualification, owner acceptance and canonical sync/archive under granted authority; source-only success cannot close delivery. `o66` remains unresolved and nonblocking. Missing snapshots after the specified model switch are explicitly disclosed unavailable, not claimed restored. Monitoring stays separate. +- **Authority and stops:** all three execution records are open, have no exported assignee, and state mapping-only/execution-authorized false. `created_by: snarkimedes` is audit authorship, not executor assignment; no owner field is exported. Review coordination implies no worker claim. Provisioning absence leaves qualification pending. Additional mitigation requires discussion with Matt; the pinned design also supplies its concrete snapshot-resolution stop condition. Publication, operator changes and release remain separately authorized. + +**Proportionate:** one epic, two meaningful delivery leaves and one bounded review record are appropriate. The task preamble's older “one implementation Bead” wording is superseded by Matt's explicit source/qualification split in this request; it is not a mapping defect. No extra tickets, graph expansion, test cuts or architecture reconsideration are warranted. + +**Evidence:** `/opt/data/research/opencode-beads-2.2.6-impact/bead-mapping-records.json` and adjacent `bead-mapping.json`; pinned files under `openspec/changes/adapt-opencode-2-2/`. + +**Limits:** export-only tracker review; parent retains live re-fetch, graph/count/hash verification and acceptance. No bd invocation, tests, research campaign, project edits, Git writes or delegation. Only this report was written. diff --git a/openspec/changes/adapt-opencode-2-2/tasks.md b/openspec/changes/adapt-opencode-2-2/tasks.md index f7d3e2f..bc0343d 100644 --- a/openspec/changes/adapt-opencode-2-2/tasks.md +++ b/openspec/changes/adapt-opencode-2-2/tasks.md @@ -1,6 +1,6 @@ # Tasks -Planning only. After owner acceptance, map this cohesive change to one implementation Bead, not one Bead per checkbox. Shared edit limits and stop conditions are in [design.md](design.md); do not begin from artifact-complete status alone. +Planning accepted; execution mapping is epic `opencode-beads-5rj`, with source/checks child `opencode-beads-5rj.1` owning 1.1, 1.2 and 2.1, and qualification/acceptance child `opencode-beads-5rj.2` owning 2.2 and 2.3, blocked by the source child. Shared edit limits and stop conditions are in [design.md](design.md). Mapping is not execution authority; discuss additional corner-case mitigation before implementation and do not begin from artifact-complete status alone. ## 1. Adapt the two host seams From ccc9851bce42ec3f5a5b348cfa4cc40e1d93a643 Mon Sep 17 00:00:00 2001 From: Matt Jackson Date: Sun, 8 Nov 2026 21:26:48 +0000 Subject: [PATCH 3/7] adapt workflows and capability guidance for OpenCode 2.2.6 --- README.md | 13 +- assets/oc-beads/resources/CODE_MODE.md | 13 +- bun.lock | 26 ++-- docs/opencode-sdk-contract.md | 12 +- package.json | 4 +- scripts/test-package.ts | 35 ++++- src/guidance.ts | 12 +- src/host.ts | 116 +++++++++++++-- src/workflows.ts | 53 +++++-- tests/context.test.ts | 10 +- tests/docs.test.ts | 19 ++- tests/guidance.test.ts | 24 +++ tests/host.test.ts | 197 ++++++++++++++++++++----- tests/workflows.test.ts | 125 +++++++++------- 14 files changed, 507 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index 4f1447d..9194396 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,12 @@ Typed registration requires the host tool, permission and session interfaces. Th All 16 implementation tasks have parent acceptance on exact OpenCode 2.1.1 / Beads 1.3.1. The [final acceptance record](docs/code-mode-final-acceptance-vuy9.md) and [bounded installed/model evaluation](docs/opencode-installed-model-continuation-vuy9.md) retain their historical candidates and probes; the published beta adds the public entrypoint and release-channel wiring. **Known limitation:** plain decline of a nested permission request does not stop the entire composition; a later independent operation can ask again. Use explicit Stop/Cancel to stop the composition. No token-saving, cross-provider checkpoint portability or multi-hour reliability claim is made. +### Unreleased OpenCode 2.2.6 source adaptation + +This checkout contains an unreleased source candidate targeting `@opencode/plugin` 2.2.6 and host admission `>=2.2.6 <2.3`. It is still package version `1.1.0-beta.5`; the published beta.5 artifact and its `>=2.1.1 <2.2` compatibility range have not changed. The candidate has not passed live host qualification and does not alter operator configuration or Beads data. + +**Known workflow-guidance limitation:** after native conversation compaction on model A, switching to model B that cannot replay A's native context can make earlier workflow instruction snapshots and captured diagnostics unavailable. The candidate restores them only when OpenCode's current native session context contains matching user-message IDs; otherwise it reports the workflow guidance unavailable and continues with the ordinary request. This is the model-switch case tracked by [upstream issue #57424](https://github.com/anomalyco/opencode/issues/57424) and local deficiency `opencode-beads-o66`; it remains unresolved. The limitation affects prompt guidance only: it does not change or repair the Beads issue database or its stored data. + ## Design The published beta/next release uses scope-bounded task execution: @@ -31,7 +37,7 @@ The published beta/next release uses scope-bounded task execution: ## Installation -The beta requires OpenCode `>=2.1.1 <2.2` and `bd` 1.3.1 or later. Exact OpenCode 2.1.1 has bounded native, Stop and installed/model evidence; other admitted 2.1 patches are not inferred qualified. The external `bd` CLI and project initialization remain prerequisites; the plugin does not install or upgrade them. Stable 1.0.2 remains separately qualified against OpenCode 2.0.26. +The published beta.5 requires OpenCode `>=2.1.1 <2.2` and `bd` 1.3.1 or later. Exact OpenCode 2.1.1 has bounded native, Stop and installed/model evidence; other admitted 2.1 patches are not inferred qualified. The unreleased source candidate instead admits `>=2.2.6 <2.3`; that range is a source target, not host qualification. The external `bd` CLI and project initialization remain prerequisites; the plugin does not install or upgrade them. Stable 1.0.2 remains separately qualified against OpenCode 2.0.26. 1. Install the [`bd` CLI](https://github.com/gastownhall/beads/blob/main/docs/getting-started/installation.md) once on the host, then initialize each project: ```bash @@ -192,12 +198,13 @@ The package vendors nothing from upstream. It depends on the installed `bd` CLI | Component | Expected range | Validated baseline | | --- | --- | --- | -| OpenCode (beta/next candidate) | `>=2.1.1 <2.2` | 2.1.1 (bounded accepted evidence) | +| OpenCode (published beta.5) | `>=2.1.1 <2.2` | 2.1.1 (bounded accepted evidence) | +| OpenCode (unreleased source candidate) | `>=2.2.6 <2.3` | 2.2.6 (source target; host gate pending) | | OpenCode (stable 1.0.2 historical) | 2.0.26 | 2.0.26 | | `bd` CLI | 1.3.1 through 1.x | 1.3.1 | | Bun (OpenCode host runtime) | >=1.3.14 | 1.4.2 | -The beta/next candidate type-checks against `@opencode/plugin` 2.1.1 and declares an optional peer range of `>=2.1.1 <2.2`. It checks `ctx.app.version` at startup and reports an unsupported-host diagnostic before registering Beads agents or hooks. This source guard does not qualify the entire host range; exact 2.1.1 has bounded accepted evidence, and future 2.1 patches are admitted candidates rather than tested versions. The stable 1.0.2 qualification on 2.0.26 is historical. See the [OpenCode V2 contract](docs/opencode-sdk-contract.md). The `bd` baseline was reviewed against Beads v1.3.1 (commit `c1c4b642ac1c08d8c828007a1c2f96e47e43ef7c`). +Published beta.5 type-checks against `@opencode/plugin` 2.1.1 and declares an optional peer range of `>=2.1.1 <2.2`. This checkout's unreleased source candidate type-checks against 2.2.6 and declares `>=2.2.6 <2.3`; its source admission guard does not qualify that host range. Exact 2.1.1 has bounded accepted evidence for the published beta, and future 2.1 patches are admitted there as candidates rather than tested versions. The stable 1.0.2 qualification on 2.0.26 is historical. See the [OpenCode V2 contract](docs/opencode-sdk-contract.md). The `bd` baseline was reviewed against Beads v1.3.1 (commit `c1c4b642ac1c08d8c828007a1c2f96e47e43ef7c`). ## Troubleshooting diff --git a/assets/oc-beads/resources/CODE_MODE.md b/assets/oc-beads/resources/CODE_MODE.md index 3e50df6..71805e7 100644 --- a/assets/oc-beads/resources/CODE_MODE.md +++ b/assets/oc-beads/resources/CODE_MODE.md @@ -4,11 +4,14 @@ Use the actual request capability block. When the beads namespace is registered and execute is visible, use native `search({namespace: "beads"})` to discover current signatures. Catalog discovery and each operation's permissions determine what is callable; execute visibility alone does not establish leaf availability. -Do not copy a signature catalog from prose or invent tools. If execute is denied, -code-mode-only Beads operations disappear. Permissioned direct shell is a fallback -only when available, authorized and no guard hold applies. Native shell is -**direct-only**: never call `tools.shell` inside execute. Repository editing, -tests and Git run in separate direct shell turns. +Do not copy a signature catalog from prose or invent tools. When a host exposes +namespaced Beads leaves directly, use only those actually visible leaves and +their current signatures; each operation still has its own permission check. +Permissioned direct shell is independently available only when shown, authorized +and no guard hold applies. Do not retry a rejected operation through another +surface; preserve explicit Stop/Cancel. Native shell is **direct-only**: never +call `tools.shell` inside execute. Repository editing, tests and Git run in +separate direct shell turns. ## Read, choose, then act diff --git a/bun.lock b/bun.lock index 13966f7..3fb5227 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,13 @@ "": { "name": "@snarkipus/opencode-beads", "devDependencies": { - "@opencode/plugin": "2.1.1", + "@opencode/plugin": "2.2.6", "@types/bun": "1.3.4", "@types/node": "^22.0.0", "typescript": "5.9.3", }, "peerDependencies": { - "@opencode/plugin": ">=2.1.1 <2.2", + "@opencode/plugin": ">=2.2.6 <2.3", }, "optionalPeers": [ "@opencode/plugin", @@ -69,11 +69,11 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.1" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-MZ7M1a1dxMqTqeNwIGSECjO7RrReEgeEwHo8BnbOqP+8IfVpdiJi/iPCmkFVWlAxanMl4qhsq69e0Hkul9t+bA=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.1.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.1.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-j9bNzQaoQvcnimBFMG0i3piEW20E8HjkXAQHTJN/XqG2e4uJ9zCv53/HCNzD7+ooFn5S4nwQy/BOXeNReMIe+A=="], - "@effect/platform-node": ["@effect/platform-node@4.0.1", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.1", "undici": "^8.11.2" }, "peerDependencies": { "effect": "^4.0.1", "redis": ">=5.0.0 <7.0.0" } }, "sha512-FT0Fu5LO0/XP9zBpj0HqrtJNLluK4StNKqf6TSulUsjp8FPBsDVXGJiDuyoN5J2yVMT7tgvhxSwaDcObWVRIaQ=="], + "@effect/platform-node": ["@effect/platform-node@4.1.0", "", { "dependencies": { "@effect/platform-node-shared": "^4.1.0", "undici": "^8.12.2" }, "peerDependencies": { "effect": "^4.1.0", "redis": ">=5.0.0 <7.0.0" } }, "sha512-v+X7moGVF9k5oMVvz0slA+NgtEod+W+oygeI285MDJgSlUCW3JcqElj+cD3rlijFZtpKhC7RLkU1GuCxjGHTpA=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.1", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.22.0" }, "peerDependencies": { "effect": "^4.0.1" } }, "sha512-VanaejudEhKJ4mpmqnEzdncVUhN6sGSO9ZDND54mRxAlZxj+4dYAWjDTYAy9o4tJo9aALeP2U8cVcOFzEdSeLg=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.1.0", "", { "dependencies": { "@types/ws": "^8.18.2", "ws": "^8.22.0" }, "peerDependencies": { "effect": "^4.1.0" } }, "sha512-uYQfFRoxWLo8fIhiSeBcmz/R1ooOfE3HF3cWHFgxz82tZMk2Wn02BGABcdLmaAch9VpDLbtWnuhSxNyBsGMUFg=="], "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], @@ -113,17 +113,17 @@ "@npmcli/run-script": ["@npmcli/run-script@10.0.4", "", { "dependencies": { "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", "proc-log": "^6.0.0" } }, "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg=="], - "@opencode/ai": ["@opencode/ai@2.1.1", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.1.1", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.1", "google-auth-library": "10.5.0" } }, "sha512-sfL8kr53p+P3NeEPOZPalMOq47rR8KROywGzemsZdqMO8MwF+nBQgi7AGolANXKL95n3M8Nnci19ffGbxZ4N5Q=="], + "@opencode/ai": ["@opencode/ai@2.2.6", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.2.6", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.1.0", "google-auth-library": "10.5.0" } }, "sha512-VRes4ACJj1Lw8tpAgWwOy8+QPeXTko/KkJA8NDGVgVtl+MmzV/J5tlrpnpAI6zAgUARYV3GDFYj6EHn1V5ndLQ=="], - "@opencode/client": ["@opencode/client@2.1.1", "", { "dependencies": { "@opencode/protocol": "2.1.1", "@opencode/schema": "2.1.1" }, "peerDependencies": { "effect": "4.0.1", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-0/JTWitCw2gkr4o+ra+76UeeSpsq7mTFUUtKMPMt4aFu+rYgZw9Lkw4EJgxYK7p3sefiiKBA8KnX0a3LbxIjkw=="], + "@opencode/client": ["@opencode/client@2.2.6", "", { "dependencies": { "@opencode/protocol": "2.2.6", "@opencode/schema": "2.2.6" }, "peerDependencies": { "effect": "4.1.0", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-o90b8Z6tk62U3MiRK1g6r7WmNWLzHzIHme75/MCWa9/XmOREMzY4W1N01wAuHwU3J6UH/PlUDNKvLkeX5qCyeA=="], - "@opencode/plugin": ["@opencode/plugin@2.1.1", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.1.1", "@opencode/client": "2.1.1", "@opencode/protocol": "2.1.1", "@opencode/schema": "2.1.1", "@opencode/util": "2.1.1", "effect": "4.0.1" }, "peerDependencies": { "@opencode/theme": "2.1.1", "@opentui/core": ">=0.6.6", "@opentui/solid": ">=0.6.6", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-wvbvx/GeFNL7lb2Aj0IlXxPN01J6L1qJ2IbkUE5FaLmnC78aLjibzFP+wuzb28yUAM65dYw7WJRvHv+5GZRL8Q=="], + "@opencode/plugin": ["@opencode/plugin@2.2.6", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.2.6", "@opencode/client": "2.2.6", "@opencode/protocol": "2.2.6", "@opencode/schema": "2.2.6", "@opencode/util": "2.2.6", "effect": "4.1.0" }, "peerDependencies": { "@opencode/theme": "2.2.6", "@opentui/core": ">=0.8.1", "@opentui/solid": ">=0.8.1", "electron": "*", "solid-js": ">=1.9.0", "vite": ">=6", "vite-plugin-solid": ">=2.11" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "electron", "solid-js", "vite", "vite-plugin-solid"] }, "sha512-PbQD4aN8YJ8w3gM5L9h2msXppxEAUVj8Ejbaj6MMNb+99jPm7/XM+g2ugZomSNs1XXuab2IlrIKDgbiI49VfLw=="], - "@opencode/protocol": ["@opencode/protocol@2.1.1", "", { "dependencies": { "@opencode/schema": "2.1.1", "effect": "4.0.1" } }, "sha512-rFsaileXP/W7GNpt2C8GcF8WtLYzyoiaq/xMLkHfAk8xnH5jMNaWJL8DRawUM2FvCaI6H6SKeX4gw6eDGhyl6A=="], + "@opencode/protocol": ["@opencode/protocol@2.2.6", "", { "dependencies": { "@opencode/schema": "2.2.6", "effect": "4.1.0" } }, "sha512-XBAgz2qnhFS/EI4P3pgC9A6HJTHnFiaQXkqux3liXwE7hZh5wEzRZ551nQZD95rkrH00EnryUSaKI2y/O2Bt6w=="], - "@opencode/schema": ["@opencode/schema@2.1.1", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.1" } }, "sha512-Qh8EmvNeE/4UzSbkvvwQ2DHOOFZMk8dteeIsX//+qMhjTXJ+xGoWo5bsoauv6pYnqaQYtEwwpMaqk2N0rO4QaA=="], + "@opencode/schema": ["@opencode/schema@2.2.6", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.1.0" } }, "sha512-TaLr30ktFFVsuReorFNbqYvybTVxV/4M44qm5XzRjUqq6deR0jH4h4Wt1gB0NTkXDFL0LMH+Ul1oVBnp5w9TRg=="], - "@opencode/util": ["@opencode/util@2.1.1", "", { "dependencies": { "@effect/opentelemetry": "4.0.1", "@effect/platform-node": "4.0.1", "@effect/platform-node-shared": "4.0.1", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "cross-spawn": "7.0.6", "effect": "4.0.1", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "open": "11.0.4", "pacote": "21.5.1" } }, "sha512-Hi6TDr355iklKhhI+wqhTWfPT4mMdFpjo5yB5PGJfdW+AJQZDdhkhdLEL5khXS3Jgf7aXATYyvBesIEXrC/ktg=="], + "@opencode/util": ["@opencode/util@2.2.6", "", { "dependencies": { "@effect/opentelemetry": "4.1.0", "@effect/platform-node": "4.1.0", "@effect/platform-node-shared": "4.1.0", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "cross-spawn": "7.0.6", "effect": "4.1.0", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "open": "11.0.4", "pacote": "21.5.1" } }, "sha512-mOj2x3Aqw66sYzXq8MrypJVksFiO7GmFoSi5zi65TLtePbZCxvS/2TMbjgfa5MfWQp8YOl2327iv4W2rg6QQag=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -271,7 +271,7 @@ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - "effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="], + "effect": ["effect@4.1.0", "", {}, "sha512-lGx2MJJyrXA8RZb2UpQ/0Nkb5Utaimby7oD92+18noBxLPE85IFOvptdzau8dvcxAOwfwHo9iqapbKeFzqQM/Q=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -487,7 +487,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici": ["undici@8.11.2", "", {}, "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ=="], + "undici": ["undici@8.12.4", "", {}, "sha512-8Mx+uNKKER2bA4AdtedkqRbwUCIJFr2nJckXRe5yrDqzePqwLP7VNcctN8/XdmDSbF7mS+jJKIszlL+vbam1VA=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], diff --git a/docs/opencode-sdk-contract.md b/docs/opencode-sdk-contract.md index a916669..6244fed 100644 --- a/docs/opencode-sdk-contract.md +++ b/docs/opencode-sdk-contract.md @@ -1,5 +1,15 @@ # OpenCode V2 Plugin Contract +## Unreleased source adaptation — OpenCode 2.2.6 + +This local source candidate pins development types to `@opencode/plugin` `2.2.6`, declares optional peer range `@opencode/plugin` `>=2.2.6 <2.3`, and admits hosts in that range. The 2.2.6 runtime candidate has not been qualified. Package version remains `1.1.0-beta.5`; the published beta.5 artifact retains its historical `>=2.1.1 <2.2` range. This source change is not a release or operator activation. + +With the installed 2.2.6 SDK, OpenCode projects durable user records into `@opencode/ai` request `Message` values without their metadata while retaining message IDs. The candidate checks session location before calling the supported `ctx.session.context({ sessionID })` API, then matches each projected typed user-message ID to the durable `SessionMessageInfo` record. One request-local read builds the ID map; no snapshot cache or alternate message API is used. The current `ocBeadsWorkflow` snapshot takes precedence over legacy `ocBeadsSync`; workflow instructions and captured diagnostics are expanded only by the existing primary and compaction hooks. A failed or incomplete native read reports guidance unavailable and leaves ordinary input intact. Location, generation and cancellation are checked at the existing host boundary. + +**Known unavailable recovery case:** after native compaction on model A, changing to model B that cannot replay A's native context may leave the earlier workflow snapshot or captured diagnostics absent from the current `session.context` result. The candidate reports that guidance unavailable and does not reconstruct it. This corresponds to [upstream issue #57424](https://github.com/anomalyco/opencode/issues/57424) and local deficiency `opencode-beads-o66`; both remain unresolved. This limitation concerns prompt guidance only; it does not read, change or repair Beads issue data. + +The candidate's native request guidance handles three observed capability shapes separately: composed `execute` with catalog discovery, directly visible namespaced Beads leaves when exposed, and direct-only shell. Each operation retains its own host permission check; failures are not retried through another surface, and Stop/Cancel remains controlling. Direct leaves and shell are used only when actually visible and permissioned. + ## Current measured boundary — tasks 6.2–6.3 Source tasks 7.1–7.3 are parent-accepted at `8831169d3c83c333fdf13d4a4efc743eaf495e2b`; [191-test/source and63 CLI evidence](code-mode-source-acceptance-vuy10.md), [15-case native](opencode-host-codemode-qualification-vuy2.md) and [74-check Stop/handoff](opencode-stop-handoff-qualification-vuy6.md) remain accepted and were consumed without rerun. The [installed/model continuation](opencode-installed-model-continuation-vuy9.md) now records passing worker evidence for selected 6.2 and subsequent 6.3 reconciliation: exact released2.1.1, unchanged accepted packed artifact, genuine OpenAI Sol/high A–D semantics, durable uncertainty/readback, actual child actors and fresh/summary/provider-native recovery, with corrected actual cleanup/17 resource passes reused. Parent acceptance is complete for all 16 implementation tasks; see [final acceptance](code-mode-final-acceptance-vuy9.md). @@ -156,7 +166,7 @@ Reviewed on 2026-10-11 for the narrow OpenCode boundary used by the published st ## Versions and sources - Historical stable 1.0.2 declarations used optional peer range `@opencode/plugin` `>=2.0.26 <3` and development dependency `2.0.26`. The only import is type-only (`import type { Plugin } from "@opencode/plugin"`), so packages install nothing from OpenCode and use the host's runtime. -- Current beta/next candidate declarations use optional peer range `@opencode/plugin` `>=2.1.1 <2.2` and development typing dependency `@opencode/plugin` `2.1.1` for development typing only. The exact 2.1.1 runtime target has not been qualified. +- Published beta.5 declarations use optional peer range `@opencode/plugin` `>=2.1.1 <2.2` and development typing dependency `@opencode/plugin` `2.1.1`; exact 2.1.1 has bounded accepted evidence for that artifact. This checkout's unreleased source candidate uses `@opencode/plugin` `>=2.2.6 <2.3` and `2.2.6` for development typing only. The 2.2.6 runtime candidate has not been qualified. - Authoritative sources: the installed `@opencode/plugin` 2.0.26 declarations, the [plugin guide](https://opencode.ai/v2/docs/build/plugins/), the [V1 migration guide](https://opencode.ai/v2/docs/build/plugins/migrate-v1/), [plugin configuration](https://opencode.ai/v2/docs/plugins/), the [skills reference](https://opencode.ai/v2/docs/skills/), the published OpenAPI document (`https://opencode.ai/v2/openapi.json`), and `@opencode/schema` 2.0.26 for event names. - Qualification: a throwaway V2 plugin (`opencode-beads-2zs.1`) and the actual source plugin were loaded into disposable projects on OpenCode 2.0.26. Results were read with `opencode debug agents`, a JSONL trace from the plugin, and short `opencode run` sessions. Test sessions were deleted afterwards. diff --git a/package.json b/package.json index 285e9b7..da6d5e8 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "validated": "1.3.1" }, "peerDependencies": { - "@opencode/plugin": ">=2.1.1 <2.2" + "@opencode/plugin": ">=2.2.6 <2.3" }, "peerDependenciesMeta": { "@opencode/plugin": { @@ -68,7 +68,7 @@ } }, "devDependencies": { - "@opencode/plugin": "2.1.1", + "@opencode/plugin": "2.2.6", "@types/bun": "1.3.4", "@types/node": "^22.0.0", "typescript": "5.9.3" diff --git a/scripts/test-package.ts b/scripts/test-package.ts index 5be7139..db46880 100644 --- a/scripts/test-package.ts +++ b/scripts/test-package.ts @@ -1,5 +1,7 @@ import type { SessionContext } from "@opencode/plugin/promise/session"; import type { Info } from "@opencode/plugin/promise/tool"; +import { Message } from "@opencode/ai/effect"; +import type { Plugin } from "@opencode/plugin"; import { createHash } from "node:crypto"; import type { CommandDefinition, CommandInvocation } from "@opencode/plugin/promise/command"; import * as fs from "node:fs/promises"; @@ -7,6 +9,21 @@ import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +type StoredSessionMessage = Awaited>[number]; +type StoredUserMessage = Extract; + +function storedUserMessage( + id: string, + text: string, + metadata: StoredUserMessage["metadata"] +): StoredUserMessage { + return { id, type: "user", time: { created: 1 }, text, ...(metadata === undefined ? {} : { metadata }) }; +} + +function projectedUserMessage(id: string, text: string) { + return Message.make({ id, role: "user", content: text }); +} + const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-beads-package-")); const archiveArgument = process.argv.indexOf("--archive"); @@ -120,7 +137,7 @@ try { if (manifest.bin !== undefined) { throw new Error("Packed package unexpectedly registers a companion CLI"); } - const opencodeRange = ">=2.1.1 <2.2"; + const opencodeRange = ">=2.2.6 <2.3"; if ( manifest.dependencies !== undefined || JSON.stringify(manifest.peerDependencies) !== @@ -237,9 +254,10 @@ try { return { dispose: async () => { disposed.add(name); } }; }; const prompts: Array> = []; + const storedMessages: StoredSessionMessage[] = []; let invoking = false; const cleanup = await (plugin!.setup as (ctx: unknown) => Promise)({ - app: { name: "OpenCode", version: "2.1.1", channel: "stable" }, + app: { name: "OpenCode", version: "2.2.6", channel: "stable" }, location: { directory: consumerDir, project: { directory: consumerDir } }, options, agent: { @@ -279,6 +297,7 @@ try { }, session: { get: async () => ({ location: { directory: consumerDir } }), + context: async () => [...storedMessages], prompt: async (input: Record) => { if (!invoking) throw new Error("Setup must not submit a prompt"); prompts.push(input); @@ -331,7 +350,10 @@ try { if (status.plugin?.name !== manifest.name || status.plugin?.version !== manifest.version) throw new Error("Packed diagnostics did not identify the loaded package"); const stored = JSON.stringify(submitted); for (const kind of ["context", "compaction"]) { - const event = {sessionID: invocation.sessionID, agent: "build", system: [], messages: [{role: "user", content: [{type: "text", text: submitted.text}], metadata}], tools: {}, options: {}} as unknown as SessionContext; + const id = `packed-diagnostics-${kind}`; + storedMessages.splice(0, storedMessages.length, storedUserMessage(id, submitted.text, metadata as StoredUserMessage["metadata"])); + const event = {sessionID: invocation.sessionID, agent: "build", system: [], messages: [projectedUserMessage(id, submitted.text)], tools: {}, options: {}} as unknown as SessionContext; + if (Object.hasOwn(event.messages[0]!, "metadata")) throw new Error("SDK projection unexpectedly retained request metadata"); await requestHooks.get(kind)!(event); if (event.messages[0]?.content[0]?.type !== "text" || event.messages[0].content[0].text !== snapshot.instructions || /ocBeadsWorkflow/.test(JSON.stringify(event.messages))) throw new Error(`Packed diagnostics expansion differs: ${kind}`); const once = JSON.stringify(event.messages); @@ -357,8 +379,11 @@ try { if (!snapshot.instructions.endsWith(`Supporting guidance: ${JSON.stringify(supporting)}`) || !(await fs.readFile(supporting, "utf8")).trim()) throw new Error(`Packed workflow resource differs: ${definition.name}`); const stored = JSON.stringify(submitted); for (const kind of ["context", "compaction"]) { - for (const routing of [metadata, { ocBeadsSync: snapshot }]) { - const event = { sessionID: invocation.sessionID, agent: "build", system: [], messages: [{ role: "user", content: [{ type: "text", text: submitted.text }], metadata: routing }], tools: {}, options: {} } as unknown as SessionContext; + for (const [routingIndex, routing] of [metadata, { ocBeadsSync: snapshot }].entries()) { + const id = `packed-workflow-${kind}-${routingIndex}`; + storedMessages.splice(0, storedMessages.length, storedUserMessage(id, submitted.text, routing as StoredUserMessage["metadata"])); + const event = { sessionID: invocation.sessionID, agent: "build", system: [], messages: [projectedUserMessage(id, submitted.text)], tools: {}, options: {} } as unknown as SessionContext; + if (Object.hasOwn(event.messages[0]!, "metadata")) throw new Error("SDK projection unexpectedly retained request metadata"); await requestHooks.get(kind)!(event); const expanded = JSON.stringify(event.messages); if (/ocBeadsWorkflow|ocBeadsSync/.test(expanded) || expanded.split("Invoked command:").length !== 2 diff --git a/src/guidance.ts b/src/guidance.ts index 90dc9eb..e2c15ca 100644 --- a/src/guidance.ts +++ b/src/guidance.ts @@ -78,17 +78,19 @@ Preserve an active /oc-beads workflow invocation's requested outcome, unresolved /** Observed after native request filtering; selective operation permission remains per call. */ export interface RequestCapabilities { - typed: "registered-execute-visible" | "unavailable" | "unknown"; + typed: "registered-execute-visible" | "registered-direct-visible" | "unavailable" | "unknown"; execute: boolean | null; shell: boolean | null; + direct: boolean | null; } -export const UNKNOWN_CAPABILITIES: RequestCapabilities = { typed: "unknown", execute: null, shell: null }; +export const UNKNOWN_CAPABILITIES: RequestCapabilities = { typed: "unknown", execute: null, shell: null, direct: null }; /** Capability statements describe observations, never a permission grant. */ export function capabilityGuidance(capabilities: RequestCapabilities): string { - if (capabilities.typed === "registered-execute-visible") return "Beads namespace is registered and execute is visible. Discover current signatures with native search({namespace: \"beads\"}); compose only operations found in the native catalog. Some or all leaves may be filtered out. Each leaf still checks its own permissions; visibility grants no authority. Native shell is direct-only; run repository work and validation in direct shell turns. Do not call tools.shell inside execute."; - if (capabilities.shell === true) return "Typed Beads composition is unavailable for this request. Use permissioned bd through the direct shell tool only when authorized and no hold applies. Shell cannot run inside execute."; - if (capabilities.execute !== null && capabilities.shell === false) return "Neither typed Beads composition nor direct shell access is available for this request. Report unavailable capability and stop the affected work; do not route around it."; + if (capabilities.typed === "registered-execute-visible") return "Beads namespace is registered and execute is visible. Discover current signatures with native search({namespace: \"beads\"}); compose only operations found in the native catalog. Some or all leaves may be filtered out. Each operation still checks its own permissions; a rejected operation is not success and must not be retried through another surface. Honor explicit Stop/Cancel. Native shell is direct-only; run repository work and validation in direct shell turns. Do not call tools.shell inside execute."; + if (capabilities.typed === "registered-direct-visible") return "Native Beads leaf tools are directly visible for this request, while execute is unavailable. Use only the listed direct leaves and their current signatures, one operation at a time. Each operation still checks its own permission; a rejected operation is not success and must not be retried through shell or another surface. Honor explicit Stop/Cancel. Direct shell is available only if separately listed and permissioned."; + if (capabilities.shell === true) return "Native Beads operations are not visible for this request. Use permissioned bd through the independently available direct shell tool only when authorized and no hold applies. Shell cannot run inside execute."; + if (capabilities.execute !== null && capabilities.shell === false) return "Neither native Beads leaves nor direct shell access are available for this request. Report unavailable capability and stop the affected work; do not route around it."; return "Beads request capability is unknown. Check actual native catalog/tools before choosing typed composition or direct shell; do not infer permission or bypass a hold."; } diff --git a/src/host.ts b/src/host.ts index 55e66bc..57fed1d 100644 --- a/src/host.ts +++ b/src/host.ts @@ -1,4 +1,5 @@ import type { Plugin } from "@opencode/plugin"; +import type { SessionContext } from "@opencode/plugin/promise/session"; import packageManifest from "../package.json"; import { applyBeadsSkill, createBeadsCommands, expandBeadsWorkflowContext, loadBeadsSkill, type SkillLoadDependencies } from "./workflows"; import { applyBeadsAgents, beadsAgents, BEADS_TASK_AGENTS } from "./agents"; @@ -17,7 +18,7 @@ import { createContinuityTools, type ContinuityDependencies, type ContinuityResu /** The slice of the OpenCode V2 plugin context this adapter uses. */ export type HostContext = Pick & { agent: Pick; - session: Pick & Partial>; + session: Pick & Partial>; tool?: Pick; permission?: Pick; command?: Pick; @@ -44,7 +45,11 @@ export interface HostDependencies { } const DEFAULT_DIAGNOSTIC_INTERVAL_MS = 60_000; -const SUPPORTED_OPENCODE_RANGE = ">=2.1.1 <2.2"; +const SUPPORTED_OPENCODE_RANGE = ">=2.2.6 <2.3"; +const DIRECT_BEADS_TOOLS = new Set([ + "beads_find", "beads_get", "beads_edges", "beads_memories", "beads_recall", "beads_claim", + "beads_note", "beads_update", "beads_close", "beads_create", "beads_remember", +]); function isSupportedOpenCodeVersion(version: string): boolean { const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/.exec(version); @@ -52,7 +57,7 @@ function isSupportedOpenCodeVersion(version: string): boolean { const major = Number(match[1]); const minor = Number(match[2]); const patch = Number(match[3]); - return major === 2 && minor === 1 && patch >= 1; + return major === 2 && minor === 2 && patch >= 6; } function eventSessionID(event: unknown): string | undefined { @@ -138,6 +143,7 @@ export async function setupBeads( const recoveryCues = new Map(); const references = new Map>(); const gaps = new Map>(); + const workflowSnapshotReads = new Map(); const requestCapabilities = new Map(); let typedRegistration = deps.continuityQualified === true ? "unavailable: host tool/permission/session interface missing" : "disabled: registration not requested"; @@ -161,6 +167,7 @@ export async function setupBeads( references.delete(sessionID); recoveryCues.delete(sessionID); gaps.delete(sessionID); + workflowSnapshotReads.delete(sessionID); requestCapabilities.delete(sessionID); for (const key of diagnosticTimes.keys()) { if (key.endsWith(`:${sessionID}`)) diagnosticTimes.delete(key); @@ -178,6 +185,7 @@ export async function setupBeads( recoveryCues.clear(); references.clear(); gaps.clear(); + workflowSnapshotReads.clear(); requestCapabilities.clear(); diagnosticTimes.clear(); const pending = registrations.splice(0).reverse().map((registration) => { @@ -205,6 +213,47 @@ export async function setupBeads( try { warn(`[opencode-beads] ${message}`); } catch { /* Diagnostics never break loading. */ } } + const workflowUnavailableNotice = "Saved Beads workflow instructions or diagnostics are unavailable for a compact command. Continue only from visible user input and supplied context; do not invent missing guidance or replay an operation."; + function reportUnavailableWorkflow(event: { sessionID: string; messages: NonNullable; system: Array<{ type: "text"; text: string }> }, unavailable: boolean): void { + if (!unavailable) return; + workflowDiagnostic(`workflow-snapshot-unavailable session=${event.sessionID}`); + if (!event.system.some((part) => part.text === workflowUnavailableNotice)) { + event.system.push({ type: "text", text: workflowUnavailableNotice }); + } + } + + function beginWorkflowSnapshotRead(sessionID: string): symbol { + const generation = Symbol(sessionID); + workflowSnapshotReads.set(sessionID, generation); + return generation; + } + + async function restoreWorkflowSnapshots( + event: { sessionID: string; messages: SessionContext["messages"]; system: Array<{ type: "text"; text: string }> }, + generation: symbol, + ): Promise { + try { + const storedMessages = await ctx.session.context({ sessionID: event.sessionID }); + if (abort.signal.aborted) return false; + if (workflowSnapshotReads.get(event.sessionID) !== generation) { + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); + return false; + } + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, storedMessages)); + return true; + } catch { + if (abort.signal.aborted) return false; + if (workflowSnapshotReads.get(event.sessionID) !== generation) { + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); + return false; + } + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); + return true; + } finally { + if (workflowSnapshotReads.get(event.sessionID) === generation) workflowSnapshotReads.delete(event.sessionID); + } + } + let skillFailure: string | undefined; if (ctx.skill) { let skill; @@ -254,6 +303,7 @@ export async function setupBeads( catch { /* Absence of lookup evidence is unknown, not an empty registry. */ } try { effectiveSkills = ctx.skill ? (await ctx.skill.list()).data.map((entry) => String(entry.id)) : "unavailable"; } catch { effectiveSkills = "unavailable: registry lookup failed"; } + const observedRequest = requestCapabilities.get(sessionID); return JSON.stringify({ plugin: { name: packageManifest.name, version: packageManifest.version }, host: hostVersion, admission: SUPPORTED_OPENCODE_RANGE, liveQualification: "not measured by diagnostics; see release acceptance evidence", @@ -273,8 +323,12 @@ export async function setupBeads( skill: { failure: skillFailure, effective: effectiveSkills }, typedContinuity: deps.continuityQualified === true ? "enabled: qualified beta registration" : "disabled: registration not requested", typedRegistration, - requestCapability: requestCapabilities.get(sessionID) ?? "unknown: no actual request observation", - leafAvailability: "unknown until native catalog discovery and per-operation permission checks; execute visibility alone does not prove leaf availability", + requestCapability: observedRequest ?? "unknown: no actual request observation", + leafAvailability: observedRequest?.capabilities.direct === true + ? "some native direct Beads leaves are visible; each still checks its operation-specific permission" + : observedRequest?.capabilities.typed === "registered-execute-visible" + ? "execute is visible; the native catalog and each operation-specific permission determine leaf availability" + : "unknown until native catalog discovery and per-operation permission checks", context, guard, persistence: gapFor(sessionID) ?? "uncertain: no final-write guarantee", }); }, deps.workflowAssets?.skillPath, abort.signal); @@ -336,24 +390,43 @@ export async function setupBeads( registrations.push(await ctx.session.hook("context", async (event) => { if (abort.signal.aborted) return; - expandBeadsWorkflowContext(event.messages); + const snapshotGeneration = beginWorkflowSnapshotRead(event.sessionID); if (ctx.session.get) { try { const info = await ctx.session.get({ sessionID: event.sessionID }); + if (abort.signal.aborted) return; + if (workflowSnapshotReads.get(event.sessionID) !== snapshotGeneration) { + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); + return; + } if (info.location.directory !== directory) { forgetSession(event.sessionID); + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); return; } } catch { + if (abort.signal.aborted) return; forgetSession(event.sessionID); + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); if (!abort.signal.aborted) event.system.push({ type: "text", text: "Beads context withheld: current session location could not be established. Do not bypass this hold with manual store access." }); return; } + } else { + workflowSnapshotReads.delete(event.sessionID); + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); } + if (ctx.session.get && !(await restoreWorkflowSnapshots(event, snapshotGeneration))) return; + else if (!ctx.session.get && workflowSnapshotReads.get(event.sessionID) === snapshotGeneration) workflowSnapshotReads.delete(event.sessionID); + if (abort.signal.aborted) return; const knownTools = event.tools && typeof event.tools === "object"; const capabilities: RequestCapabilities = knownTools ? { execute: Object.hasOwn(event.tools, "execute"), shell: Object.hasOwn(event.tools, "shell"), - typed: typedRegistration.startsWith("registered") && Object.hasOwn(event.tools, "execute") ? "registered-execute-visible" : "unavailable", + direct: Object.keys(event.tools).some((name) => DIRECT_BEADS_TOOLS.has(name)), + typed: typedRegistration.startsWith("registered") && Object.hasOwn(event.tools, "execute") + ? "registered-execute-visible" + : typedRegistration.startsWith("registered") && Object.keys(event.tools).some((name) => DIRECT_BEADS_TOOLS.has(name)) + ? "registered-direct-visible" + : "unavailable", } : { ...UNKNOWN_CAPABILITIES }; requestCapabilities.set(event.sessionID, { agent: event.agent, capabilities }); const text = await controller.systemFor(event.sessionID, event.agent, capabilities); @@ -375,9 +448,34 @@ export async function setupBeads( } })); - registrations.push(await ctx.session.hook("compaction", (event) => { + registrations.push(await ctx.session.hook("compaction", async (event) => { + if (abort.signal.aborted) return; + const snapshotGeneration = beginWorkflowSnapshotRead(event.sessionID); + if (ctx.session.get) { + try { + const info = await ctx.session.get({ sessionID: event.sessionID }); + if (abort.signal.aborted) return; + if (workflowSnapshotReads.get(event.sessionID) !== snapshotGeneration) { + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); + return; + } + if (info.location.directory !== directory) { + forgetSession(event.sessionID); + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); + return; + } + } catch { + if (abort.signal.aborted) return; + forgetSession(event.sessionID); + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); + return; + } + if (!(await restoreWorkflowSnapshots(event, snapshotGeneration))) return; + } else { + workflowSnapshotReads.delete(event.sessionID); + reportUnavailableWorkflow(event, expandBeadsWorkflowContext(event.messages, [])); + } if (abort.signal.aborted) return; - expandBeadsWorkflowContext(event.messages); if (beadsSessions.get(event.sessionID)?.has(event.agent)) { event.system.push({ type: "text", text: BEADS_COMPACTION_GUIDANCE }); const known = references.get(event.sessionID); diff --git a/src/workflows.ts b/src/workflows.ts index 7e35e5e..0983693 100644 --- a/src/workflows.ts +++ b/src/workflows.ts @@ -7,6 +7,8 @@ import type { CommandDefinition } from "@opencode/plugin/promise/command"; type SkillEditor = Parameters[0]>[0]; export type BeadsSkill = Parameters[0]; +type StoredSessionMessage = Awaited>[number]; +type RequestMessage = SessionContext["messages"][number]; /** Package-relative paths survive npm installation, spaces and ignored/deep install roots. */ export const BEADS_SKILL_PATH = fileURLToPath(new URL("../assets/oc-beads/SKILL.md", import.meta.url)); @@ -88,26 +90,53 @@ export function applyBeadsSkill(editor: SkillEditor, skill: BeadsSkill, diagnose editor.add(skill); } -/** Expand supported workflow/legacy snapshots only in transient user request copies. */ -export function expandBeadsWorkflowContext(messages: SessionContext["messages"]): void { +function storedWorkflowInstructions(metadata: unknown): string | undefined { + if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) return undefined; + const snapshotInstructions = (snapshot: unknown): string | undefined => { + if (typeof snapshot !== "object" || snapshot === null || Array.isArray(snapshot)) return undefined; + if (!("version" in snapshot) || snapshot.version !== 1 + || !("instructions" in snapshot) || typeof snapshot.instructions !== "string") return undefined; + return snapshot.instructions; + }; + return snapshotInstructions(Reflect.get(metadata, "ocBeadsWorkflow")) + ?? snapshotInstructions(Reflect.get(metadata, "ocBeadsSync")); +} + +function hasWorkflowInvocation(message: RequestMessage): boolean { + if (message.role !== "user") return false; + let text = ""; + for (const part of message.content) if (part.type === "text") text += `${text ? "\n" : ""}${part.text}`; + const command = /(?:^|\n)\/oc-beads:([a-z-]+)(?=\s|$)/m.exec(text)?.[1]; + return command === "diagnostics" || (command !== undefined && Object.hasOwn(WORKFLOWS, command)); +} + +/** Expand snapshots from native durable records into transient request copies by typed message ID. */ +export function expandBeadsWorkflowContext( + messages: SessionContext["messages"], + storedMessages: readonly StoredSessionMessage[], +): boolean { + const instructionsByID = new Map(); + for (const stored of storedMessages) { + if (stored.type !== "user") continue; + const instructions = storedWorkflowInstructions(stored.metadata); + if (instructions !== undefined) instructionsByID.set(stored.id, instructions); + } + + let unavailable = false; for (const [index, message] of messages.entries()) { if (message.role !== "user") continue; - const metadata = { ...message.metadata }; - let instructions: string | undefined; - for (const key of ["ocBeadsSync", "ocBeadsWorkflow"]) { - const snapshot = metadata[key]; - if (!snapshot || typeof snapshot !== "object" || !("version" in snapshot) || snapshot.version !== 1 - || !("instructions" in snapshot) || typeof snapshot.instructions !== "string") continue; - instructions = snapshot.instructions; - delete metadata[key]; + const instructions = message.id === undefined ? undefined : instructionsByID.get(message.id); + if (instructions === undefined) { + unavailable ||= hasWorkflowInvocation(message); + continue; } - if (instructions === undefined) continue; + if (message.content[0]?.type === "text" && message.content[0].text === instructions) continue; messages[index] = { ...message, - metadata, content: [{ type: "text", text: instructions }, ...message.content], }; } + return unavailable; } /** Build native prompt commands with compact text and durable static workflow instructions. */ diff --git a/tests/context.test.ts b/tests/context.test.ts index 8abd84c..9fbe6c2 100644 --- a/tests/context.test.ts +++ b/tests/context.test.ts @@ -46,15 +46,19 @@ function fixture( describe("context controller", () => { test("renders actual request actors and capabilities without caching one agent identity", async () => { const f = fixture(); - const typed = { typed: "registered-execute-visible", execute: true, shell: true } as const; + const typed = { typed: "registered-execute-visible", execute: true, shell: true, direct: false } as const; const a = await f.controller.systemFor("s1", "build", typed); - const b = await f.controller.systemFor("s1", "review", { typed: "unavailable", execute: false, shell: true }); + const b = await f.controller.systemFor("s1", "review", { typed: "unavailable", execute: false, shell: true, direct: false }); expect(a).toContain("opencode:build:"); expect(a).toContain("namespace"); expect(b).toContain("opencode:review:"); expect(b).not.toContain("opencode:build:"); expect(b).toContain("direct shell"); - expect(await f.controller.systemFor("s1", "review", { typed: "unavailable", execute: false, shell: false })).toContain("Neither"); + const direct = await f.controller.systemFor("s1", "restricted", { typed: "registered-direct-visible", execute: false, shell: false, direct: true }); + expect(direct).toContain("Native Beads leaf tools are directly visible"); + expect(direct).toContain("rejected operation is not success"); + expect(direct).toContain("Stop/Cancel"); + expect(await f.controller.systemFor("s1", "review", { typed: "unavailable", execute: false, shell: false, direct: false })).toContain("Neither"); expect(f.prime).toHaveBeenCalledTimes(1); const held = fixture({ guard: { kind: "migration-pending", installed: "1.3.1", store: "1.2.0" } }); expect(await held.controller.systemFor("s1", "build", typed)).not.toContain("direct shell"); diff --git a/tests/docs.test.ts b/tests/docs.test.ts index b49831a..6348b2a 100644 --- a/tests/docs.test.ts +++ b/tests/docs.test.ts @@ -202,18 +202,21 @@ describe("documentation contracts", () => { test("keeps the compatibility record on the selected beta/next OpenCode baseline", () => { expect(sdkContract).toContain("# OpenCode V2 Plugin Contract"); - expect(packageManifest.peerDependencies["@opencode/plugin"]).toBe(">=2.1.1 <2.2"); + expect(packageManifest.peerDependencies["@opencode/plugin"]).toBe(">=2.2.6 <2.3"); expect(packageManifest.peerDependenciesMeta["@opencode/plugin"]?.optional).toBeTrue(); - expect(packageManifest.devDependencies["@opencode/plugin"]).toBe("2.1.1"); - expect(sdkContract).toContain("`@opencode/plugin` `>=2.1.1 <2.2`"); - expect(sdkContract).toContain("`@opencode/plugin` `2.1.1` for development typing only"); + expect(packageManifest.devDependencies["@opencode/plugin"]).toBe("2.2.6"); + expect(sdkContract).toContain("`@opencode/plugin` `>=2.2.6 <2.3`"); + expect(sdkContract).toContain("`2.2.6` for development typing only"); expect(sdkContract).toContain("`/server`, then ``"); expect(sdkContract).toContain("`session.compaction.ended`"); - expect(sdkContract).toContain("The exact 2.1.1 runtime target has not been qualified"); + expect(sdkContract).toContain("The 2.2.6 runtime candidate has not been qualified"); expect(sdkContract).not.toContain("1.18."); - expect(readme).toContain( - "| OpenCode (beta/next candidate) | `>=2.1.1 <2.2` | 2.1.1 (bounded accepted evidence) |" - ); + expect(readme).toContain("| OpenCode (published beta.5) | `>=2.1.1 <2.2` | 2.1.1 (bounded accepted evidence) |"); + expect(readme).toContain("| OpenCode (unreleased source candidate) | `>=2.2.6 <2.3` | 2.2.6 (source target; host gate pending) |"); + expect(readme).toContain("after native conversation compaction on model A, switching to model B"); + expect(readme).toContain("upstream issue #57424"); + expect(readme).toContain("opencode-beads-o66"); + expect(readme).toContain("does not change or repair the Beads issue database or its stored data"); expect(readme).toContain("| OpenCode (stable 1.0.2 historical) | 2.0.26 | 2.0.26 |"); expect(readme).toContain("| `bd` CLI | 1.3.1 through 1.x | 1.3.1 |"); expect(readme).not.toContain("@opencode-ai/"); diff --git a/tests/guidance.test.ts b/tests/guidance.test.ts index 63518f6..f1a4293 100644 --- a/tests/guidance.test.ts +++ b/tests/guidance.test.ts @@ -5,6 +5,7 @@ import { BEADS_TASK_AGENT_DESCRIPTION, BEADS_TASK_AGENT_PROMPT, beadsGuidance, + capabilityGuidance, } from "../src/guidance"; const unsupportedPattern = /beads mcp|mcp server|via mcp|\bmcp\b|claude code|\/plugin update beads/i; @@ -90,6 +91,29 @@ describe("project-owned task-agent contract", () => { } }); + test("describes composed, direct-leaf, shell, and wholly denied requests separately", () => { + const composed = capabilityGuidance({ typed: "registered-execute-visible", execute: true, shell: true, direct: false }); + expect(composed).toContain("Beads namespace is registered and execute is visible"); + expect(composed).toContain("Each operation still checks its own permissions"); + expect(composed).toContain("rejected operation is not success"); + expect(composed).toContain("Stop/Cancel"); + expect(composed).toContain("Do not call tools.shell inside execute"); + + const direct = capabilityGuidance({ typed: "registered-direct-visible", execute: false, shell: false, direct: true }); + expect(direct).toContain("Native Beads leaf tools are directly visible"); + expect(direct).toContain("Use only the listed direct leaves"); + expect(direct).toContain("rejected operation is not success"); + expect(direct).toContain("Stop/Cancel"); + + const shell = capabilityGuidance({ typed: "unavailable", execute: false, shell: true, direct: false }); + expect(shell).toContain("independently available direct shell tool"); + expect(shell).toContain("Shell cannot run inside execute"); + + const denied = capabilityGuidance({ typed: "unavailable", execute: false, shell: false, direct: false }); + expect(denied).toContain("Neither native Beads leaves nor direct shell access"); + expect(denied).toContain("do not route around it"); + }); + test("gives the Luna profile the same prompt and description", () => { const agents = beadsAgents({ luna: true }); expect(agents["beads-task-agent-luna"]).toEqual({ diff --git a/tests/host.test.ts b/tests/host.test.ts index 9151801..bd8facd 100644 --- a/tests/host.test.ts +++ b/tests/host.test.ts @@ -1,5 +1,6 @@ import { createHash } from "node:crypto"; -import { make } from "@opencode/ai/promise"; +import { make } from "@opencode/ai"; +import { Message } from "@opencode/ai/effect"; import { configure } from "@opencode/ai/providers/openai"; import baseline from "./fixtures/non-sync-command-baseline.json"; import { describe, expect, mock, test } from "bun:test"; @@ -19,6 +20,12 @@ import { BEADS_COMPACTION_GUIDANCE, primeFailureNotice } from "../src/guidance"; type ContextEvent = { messages?: SessionContext["messages"]; tools?: Record; sessionID: string; agent: string; system: Array<{ type: "text"; text: string }> }; type ToolAfterEvent = Extract[1]>[0], { status: string }>; +type StoredSessionMessage = Awaited>[number]; +type StoredUserMessage = Extract; + +function storedUserMessage(id: string, text: string, metadata?: StoredUserMessage["metadata"]): StoredUserMessage { + return { id, type: "user", time: { created: 1 }, text, ...(metadata === undefined ? {} : { metadata }) }; +} /** A controllable server event stream. */ function eventStream() { @@ -77,12 +84,13 @@ function fakeContext(options: { return { dispose: async () => {} }; }); const ctx = { - app: { name: "OpenCode", version: options.appVersion ?? "2.1.1", channel: "stable" }, + app: { name: "OpenCode", version: options.appVersion ?? "2.2.6", channel: "stable" }, location: { directory: options.directory ?? "/project" }, options: options.pluginOptions ?? {}, event: { subscribe: events.subscribe }, agent: { get: agentGet, transform: agentTransform }, session: { + context: mock(async () => []), hook: mock(async (name: string, callback: (event: ContextEvent) => void) => { hooks.set(name, (event) => { event.messages ??= []; return callback(event); }); return { dispose: async () => {} }; @@ -331,11 +339,17 @@ describe("setupBeads", () => { const typed = (await f.request("actual", "build", { execute: {}, shell: {} }))[0]!; expect(typed).toContain(executorActor({ sessionID: "actual", agent: "build" })); expect(typed).toContain("Beads namespace is registered and execute is visible"); + const direct = (await f.request("actual", "restricted", { beads_get: {}, beads_note: {} }))[0]!; + expect(direct).toContain(executorActor({ sessionID: "actual", agent: "restricted" })); + expect(direct).toContain("Native Beads leaf tools are directly visible"); + expect(direct).toContain("Use only the listed direct leaves"); + expect(direct).toContain("Each operation still checks its own permission"); + expect(direct).not.toContain("direct shell tool"); const shell = (await f.request("actual", "review", { shell: {} }))[0]!; expect(shell).toContain(executorActor({ sessionID: "actual", agent: "review" })); expect(shell).not.toContain(executorActor({ sessionID: "actual", agent: "build" })); expect(shell).toContain("direct shell tool"); - expect((await f.request("actual", "review", {}))[0]).toContain("Neither"); + expect((await f.request("actual", "review", {}))[0]).toContain("Neither native Beads leaves nor direct shell access"); expect(prime).toHaveBeenCalledTimes(1); guard = { kind: "migration-pending", installed: "1.3.1", store: "1.2.0" }; f.events.push({ type: "session.compaction.ended", data: { sessionID: "actual" } }); await settle(); @@ -398,22 +412,22 @@ describe("setupBeads", () => { cleanup(); }); test("reports and skips setup for an unsupported OpenCode host", async () => { - for (const appVersion of ["2.0.29", "2.1.0", "2.1.1-beta.1", "2.2.0", "unknown"]) { + for (const appVersion of ["2.0.29", "2.1.0", "2.1.1", "2.2.5", "2.3.0", "2.2.6-beta.1", "unknown"]) { const f = fakeContext({ appVersion }); const warn = mock(() => {}); const cleanup = await setupBeads(f.ctx, { warn }); expect(warn).toHaveBeenCalledWith(expect.stringContaining(`OpenCode ${appVersion}`)); - expect(warn).toHaveBeenCalledWith(expect.stringContaining(">=2.1.1 <2.2")); + expect(warn).toHaveBeenCalledWith(expect.stringContaining(">=2.2.6 <2.3")); expect(f.agents.size).toBe(0); expect(f.hooks.size).toBe(0); cleanup(); } }); - test("admits candidate patch hosts within the selected 2.1 line", async () => { - const f = fakeContext({ appVersion: "2.1.8" }); + test("admits candidate patch hosts within the selected 2.2 line", async () => { + const f = fakeContext({ appVersion: "2.2.8" }); const cleanup = await setupBeads(f.ctx, { runtime: { guard: async () => ({ kind: "ready" as const }), prime: async () => "PRIME" }, }); @@ -935,21 +949,26 @@ describe("owned workflow registration", () => { const c = commandFixture(f); const cleanup = await setupBeads(f.ctx, { warn: () => {} }); const update = c.commands().get("oc-beads:update")!; - const stored: SessionContext["messages"] = []; + const stored: StoredSessionMessage[] = []; + const projected: SessionContext["messages"] = []; + const context = mock(async () => stored); + Object.assign(f.ctx.session, { context }); for (const [stem, text, delivery] of [["show", "fixture-1", "queue"], ["update", "append note", "steer"], ["compact", "explain --auto", "queue"], ["sync", "explain only", "steer"]] as const) { await c.commands().get(`oc-beads:${stem}`)!.execute({ ...c.invocation, delivery, prompt: { ...c.invocation.prompt, text } }); const sent = c.prompt.mock.calls.at(-1)![0]; expect(sent.text).toBe(`/oc-beads:${stem} ${text}`); expect(sent.files).toBe(c.invocation.prompt.files); expect(sent.delivery).toBe(delivery); - const metadata = sent.metadata as { ocBeadsWorkflow: unknown }; - stored.push({ role: "user", content: [{ type: "text", text: String(sent.text) }, { type: "text", text: "native prepared attachment/skill + hook edit" }], metadata: stem === "sync" ? { ocBeadsSync: metadata.ocBeadsWorkflow } : metadata } as SessionContext["messages"][number]); + const workflow = (sent.metadata as { ocBeadsWorkflow: { version: number; instructions: string } }).ocBeadsWorkflow; + const messageID = `workflow-${stem}`; + stored.push(storedUserMessage(messageID, String(sent.text), stem === "sync" ? { ocBeadsSync: workflow } : { ocBeadsWorkflow: workflow })); + projected.push(Message.make({ id: messageID, role: "user", content: [Message.text(String(sent.text)), Message.text("native prepared attachment/skill + hook edit")] })); } expect(c.prompt).toHaveBeenCalledTimes(4); - const raw = JSON.stringify(stored); - // Native-window replay decodes lowered messages, preserving per-message metadata. + const raw = JSON.stringify(projected); + const storedRaw = JSON.stringify(stored); for (const kind of ["context", "compaction"]) { - const event = { sessionID: "s1", agent: "build", system: [], messages: JSON.parse(raw) } as ContextEvent; + const event = { sessionID: "s1", agent: "build", system: [], messages: projected.map((message) => Message.make(message)) } as ContextEvent; await f.hooks.get(kind)!(event); const once = JSON.stringify(event.messages); expect(once).not.toContain("ocBeadsSync"); @@ -960,27 +979,124 @@ describe("owned workflow registration", () => { expect(JSON.stringify(event.messages)).toBe(once); expect(event.messages![1]!.content).toContainEqual({ type: "text", text: "/oc-beads:update append note" }); } - // Restoring instruction content must precede session-location and guard holds. + expect(context).toHaveBeenCalledTimes(4); + // A moved session is withheld before native snapshot lookup. Object.assign(f.ctx.session, { get: async () => ({ location: { directory: "/elsewhere" } }) }); - const moved = { sessionID: "s1", agent: "build", system: [], messages: JSON.parse(raw) } as ContextEvent; + const moved = { sessionID: "s1", agent: "build", system: [], messages: projected.map((message) => Message.make(message)) } as ContextEvent; await f.hooks.get("context")!(moved); - expect(JSON.stringify(moved.messages)).toContain("Requested outcome:"); - expect(moved.system).toEqual([]); - expect(JSON.stringify(stored)).toBe(raw); + expect(JSON.stringify(moved.messages)).not.toContain("Requested outcome:"); + expect(JSON.stringify(moved.system)).toContain("Saved Beads workflow instructions or diagnostics are unavailable"); + expect(context).toHaveBeenCalledTimes(4); + expect(JSON.stringify(stored)).toBe(storedRaw); expect([...f.hooks.keys()].sort()).toEqual(["compaction", "context"]); // Summary recent-context uses stored visible text; no promise of verbatim snapshot retention. - const recent = stored.flatMap(m => m.content).map(p => p.type === "text" ? p.text : "").join("\n"); + const recent = stored.filter((message) => message.type === "user").map((message) => message.text).join("\n"); expect(recent).not.toContain("Requested outcome:"); expect(BEADS_COMPACTION_GUIDANCE).toContain("active /oc-beads workflow invocation's requested outcome, unresolved inputs or explicit modes, and applicable authority limits"); const calls = c.prompt.mock.calls.length; await cleanup(); - const unloaded = { sessionID: "s1", agent: "build", system: [], messages: JSON.parse(raw) } as ContextEvent; + const unloaded = { sessionID: "s1", agent: "build", system: [], messages: projected.map((message) => Message.make(message)) } as ContextEvent; await f.hooks.get("context")!(unloaded); expect(JSON.stringify(unloaded.messages)).toBe(raw); await expect(update.execute(c.invocation)).rejects.toThrow("unloaded"); expect(c.prompt).toHaveBeenCalledTimes(calls); }); + test("restores a saved workflow by ID from native context after the SDK projects a metadata-free user message", async () => { + const f = fakeContext({ appVersion: "2.2.6" }); + const c = commandFixture(f); + const order: string[] = []; + const cleanup = await setupBeads(f.ctx, { warn: () => {} }); + await c.commands().get("oc-beads:show")!.execute({ + ...c.invocation, + delivery: "queue", + prompt: { ...c.invocation.prompt, text: "fixture-1 --json" }, + }); + const sent = c.prompt.mock.calls.at(-1)![0]; + const workflow = (sent.metadata as { ocBeadsWorkflow: { version: number; instructions: string } }).ocBeadsWorkflow; + const messageID = "workflow-message-1"; + const get = mock(async () => { + order.push("location"); + return { location: { directory: "/project" } }; + }); + const context = mock(async ({ sessionID }: { sessionID: string }) => { + order.push("context"); + expect(sessionID).toBe(c.invocation.sessionID); + return [{ + id: messageID, + type: "user" as const, + time: { created: 1 }, + text: String(sent.text), + metadata: { ocBeadsWorkflow: workflow }, + }]; + }); + Object.assign(f.ctx.session, { get, context }); + + const projected = Message.make({ id: messageID, role: "user", content: String(sent.text) }); + expect(projected.role).toBe("user"); + expect(Object.hasOwn(projected, "metadata")).toBeFalse(); + const event: ContextEvent = { sessionID: c.invocation.sessionID, agent: "build", system: [], messages: [projected] }; + await f.hooks.get("context")!(event); + + expect(order).toEqual(["location", "context"]); + expect(context).toHaveBeenCalledTimes(1); + expect(event.messages?.[0]?.content[0]).toEqual(Message.text(workflow.instructions)); + expect(event.messages?.[0]?.content[1]).toEqual(Message.text(String(sent.text))); + expect(c.prompt.mock.calls.at(-1)![0].delivery).toBe("queue"); + await cleanup(); + }); + + test("reports unavailable snapshots after a rejected or incomplete native read without re-admission", async () => { + for (const result of ["rejected", "missing"] as const) { + const f = fakeContext(); + const c = commandFixture(f); + const cleanup = await setupBeads(f.ctx, { warn: () => {} }); + await c.commands().get("oc-beads:show")!.execute(c.invocation); + const message = Message.make({ id: `missing-${result}`, role: "user", content: "/oc-beads:show fixture-1" }); + const context = mock(async () => { + if (result === "rejected") throw new Error("fixture native read rejected"); + return []; + }); + Object.assign(f.ctx.session, { context }); + const before = JSON.stringify(message.content); + const event: ContextEvent = { sessionID: "s1", agent: "build", system: [], messages: [message] }; + const promptCount = c.prompt.mock.calls.length; + await f.hooks.get("context")!(event); + expect(JSON.stringify(event.messages?.[0]?.content)).toBe(before); + expect(JSON.stringify(event.system)).toContain("Saved Beads workflow instructions or diagnostics are unavailable"); + expect(context).toHaveBeenCalledTimes(1); + expect(c.prompt).toHaveBeenCalledTimes(promptCount); + await cleanup(); + } + }); + + test("discards a late native snapshot after session generation changes", async () => { + const f = fakeContext(); + const c = commandFixture(f); + const messageID = "moving-workflow"; + const stored = storedUserMessage(messageID, "/oc-beads:show fixture-1", { + ocBeadsWorkflow: { version: 1, instructions: "STALE SNAPSHOT" }, + }); + let release: (messages: StoredSessionMessage[]) => void = () => {}; + const context = mock(async () => new Promise((resolve) => { release = resolve; })); + Object.assign(f.ctx.session, { context }); + const cleanup = await setupBeads(f.ctx, { warn: () => {} }); + const message = Message.make({ id: messageID, role: "user", content: stored.text }); + const before = JSON.stringify(message.content); + const event: ContextEvent = { sessionID: "s1", agent: "build", system: [], messages: [message] }; + const pending = f.hooks.get("context")!(event); + await settle(); + f.events.push({ type: "session.moved", data: { sessionID: "s1" } }); + await settle(); + release([stored]); + await pending; + expect(JSON.stringify(event.messages?.[0]?.content)).toBe(before); + expect(JSON.stringify(event.system)).toContain("Saved Beads workflow instructions or diagnostics are unavailable"); + expect(JSON.stringify(event.messages)).not.toContain("STALE SNAPSHOT"); + expect(context).toHaveBeenCalledTimes(1); + await cleanup(); + }); + test("actual Responses serialization retains mixed workflow and legacy instructions once on native replay", async () => { const f = fakeContext(); const c = commandFixture(f); @@ -996,12 +1112,14 @@ describe("owned workflow registration", () => { const client = make(); const model = configure({ apiKey: "disposable-fixture", baseURL: `${server.url.origin}/v1` }).responses("fixture"); const serialize = async (messages: SessionContext["messages"]) => { - await client.llm.generate({ model, messages }); + await client.generate({ model, messages }); return JSON.stringify(captures.at(-1)); }; try { - const stored: SessionContext["messages"] = []; + const stored: StoredSessionMessage[] = []; + const projected: SessionContext["messages"] = []; const expected: SessionContext["messages"] = []; + Object.assign(f.ctx.session, { context: mock(async () => stored) }); const stems = ["show", "update", "compact", "sync", "diagnostics"]; for (const [index, stem] of stems.entries()) { const details = index === stems.length - 1 ? "current turn exact details" : `fixture-1 ${stem} literal --apply --auto`; @@ -1016,21 +1134,26 @@ describe("owned workflow registration", () => { ); expect(createHash("sha256").update(instructions).digest("hex")).toBe(baseline.syncInstructionsSha256); } - const content = [{ type: "text" as const, text: String(sent.text) }, { type: "text" as const, text: "native file/skill preparation + prompt hook edits" }]; + const messageID = `response-workflow-${stem}`; + const content = [String(sent.text), "native file/skill preparation + prompt hook edits"]; // Legacy retained history uses its stored snapshot, never a current-definition lookup. - const metadata = stem === "sync" ? { ocBeadsSync: snapshot, foreign: true } : { ...sent.metadata as Record, foreign: true }; - stored.push({ role: "user", content, metadata }); - expected.push({ role: "user", content: [{ type: "text", text: snapshot.instructions }, ...content], metadata: { foreign: true } }); + const metadata: StoredUserMessage["metadata"] = stem === "sync" + ? { ocBeadsSync: snapshot, foreign: true } + : { ocBeadsWorkflow: snapshot, foreign: true }; + stored.push(storedUserMessage(messageID, String(sent.text), metadata)); + projected.push(Message.make({ id: messageID, role: "user", content: content.map((text) => Message.text(text)) })); + expected.push(Message.make({ id: messageID, role: "user", content: [snapshot.instructions, ...content].map((text) => Message.text(text)) })); if (index === 0) { - const assistant = { role: "assistant" as const, content: [{ type: "text" as const, text: "interleaved assistant" }] }; - stored.push(assistant); + const assistant = Message.make({ role: "assistant", content: Message.text("interleaved assistant") }); + projected.push(assistant); expected.push(assistant); } } const storage = JSON.stringify(stored); + const projectedStorage = JSON.stringify(projected); let candidate: string | undefined; for (const kind of ["context", "compaction"]) { - const event = { sessionID: "s1", agent: "build", system: [], messages: JSON.parse(storage) } as ContextEvent; + const event = { sessionID: "s1", agent: "build", system: [], messages: projected.map((message) => Message.make(message)) } as ContextEvent; await f.hooks.get(kind)!(event); const serialized = await serialize(event.messages!); candidate ??= serialized; @@ -1043,12 +1166,13 @@ describe("owned workflow registration", () => { await f.hooks.get(kind)!(event); expect(JSON.stringify(event.messages)).toBe(once); expect(await serialize(event.messages!)).toBe(candidate); - const replay = { ...event, messages: JSON.parse(storage) }; + const replay = { ...event, messages: projected.map((message) => Message.make(message)) }; await f.hooks.get(kind)!(replay); expect(await serialize(replay.messages!)).toBe(candidate); } expect(JSON.stringify(stored)).toBe(storage); - const auxiliary = await serialize(stored); + expect(JSON.stringify(projected)).toBe(projectedStorage); + const auxiliary = await serialize(projected); expect(auxiliary).not.toContain("Invoked command:"); expect(auxiliary).not.toContain("Report these observed Beads diagnostics"); expect(auxiliary).not.toMatch(/ocBeadsWorkflow|ocBeadsSync/); @@ -1075,19 +1199,22 @@ describe("owned workflow registration", () => { const report = JSON.parse(snapshot.instructions.split("\n")[1]!); const manifest = await Bun.file(new URL("../package.json", import.meta.url)).json(); expect(report.plugin).toEqual({name: manifest.name, version: manifest.version}); - expect(report.host).toBe("2.1.1"); + expect(report.host).toBe("2.2.6"); expect(prime).not.toHaveBeenCalled(); - const stored = {role: "user" as const, content: [{type:"text" as const, text: String(sent.text)}], metadata}; + const stored = storedUserMessage("diagnostics-message", String(sent.text), { ocBeadsWorkflow: snapshot }); + const request = Message.make({ id: stored.id, role: "user", content: String(sent.text) }); const raw = JSON.stringify(stored); + const context = mock(async () => [stored]); + Object.assign(f.ctx.session, { context }); for (const kind of ["context", "compaction"]) { - const event: ContextEvent = {sessionID:"s1", agent:"build", system:[], messages:[JSON.parse(raw)]}; + const event: ContextEvent = {sessionID:"s1", agent:"build", system:[], messages:[Message.make(request)]}; await f.hooks.get(kind)!(event); expect(event.messages![0]!.content[0]).toEqual({type:"text", text:snapshot.instructions}); expect(JSON.stringify(event.messages)).not.toContain("ocBeadsWorkflow"); expect(JSON.stringify(event.messages).split(snapshot.instructions.split("\n")[0]!)).toHaveLength(2); } for (const kind of ["title", "generate"]) { - const event: ContextEvent = {sessionID:"s1", agent:"build", system:[], messages:[JSON.parse(raw)]}; + const event: ContextEvent = {sessionID:"s1", agent:"build", system:[], messages:[Message.make(request)]}; await f.hooks.get(kind)?.(event); expect(JSON.stringify(event.messages!.map(message => message.content))).not.toContain('Report these observed Beads diagnostics'); } diff --git a/tests/workflows.test.ts b/tests/workflows.test.ts index f357e2d..ec31372 100644 --- a/tests/workflows.test.ts +++ b/tests/workflows.test.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import { Message } from "@opencode/ai/effect"; import baseline from "./fixtures/non-sync-command-baseline.json"; import { describe, expect, mock, test } from "bun:test" import { existsSync, readFileSync } from "node:fs" @@ -15,6 +16,17 @@ import { import type { Plugin } from "@opencode/plugin"; import type { CommandInvocation } from "@opencode/plugin/promise/command"; +type StoredSessionMessage = Awaited>[number]; +type StoredUserMessage = Extract; + +function storedUserMessage(id: string, text: string, metadata?: StoredUserMessage["metadata"]): StoredUserMessage { + return { id, type: "user", time: { created: 1 }, text, ...(metadata === undefined ? {} : { metadata }) }; +} + +function projectedUserMessage(id: string, text: string) { + return Message.make({ id, role: "user", content: text }); +} + type InventoryEntry = { category: string path: string @@ -223,9 +235,12 @@ describe("oc-beads guidance assets", () => { /** Visible bodies and effective static instructions are asserted separately. */ function effectiveText(sent: Record): string { - const messages = [{ role: "user" as const, content: [{ type: "text" as const, text: String(sent.text) }], metadata: sent.metadata as Record }]; - expandBeadsWorkflowContext(messages); - return messages[0]!.content.map(part => part.text).join("\n"); + const id = "workflow-command"; + const metadata = sent.metadata as { ocBeadsWorkflow: { version: number; instructions: string } }; + const stored = storedUserMessage(id, String(sent.text), { ocBeadsWorkflow: metadata.ocBeadsWorkflow }); + const messages = [projectedUserMessage(id, String(sent.text))]; + expandBeadsWorkflowContext(messages, [stored]); + return messages[0]!.content.filter(part => part.type === "text").map(part => part.text).join("\n"); } describe("packaged native workflows", () => { @@ -251,15 +266,17 @@ describe("packaged native workflows", () => { expect(snapshot.instructions.split(report)).toHaveLength(2); expect(snapshot.instructions).toContain("do not run store-opening commands"); expect(snapshot.instructions).toContain("plugin name/version"); - const stored = JSON.stringify(sent); - const messages = [{role: "user" as const, content: [{type: "text" as const, text: String(sent.text)}], metadata: sent.metadata as Record}]; - expandBeadsWorkflowContext(messages); - expect(messages[0]!.content[0]!.text).toBe(snapshot.instructions); - expect(messages[0]!.metadata).toEqual({}); + const storageText = JSON.stringify(sent); + const stored = storedUserMessage("diagnostic-command", String(sent.text), { + ocBeadsWorkflow: (sent.metadata as {ocBeadsWorkflow: {version: number; instructions: string}}).ocBeadsWorkflow, + }); + const messages = [projectedUserMessage(stored.id, stored.text)]; + expandBeadsWorkflowContext(messages, [stored]); + expect(messages[0]!.content[0]).toEqual(Message.text(snapshot.instructions)); const expanded = JSON.stringify(messages); - expandBeadsWorkflowContext(messages); + expandBeadsWorkflowContext(messages, [stored]); expect(JSON.stringify(messages)).toBe(expanded); - expect(JSON.stringify(sent)).toBe(stored); + expect(JSON.stringify(sent)).toBe(storageText); expect(diagnostics).toHaveBeenCalledTimes(count + 1); } } @@ -316,65 +333,71 @@ describe("packaged native workflows", () => { expect(diagnostics).not.toHaveBeenCalled(); }); - test("generalized and legacy snapshots select one retained instruction body and strip valid keys", () => { + test("generalized and legacy snapshots select one stored instruction body without mutating history", () => { const good = (instructions: string) => ({ version: 1, instructions }); const malformed = { version: 1, instructions: 42 }; - for (const [metadata, instructions, remaining] of [ - [{ ocBeadsWorkflow: good("GENERAL") }, "GENERAL", {}], - [{ ocBeadsSync: good("LEGACY") }, "LEGACY", {}], - [{ ocBeadsWorkflow: good("GENERAL"), ocBeadsSync: good("LEGACY") }, "GENERAL", {}], - [{ ocBeadsWorkflow: malformed, ocBeadsSync: good("LEGACY") }, "LEGACY", { ocBeadsWorkflow: malformed }], - [{ ocBeadsWorkflow: good("GENERAL"), ocBeadsSync: malformed }, "GENERAL", { ocBeadsSync: malformed }], + for (const [metadata, instructions] of [ + [{ ocBeadsWorkflow: good("GENERAL") }, "GENERAL"], + [{ ocBeadsSync: good("LEGACY") }, "LEGACY"], + [{ ocBeadsWorkflow: good("GENERAL"), ocBeadsSync: good("LEGACY") }, "GENERAL"], + [{ ocBeadsWorkflow: malformed, ocBeadsSync: good("LEGACY") }, "LEGACY"], + [{ ocBeadsWorkflow: good("GENERAL"), ocBeadsSync: malformed }, "GENERAL"], ] as const) { - const stored = { id: "retained-id", role: "user" as const, content: [{ type: "text" as const, text: "hook-edited /oc-beads:show" }], metadata: { ...metadata, foreign: "keep" } }; + const stored = storedUserMessage("retained-id", "hook-edited /oc-beads:show", { ...metadata, foreign: "keep" }); const raw = JSON.stringify(stored); - const messages = [stored]; - expandBeadsWorkflowContext(messages); - expect(messages[0]!.content).toEqual([{ type: "text", text: instructions }, ...stored.content]); - expect(messages[0]!.metadata as Record).toEqual({ ...remaining, foreign: "keep" }); + const request = projectedUserMessage(stored.id, stored.text); + const messages = [request]; + expect(expandBeadsWorkflowContext(messages, [stored])).toBeFalse(); + expect(messages[0]!.content).toEqual([{ type: "text", text: instructions }, ...request.content]); expect(messages[0]!.id).toBe(stored.id); expect(messages[0]!.role).toBe("user"); expect(JSON.stringify(stored)).toBe(raw); const once = JSON.stringify(messages); - expandBeadsWorkflowContext(messages); + expandBeadsWorkflowContext(messages, [stored]); expect(JSON.stringify(messages)).toBe(once); - const replay = [JSON.parse(raw)]; - expandBeadsWorkflowContext(replay); + const replay = [projectedUserMessage(stored.id, stored.text)]; + expandBeadsWorkflowContext(replay, [stored]); expect(JSON.stringify(replay)).toBe(once); } }); - test("sync expansion preserves hook-edited content, foreign metadata and storage on serialized replay", () => { - const tagged = { id: "native-id", role: "user" as const, content: [{ type: "text" as const, text: "prompt hook edited details" }, { type: "text" as const, text: "prepared skill/file content" }], metadata: { foreign: { keep: true }, ocBeadsSync: { version: 1, instructions: "SNAPSHOT" } } }; - const raw = JSON.stringify(tagged); - const messages = [tagged]; - expandBeadsWorkflowContext(messages); - expect(JSON.stringify(tagged)).toBe(raw); - expect(messages[0]).not.toBe(tagged); + test("sync expansion preserves projected content and durable storage on replay", () => { + const stored = storedUserMessage("native-id", "prompt hook edited details", { + foreign: { keep: true }, ocBeadsSync: { version: 1, instructions: "SNAPSHOT" }, + }); + const raw = JSON.stringify(stored); + const projected = Message.make({ id: stored.id, role: "user", content: [Message.text("prompt hook edited details"), Message.text("prepared skill/file content")] }); + const messages = [projected]; + expect(expandBeadsWorkflowContext(messages, [stored])).toBeFalse(); + expect(JSON.stringify(stored)).toBe(raw); + expect(messages[0]).not.toBe(projected); expect(messages[0]!.id).toBe("native-id"); - expect(messages[0]!.content).toEqual([{ type: "text", text: "SNAPSHOT" }, ...tagged.content]); - expect(messages[0]!.metadata as Record).toEqual({ foreign: { keep: true } }); + expect(JSON.stringify(messages[0]!.content)).toBe(JSON.stringify([Message.text("SNAPSHOT"), ...projected.content])); const once = JSON.stringify(messages); - expandBeadsWorkflowContext(messages); + expandBeadsWorkflowContext(messages, [stored]); expect(JSON.stringify(messages)).toBe(once); - const replay = [JSON.parse(raw)]; - expandBeadsWorkflowContext(replay); + const replay = [Message.make({ id: stored.id, role: "user", content: [Message.text("prompt hook edited details"), Message.text("prepared skill/file content")] })]; + expandBeadsWorkflowContext(replay, [stored]); expect(JSON.stringify(replay)).toBe(once); - for (const message of [ - { ...tagged, role: "assistant" as const }, - { ...tagged, metadata: { ocBeadsSync: { version: 2, instructions: "FUTURE" } } }, - { ...tagged, metadata: { ocBeadsSync: { version: 1, instructions: 1 } } }, - { ...tagged, metadata: { ocBeadsWorkflow: { version: 2, instructions: "FUTURE" } } }, - { ...tagged, metadata: { ocBeadsWorkflow: { version: 1, instructions: null } } }, - { ...tagged, metadata: { ocBeadsWorkflow: null, ocBeadsSync: "invalid", foreign: true } }, - { ...tagged, role: "assistant" as const, metadata: { ocBeadsWorkflow: { version: 1, instructions: "INERT" } } }, - { ...tagged, metadata: {} }, - { role: "user" as const, content: [{ type: "text" as const, text: "/oc-beads:sync" }] }, - ]) { - const untouched = [message]; - expandBeadsWorkflowContext(untouched); - expect(untouched[0]).toBe(message); + const malformed = [ + storedUserMessage("bad-version", "/oc-beads:sync", { ocBeadsSync: { version: 2, instructions: "FUTURE" } }), + storedUserMessage("bad-instructions", "/oc-beads:sync", { ocBeadsWorkflow: { version: 1, instructions: 1 } }), + storedUserMessage("null-snapshot", "/oc-beads:sync", { ocBeadsWorkflow: null, ocBeadsSync: "invalid", foreign: true }), + ]; + for (const record of malformed) { + const request = projectedUserMessage(record.id, record.text); + const untouched = [request]; + expect(expandBeadsWorkflowContext(untouched, [record])).toBeTrue(); + expect(untouched[0]).toBe(request); } + const assistant = Message.make({ id: stored.id, role: "assistant", content: "assistant" }); + const assistantMessages = [assistant]; + expect(expandBeadsWorkflowContext(assistantMessages, [stored])).toBeFalse(); + expect(assistantMessages[0]).toBe(assistant); + const ordinary = projectedUserMessage("ordinary", "plain user input"); + const ordinaryMessages = [ordinary]; + expect(expandBeadsWorkflowContext(ordinaryMessages, [])).toBeFalse(); + expect(ordinaryMessages[0]).toBe(ordinary); }); test("ambiguous sync admission failure propagates once without retry", async () => { From 836efe4892f332b9e57415f11f093912b3e509be Mon Sep 17 00:00:00 2001 From: Matt Jackson Date: Sun, 8 Nov 2026 21:40:25 +0000 Subject: [PATCH 4/7] docs: accept reviewed OpenCode 2.2 source checkpoint --- docs/reviews/claude-opencode-22-source-81x.md | 76 +++++++++++++++++++ .../opencode-22-source-acceptance-5rj1.md | 50 ++++++++++++ openspec/changes/adapt-opencode-2-2/tasks.md | 6 +- 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 docs/reviews/claude-opencode-22-source-81x.md create mode 100644 docs/reviews/opencode-22-source-acceptance-5rj1.md diff --git a/docs/reviews/claude-opencode-22-source-81x.md b/docs/reviews/claude-opencode-22-source-81x.md new file mode 100644 index 0000000..6ab7c8e --- /dev/null +++ b/docs/reviews/claude-opencode-22-source-81x.md @@ -0,0 +1,76 @@ +# Source review: opencode-beads-5rj.1 + +**Candidate:** `ccc9851bce42ec3f5a5b348cfa4cc40e1d93a643`, compared with the accepted base `cd4630c7a4322a8a1439acb714274f6a89ff6c15`. I only read files; nothing was run. + +## Verdicts +- **SOURCE: READY** +- **Standards: READY** +- **Spec (tasks 1.1, 1.2, 2.1 source portion): READY** + +**No blockers found.** + +## What I checked against the contract + +**Metadata transport (1.1)** +- `src/host.ts:236` makes one `ctx.session.context({sessionID})` call per hook run. +- `src/workflows.ts:576-581` builds an ID map that exists only for that request. There is no cache and no per-message lookup. +- Matching uses `message.id` from the published `SessionContext["messages"]` type against the stored record's `id`, with no casts. +- The upstream conversion step keeps user-message IDs (`to-llm-message.ts:236,250,256,258`), which confirms the gap this change is built around. + +**Order of checks** +- In both hooks, the location check (`host.ts:396-406`, `456-466`) runs before the history read. +- After every await, the code checks for unload/cancel (`abort`) and that the request is still current (`host.ts:237-241`, `245-249`, `397-401`, `457-461`). +- `forgetSession` clears the in-flight read marker (`host.ts:170`). So a move, fork or delete during the read throws away a late result. The "late native snapshot" test covers this. + +**Precedence and repeat runs** +- `storedWorkflowInstructions` (`workflows.ts:551-561`) prefers a valid `ocBeadsWorkflow` and falls back to the legacy `ocBeadsSync`. Malformed or wrong-version records are ignored. +- The first-part equality check (`workflows.ts:600`) means running expansion twice adds nothing. +- Stored history is never changed. The request copy is replaced, not edited. + +**Unavailable notice** +- The notice tells the model not to invent missing guidance or replay an operation. It is added at most once per request (`host.ts:220`), and the warning log fires once per session (`workflowDiagnostic`). +- If the read fails, the user's text is left alone and the rest of the guidance still runs (`host.ts:250-251`), as `design.md:17` requires. +- There is no re-submission or retry, and nothing is reconstructed from current command definitions. This matches the owner-settled o66 outcome. + +**Hook scope** +- Only the context and compaction hooks expand snapshots. The title/generate tests confirm they don't. + +**Direct tools (1.2)** +- `DIRECT_BEADS_TOOLS` (`host.ts:48-51`) matches 2.2.6 tool naming (`runtime.ts:290-293`: `${namespace}_${name}`) and the 11 operations in `continuity.ts:55`. +- In 2.2.6, when execute is wholly denied, Code Mode tools are exposed directly (`tool.ts:262-264`). So `registered-direct-visible` describes something the host really does. +- Tool definitions, permissions (`permission: "shell"`), actor handling and Stop behavior are unchanged. +- The guidance text says a rejected operation must not be retried elsewhere, that Stop/Cancel must be honored, and that shell is only available if it is listed separately (`guidance.ts:90-93`). + +**Version range and docs** +- Version admission is `2.2.x` with patch ≥ 6, and the unsupported-version list includes `2.2.5`, `2.3.0` and `2.2.6-beta.1`. The package version is unchanged. +- The README separates the published beta.5 (`>=2.1.1 <2.2`) from the unreleased source and claims no host qualification. +- The README limitation covers the trigger, the impact, that Beads data is unaffected, upstream #57424 and the unresolved `opencode-beads-o66`. +- The outdated "operations disappear when execute is denied" wording is gone from `CODE_MODE.md`. I found no remaining copies of it. + +**Tests** +- Tests build real SDK 2.2.6 messages with `Message.make` and assert that `metadata` is absent. They also assert location-then-history order and exactly one read, and cover rejected/missing reads, a late result after a move, and input/files/delivery preservation. +- These checks would fail if the code still relied on request metadata. They exercise the code against the SDK types, not a real installed 2.2.6 host. + +## Optional advice (not required) +1. **Overlapping requests in one session** (`host.ts:238-240`, `398-401`): if a second request for the same session starts before the first one's read finishes, the first request shows the unavailable notice even though its snapshots were readable. It also skips the rest of its Beads guidance. I haven't shown this can happen, since the 2.2.6 runner seems to run a session's hooks one at a time. Worth watching in 2.2, not fixing now. +2. **Stale diagnostics label** (`host.ts:387`): `typedRegistration` still says `"code-mode-only"`, but 2.2.6 can expose the tools directly. The `leafAvailability` field is correct; the label could just say `"registered: beads namespace"`. +3. **Dead line** (`host.ts:419`): the marker was already deleted at `host.ts:415`. The two hooks also repeat the same location/read block, which could be shared. Cosmetic. +4. **Over-eager notice** (`workflows.ts:567`): `hasWorkflowInvocation` matches any user line starting with `/oc-beads:`, so an ordinary message quoting a command could trigger the notice. This errs toward disclosure, which is acceptable. +5. **Log key not cleared on session reset** (`host.ts:219`): the key format (`session=X`) doesn't match `forgetSession`'s `:X` suffix match, so the warning won't repeat after a move or fork. It's harmless and consistent with "once". + +## Inspected +- The frozen `source.patch` and `tracking.json` +- Current `src/host.ts`, `src/workflows.ts` (through the patch) and `src/guidance.ts` +- The `continuity.ts` tool names and permission option +- The spec, design and tasks files under `openspec/changes/adapt-opencode-2-2/` +- `AGENTS.md`, sampled for rules +- Upstream 2.2.6 `to-llm-message.ts`, `tool.ts` and `tool/runtime.ts` +- The installed `@opencode/ai` 2.2.6 `./effect` export + +## Not executed or not checked +- Typecheck, the 218 tests, the packed smoke test, OpenSpec validation and diff checks. These remain worker claims plus your own reruns. +- The frozen `.tgz`: I didn't check its contents or digest. +- The full plugin `session.context` declaration chain, beyond how the code uses it. +- Task 2.2/2.3: a real 2.2.6 host, model and compaction run, including whether `session.context` IDs match request IDs on an installed host, and the o66 model-switch disclosure. + +READY here doesn't close the 2.2 gate and doesn't authorize push, release or operator changes. diff --git a/docs/reviews/opencode-22-source-acceptance-5rj1.md b/docs/reviews/opencode-22-source-acceptance-5rj1.md new file mode 100644 index 0000000..8691e79 --- /dev/null +++ b/docs/reviews/opencode-22-source-acceptance-5rj1.md @@ -0,0 +1,50 @@ +# Source acceptance — OpenCode 2.2 adaptation + +## Current status + +Parent accepts **source tasks 1.1, 1.2 and 2.1 only** for `opencode-beads-5rj.1`. Runtime source is frozen at `ccc9851bce42ec3f5a5b348cfa4cc40e1d93a643`, against accepted planning/mapping base `cd4630c7a4322a8a1439acb714274f6a89ff6c15`. Tasks 2.2 and 2.3, installed-host qualification and final owner acceptance remain unexecuted and separately gated under `opencode-beads-5rj.2`. The epic and active specification change are not complete. + +Codex execution: `codex:01a1bce9`, session `01a1bce9-a85c-7492-bf4a-8f462c056a12`, observed `gpt-6-luna/max`. The worker completed normally and handed off SOURCE_READY; this document supersedes its parent-review-pending status without rewriting the original handoff. + +## Independent review + +Claude source review record `opencode-beads-81x`, session `8173b43a-85d9-4d0c-b5a3-08e91dbf69df`, observed `claude-opus-5-5`, requested medium effort. Read/Grep/Glob only; final result completed without error or permission denials. The complete tracked candidate hash manifest was unchanged across review. + +- **Standards: READY, no blocking findings.** +- **Spec, source tasks 1.1/1.2/2.1: READY, no blocking findings.** +- Original report preserved verbatim in [claude-opencode-22-source-81x.md](claude-opencode-22-source-81x.md). +- Some workflow line references in that report use source-patch positions rather than actual source lines. Parent inspected the implementation directly: snapshot validation and current/legacy precedence are at `src/workflows.ts:93–103`, and request-local ID matching/expansion at `114–139`. This citation correction does not change the report's substantive findings. + +The review's optional overlapping-request observation is not an established host defect. Its registration-label, redundant-line/extraction, conservative notice recognition and log-key suggestions remain nonblocking; no additional mitigation or refactor was commissioned. `code-mode-only` still describes the native registration option, not a claim that 2.2 cannot expose direct leaves. Per-request leaf availability describes the actual request surface. + +## Execution and preservation evidence + +Parent independently reran: + +- Full suite: **218 pass, 0 fail, 11,883 assertions** across 12 files. +- Exact frozen-archive package smoke: passed; packed public V2 plugin loaded. +- Strict global OpenSpec validation: **4/4 passed**, including the active change. +- Candidate diff whitespace check: passed. + +Typecheck passed in the Codex execution log (`$ tsc --noEmit`, successful command result). This is inspected worker execution evidence, not a separately executed parent typecheck; the parent's gateway compiler scan-size limit was not bypassed. Installed SDK was independently verified as **2.2.6**. + +The worker's retained RED→GREEN receipt isolates the transport failure after the required SDK harness migration and old admission refusal: location alone was observed before the repair; location then one native context read restores snapshots by retained user ID after it. Tests now use SDK metadata-free projected messages and paired durable native records, rather than injecting metadata directly into model messages. These are deterministic source seams, not an installed-host qualification claim. + +Parent compared command callback/payload construction and the workflow purpose/resource inventory byte-for-byte with the accepted base. Continuity operations/CLI invocation, agent definitions, context/cache module, repository guidance and shared Beads skill are unchanged. The accepted deltas are confined to SDK/admission, workflow transport, request capabilities, fixtures and targeted guidance/documentation. No operator configuration/authentication/database changes or upstream patches were made. + +## Frozen artifact handoff + +- File: `snarkipus-opencode-beads-1.1.0-beta.5.tgz` +- SHA-256: `072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836` +- Runtime source: `ccc9851bce42ec3f5a5b348cfa4cc40e1d93a643` +- Parent verified all **31 packed files** are byte-identical to the corresponding committed source files, with exact expected membership. +- Evidence root: `/opt/data/research/opencode-beads-2.2.6-impact/implementation-5rj1/`; worker `implementation-report.md`, `run.log`, parent `parent-tests.log`, `parent-package.log`, and `claude-source-review/` frozen packet/result. + +The internal version remains `1.1.0-beta.5`; this unreleased archive is **not** the published beta.5 artifact. Identify the candidate by source commit and digest. Published beta.5 retains its old `<2.2` admission. The same frozen archive, not a repackaged successor, is the input to task 2.2. + +## Remaining boundaries + +- Native context/real provider, compaction/model-switch, direct/composed/denied native operations, worker identity and Stop/unload qualification remain unrun under 2.2; final owner acceptance is 2.3. +- The native-compaction/model-switch snapshot-access deficiency `opencode-beads-o66` remains unresolved, upstream-blocked and owner-accepted as nonblocking. README and the active spec disclose unavailable guidance, not restored snapshots or a fixed deficiency. +- Source monitoring remains separate under `opencode-beads-rou`. +- The local evidence/task-marker successor changes no packed file and does not replace the artifact. No source push/merge, registry publication, version promotion, Zeus change, operator activation, canonical-spec synchronization or archival is authorized or implied by this source acceptance. diff --git a/openspec/changes/adapt-opencode-2-2/tasks.md b/openspec/changes/adapt-opencode-2-2/tasks.md index bc0343d..54e37e5 100644 --- a/openspec/changes/adapt-opencode-2-2/tasks.md +++ b/openspec/changes/adapt-opencode-2-2/tasks.md @@ -4,12 +4,12 @@ Planning accepted; execution mapping is epic `opencode-beads-5rj`, with source/c ## 1. Adapt the two host seams -- [ ] 1.1 In the existing workflow/host modules, pin SDK 2.2.6, align admission to `>=2.2.6 <2.3`, migrate the test AI import/client calls and resolve retained snapshots through the native current-context API. Move location validation before lookup and assert published message-ID typing without casts. Land the projection-boundary RED→GREEN regression, rejected-read/unavailable-snapshot behavior, current/legacy precedence, diagnostics captured once, exact input/files/delivery and primary/compaction-only replay tests together. Preserve request IDs/content and stored history; verify one request-local read rather than a per-message lookup/cache. Add README limitation coverage: precise model-switch trigger, missing historical instructions/reports, unaffected Beads data, upstream #57424 and unresolved `opencode-beads-o66`; distinguish unreleased source from published beta.5. Completion: focused tests pass on the actual installed SDK, no unsupported host registers and user-visible documentation describes the deficiency without claiming it fixed. -- [ ] 1.2 Update capability derivation and shared guidance for composed tools, native direct Beads leaves and independently available shell. Keep the eleven tool definitions, permissions and actor contract unchanged. Land representative tests/docs for execute allowed, execute denied with direct leaves available, and shell wholly denied; include an operation-specific rejection and Stop behavior. Completion: request-catalog fixtures demonstrate truthful availability without direct re-registration, bypass or shell-inside-execute advice. +- [x] 1.1 In the existing workflow/host modules, pin SDK 2.2.6, align admission to `>=2.2.6 <2.3`, migrate the test AI import/client calls and resolve retained snapshots through the native current-context API. Move location validation before lookup and assert published message-ID typing without casts. Land the projection-boundary RED→GREEN regression, rejected-read/unavailable-snapshot behavior, current/legacy precedence, diagnostics captured once, exact input/files/delivery and primary/compaction-only replay tests together. Preserve request IDs/content and stored history; verify one request-local read rather than a per-message lookup/cache. Add README limitation coverage: precise model-switch trigger, missing historical instructions/reports, unaffected Beads data, upstream #57424 and unresolved `opencode-beads-o66`; distinguish unreleased source from published beta.5. Completion: focused tests pass on the actual installed SDK, no unsupported host registers and user-visible documentation describes the deficiency without claiming it fixed. +- [x] 1.2 Update capability derivation and shared guidance for composed tools, native direct Beads leaves and independently available shell. Keep the eleven tool definitions, permissions and actor contract unchanged. Land representative tests/docs for execute allowed, execute denied with direct leaves available, and shell wholly denied; include an operation-specific rejection and Stop behavior. Completion: request-catalog fixtures demonstrate truthful availability without direct re-registration, bypass or shell-inside-execute advice. ## 2. Verify one candidate and accept the change -- [ ] 2.1 After 1.1–1.2, run typecheck, the existing full tests, packed-install smoke, strict change/global OpenSpec validation and diff hygiene. Compare command purposes/resources, CLI argv and agent definitions against the accepted baseline; explain only intended changes. Freeze one local package artifact with source identity/digest for 2.2. Completion: normal gates pass and the handoff lists actual checks, preserved surfaces and remaining live gates; no registry publication. +- [x] 2.1 After 1.1–1.2, run typecheck, the existing full tests, packed-install smoke, strict change/global OpenSpec validation and diff hygiene. Compare command purposes/resources, CLI argv and agent definitions against the accepted baseline; explain only intended changes. Freeze one local package artifact with source identity/digest for 2.2. Completion: normal gates pass and the handoff lists actual checks, preserved surfaces and remaining live gates; no registry publication. - [ ] 2.2 Qualify that same artifact on exact OpenCode 2.2.6 with isolated HOME/XDG/database, an isolated Beads fixture and an authorized provider route. Use one bounded scenario set: command/diagnostics through stored context and provider request, legacy/current snapshots, primary/compaction versus auxiliary exclusion, native composed/direct/denied operations, scoped worker identity and Stop/unload. Cover summary/native replay; specifically, after native compaction on A then switching to non-replayable B, pre-compaction plain transcript snapshots absent from latest context must be **disclosed unavailable**, while accessible snapshots expand. Record that as a verified limitation, not restored snapshots or a fixed deficiency. A controlled-provider fixture can exercise those model identities; do not add a second genuine-provider campaign. Reuse deterministic per-command/argv coverage; one representative genuine-model workflow is sufficient, not 30 model runs or a provider/variant matrix. Completion: retained evidence identifies passes, verified limitations, failures and unexecuted checks, without operator auth/data/config changes; unavailable provisioning leaves this gate pending. - [ ] 2.3 Review the frozen delta and 2.1–2.2 evidence for contract correctness and proportionality; resolve demonstrated adapter defects only. Reconcile current docs and task markers from accepted evidence, leaving limitations/unexecuted checks explicit and `opencode-beads-o66` unresolved. Completion: owner receives the candidate, source/installed-host verdicts and concise stable-2.0 migration/release recommendation; source readiness is not publication, activation or deficiency-resolution approval. From f22177939b65ad95e6cf6b7cdb049c07447fd8aa Mon Sep 17 00:00:00 2001 From: Matt Jackson Date: Sun, 8 Nov 2026 23:05:11 +0000 Subject: [PATCH 5/7] docs: add proportional engineering agent guidance --- AGENTS.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 88a20c2..2c744bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,25 @@ - JSDoc comments for function documentation - Async/await for async operations +## Proportional engineering + +- Before adding complexity for a speculative or vanishingly unlikely race or + security edge case, explain the concrete failure mode, likelihood, and + complexity cost to the user and get their buy-in. Do not silently expand scope + for theoretical robustness. +- Validate external inputs at the boundary that owns them; trust values already + guaranteed by supported schemas or internal contracts. Preserve integration + responsibilities without duplicating upstream internals or speculative repairs. +- Add helpers, wrappers or abstractions only for actual reuse, a genuinely complex + boundary, or a meaningful concept that improves readability. Do not extract + single-use helpers or introduce frameworks preemptively. +- Test the actual changed implementation and dispatch path, not a duplicate of its + logic in the test. Prefer small realistic fixtures; mock external effects when + isolation is useful, without replacing the behavior being verified. + +Adapted selectively from [OpenCode's guidance](https://github.com/anomalyco/opencode/blob/84578e123ffd31e31a174457e7350961da4fabbc/AGENTS.md); +its repository-specific tools, architecture and style rules do not apply here. + ## Project Structure - `src/plugin.ts` - OpenCode V2 plugin entry point (default export `{ id, setup }`) - `src/host.ts` - Wires the context hook, session events, and agent registration From 06eb523b821beff689c9ec60340f729ef3b040e1 Mon Sep 17 00:00:00 2001 From: Matt Jackson Date: Mon, 9 Nov 2026 00:16:25 +0000 Subject: [PATCH 6/7] docs: accept qualified OpenCode 2.2 candidate after final Opus review --- docs/opencode-22-qualification-5rj2.md | 55 +++++++++++ docs/opencode-sdk-contract.md | 4 +- .../claude-opencode-22-final-recheck-tjd.md | 33 +++++++ docs/reviews/claude-opencode-22-final-tjd.md | 50 ++++++++++ ...encode-22-controlled-qualification-5rj2.md | 80 ++++++++++++++++ .../opencode-22-genuine-workflow-5rj2.md | 95 +++++++++++++++++++ .../opencode-22-mechanics-completion-5rj2.md | 70 ++++++++++++++ openspec/changes/adapt-opencode-2-2/tasks.md | 4 +- tests/docs.test.ts | 5 +- 9 files changed, 391 insertions(+), 5 deletions(-) create mode 100644 docs/opencode-22-qualification-5rj2.md create mode 100644 docs/reviews/claude-opencode-22-final-recheck-tjd.md create mode 100644 docs/reviews/claude-opencode-22-final-tjd.md create mode 100644 docs/reviews/opencode-22-controlled-qualification-5rj2.md create mode 100644 docs/reviews/opencode-22-genuine-workflow-5rj2.md create mode 100644 docs/reviews/opencode-22-mechanics-completion-5rj2.md diff --git a/docs/opencode-22-qualification-5rj2.md b/docs/opencode-22-qualification-5rj2.md new file mode 100644 index 0000000..2e47023 --- /dev/null +++ b/docs/opencode-22-qualification-5rj2.md @@ -0,0 +1,55 @@ +# OpenCode 2.2.6 qualification — parent accepted + +## Current verdict + +**Source and the selected installed-host qualification are accepted locally.** Tasks 2.2 and 2.3 under `opencode-beads-5rj.2` are complete, following the previously accepted source tasks 1.1, 1.2 and 2.1. **The owner-required final Claude Opus 5.5 gate returned Standards READY / Spec READY after one documentation-only correction and same-session recheck.** Parent acceptance uses retained native evidence, independent offline checks and current cleanup/preservation readback—not worker exit codes alone. + +- **Standards: READY.** Qualification used the public frozen package and supported host interfaces; production code, permissions and attribution were not modified to make probes pass. No upstream patch, retry/routing framework or expanded host/provider matrix was added. +- **Spec: READY for the five selected tasks.** Source checks, controlled native mechanics and one genuine model-selected workflow cover their respective boundaries. Known limitations remain explicit rather than being relabelled fixes. The [original final review](reviews/claude-opencode-22-final-tjd.md) retains its documentation-only REVISE; the [same-session recheck](reviews/claude-opencode-22-final-recheck-tjd.md) records F1 resolved and final READY. Actual reviewer model/session and no-error/no-denial completion were verified; all 201 corrected candidate file hashes remained unchanged across recheck. The status/marker/report additions in this local closeout are evidence-only successors, not runtime changes. +- The execution epic remains open for canonical-spec synchronization/archive and its separately authorized publication. This verdict does not authorize source push/merge, a registry release, changing npm tags or operator activation. + +## Exact candidate + +- Runtime source: `ccc9851bce42ec3f5a5b348cfa4cc40e1d93a643`. +- Source acceptance: [review and parent evidence](reviews/opencode-22-source-acceptance-5rj1.md). +- Qualification checkout: `f22177939b65ad95e6cf6b7cdb049c07447fd8aa`; its successors to runtime source contain acceptance evidence and agent-tooling guidance, not packed runtime changes. +- Archive: `snarkipus-opencode-beads-1.1.0-beta.5.tgz`. +- SHA-256: `072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836`. +- All 31 installed package files match that archive. The internal beta.5 version does **not** identify the published beta.5 artifact. +- Actual CLI/server qualification target: **OpenCode 2.2.6**. Beads fixture CLI: **1.3.4**; the supported minimum remains 1.3.1, with prior deterministic minimum-version evidence reused. + +## Evidence and acceptance boundaries + +The following original reports are retained verbatim. Their worker-time pending/partial verdicts describe their own handoffs; this current parent summary integrates the later evidence without rewriting those observations. + +1. [Initial controlled installed-host pass — historical partial handoff](reviews/opencode-22-controlled-qualification-5rj2.md): compact native command admission, actual storage/projector/provider snapshot transport, current/legacy snapshots, once-captured diagnostics replay, auxiliary generate exclusion, native namespace discovery, composed/direct operations, independent permissions, denied-target nonexecution, scoped native child identity, summary/native replay, Stop and actual plugin disposal. Parent independently reran its **33 raw-evidence assertions**. +2. [Completion of title and pending-disposal mechanics](reviews/opencode-22-mechanics-completion-5rj2.md): actual native title-provider frame excluded snapshots while the matching primary request expanded once; a genuinely pending diagnostics callback was aborted by the original product cleanup before held real subprocess output was released. No late prompt or provider call occurred. Parent independently reran **22 assertions**, verified prior/source hashes and checked current owned-process/listener absence. +3. [One genuine model-selected workflow](reviews/opencode-22-genuine-workflow-5rj2.md): native **OpenAI subscription / gpt-6.1-sol / high**, session `ses_e42640b98ffeC3CF3XhLJXcA5I`. An ordinary `/oc-beads:comments` request led the model to load the installed skill, discover native signatures, get the target and append the exact requested note through `beads.note`. Separate once-only permissions protected get and note. Independent readback established exactly one append, preserved original target fields/authorship/assignment and an unchanged sentinel. The final assistant report completed truthfully. Parent independently reran **29 assertions** and verified current cleanup and source/prior-evidence hashes. + +The genuine run is one sample, not a model reliability certification or a new all-command campaign. Its observation-only request contexts are pre-serializer evidence, not genuine wire captures; the actual controlled-provider transport observations cover that separate boundary. The genuine run used a fresh user-completed native isolated login, not copied operator credentials, a credential translator or a controlled-provider response. The five primary steps belong to the one requested workflow, not five separately prompted samples. + +The unchanged source suite, typecheck, exact-package smoke and strict specifications were consumed from accepted source evidence rather than repeated for each runtime pass. The suite had 218 passing tests and 11,883 assertions. Qualification worker: `codex:86c72145`, actual Codex session `01a1bd7e-5bd2-7720-88f3-870d909d432b`, observed Astra/medium; that is separate from the genuine OpenCode model/actor. + +Durable raw evidence and offline analyzers are under `/opt/data/research/opencode-beads-2.2.6-impact/qualification-5rj2/`, with `completion-pass/` and `genuine-pass/` separating the bounded continuations. Parent checked the original 197 tracked-file baseline and each pass's preserved earlier evidence before making this intentional documentation/marker successor. + +## Limitations retained + +- **Frozen README status:** `README.md:24` still says the candidate has not passed live host qualification, and its compatibility-table row at line 202 says `2.2.6 (source target; host gate pending)`. Those are frozen packed text, superseded by this summary for archive SHA-256 `072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836`. Update both README statements and their `tests/docs.test.ts` compatibility-row assertion in the separately authorized 2.0.0 metadata change; do not mutate or silently repackage this qualified archive. + +- **`opencode-beads-o66` remains unresolved and upstream-blocked.** After native compaction on A then switching to non-replayable B, old ordinary transcript commands can lack accessible saved snapshots. The controlled native run verified the selected outcome: explicit unavailable-guidance disclosure, expansion of accessible snapshots, and no invented historical instructions. This is a verified limitation, not restored old snapshots or a fixed deficiency. README and the active specification retain it. +- Stop aborted the selected waiting operation and prevented its write, but the old host permission entry remained listed through session deletion. That stronger retirement observation remains failed historical evidence; no target execution, bypass or adapter repair requirement is inferred. The genuine run's final execution/permission inventories were empty. +- Original malformed controlled-compaction responses and the first unload sample's too-early callback return remain preserved as invalid/inconclusive observations. The separately identified fixture correction and actual pending-callback measurement supply the passing evidence; they do not retroactively turn the original attempts green. +- Source monitoring remains separate under `opencode-beads-rou`. No quantitative cost or context-savings claim is made. + +## Cleanup and next release decision + +Owned host/provider/fixture processes and loopback listeners were removed; transient server secrets/private startup logs were deleted. Source/archive and scoped operator fingerprints were preserved. The fresh authenticated native test profile remains private at `/opt/data/cache/scratch/oc-beads-22-genuine` for parent custody; its real credential database was not copied into evidence or deleted. No Zeus or operator service/configuration change occurred. + +Recommended next steps, under their own authority: + +- Synchronize the accepted active deltas into canonical specs, rehearse/validate archive, then publish the reviewed source and specification closeout. +- Prepare **stable 2.0.0**, not a silent 1.x promotion: new host admission is `>=2.2.6 <2.3`; there is no parallel 2.1 compatibility layer. Document upgrading OpenCode first and installing the new plugin deliberately. +- Preserve existing stable/beta exact pins for users staying on older hosts. Until a trusted release is authorized and verified, npm `latest=1.0.2` and `next=1.1.0-beta.5` remain the previously published releases, not this local candidate. +- A future version/migration-note change creates a new release archive identity and needs ordinary release checks/provenance; it does not erase this exact-candidate qualification or silently authorize another live campaign. + +Parent acceptance is not package publication, operator activation, canonical archival or resolution of the known deficiency. diff --git a/docs/opencode-sdk-contract.md b/docs/opencode-sdk-contract.md index 6244fed..a2a50de 100644 --- a/docs/opencode-sdk-contract.md +++ b/docs/opencode-sdk-contract.md @@ -2,7 +2,7 @@ ## Unreleased source adaptation — OpenCode 2.2.6 -This local source candidate pins development types to `@opencode/plugin` `2.2.6`, declares optional peer range `@opencode/plugin` `>=2.2.6 <2.3`, and admits hosts in that range. The 2.2.6 runtime candidate has not been qualified. Package version remains `1.1.0-beta.5`; the published beta.5 artifact retains its historical `>=2.1.1 <2.2` range. This source change is not a release or operator activation. +This local source candidate pins development types to `@opencode/plugin` `2.2.6`, declares optional peer range `@opencode/plugin` `>=2.2.6 <2.3`, and admits hosts in that range. Exact OpenCode 2.2.6 qualification is parent-accepted for archive SHA-256 `072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836`; see [current qualification](opencode-22-qualification-5rj2.md). The `opencode-beads-o66` deficiency remains unresolved. Package version remains `1.1.0-beta.5`; the published beta.5 artifact retains its historical `>=2.1.1 <2.2` range. This source change is not a release or operator activation. With the installed 2.2.6 SDK, OpenCode projects durable user records into `@opencode/ai` request `Message` values without their metadata while retaining message IDs. The candidate checks session location before calling the supported `ctx.session.context({ sessionID })` API, then matches each projected typed user-message ID to the durable `SessionMessageInfo` record. One request-local read builds the ID map; no snapshot cache or alternate message API is used. The current `ocBeadsWorkflow` snapshot takes precedence over legacy `ocBeadsSync`; workflow instructions and captured diagnostics are expanded only by the existing primary and compaction hooks. A failed or incomplete native read reports guidance unavailable and leaves ordinary input intact. Location, generation and cancellation are checked at the existing host boundary. @@ -166,7 +166,7 @@ Reviewed on 2026-10-11 for the narrow OpenCode boundary used by the published st ## Versions and sources - Historical stable 1.0.2 declarations used optional peer range `@opencode/plugin` `>=2.0.26 <3` and development dependency `2.0.26`. The only import is type-only (`import type { Plugin } from "@opencode/plugin"`), so packages install nothing from OpenCode and use the host's runtime. -- Published beta.5 declarations use optional peer range `@opencode/plugin` `>=2.1.1 <2.2` and development typing dependency `@opencode/plugin` `2.1.1`; exact 2.1.1 has bounded accepted evidence for that artifact. This checkout's unreleased source candidate uses `@opencode/plugin` `>=2.2.6 <2.3` and `2.2.6` for development typing only. The 2.2.6 runtime candidate has not been qualified. +- Published beta.5 declarations use optional peer range `@opencode/plugin` `>=2.1.1 <2.2` and development typing dependency `@opencode/plugin` `2.1.1`; exact 2.1.1 has bounded accepted evidence for that artifact. This checkout's unreleased source candidate uses `@opencode/plugin` `>=2.2.6 <2.3` and `2.2.6` for development typing only. Exact OpenCode 2.2.6 qualification is parent-accepted for archive SHA-256 `072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836`; see [current qualification](opencode-22-qualification-5rj2.md). The `opencode-beads-o66` deficiency remains unresolved. - Authoritative sources: the installed `@opencode/plugin` 2.0.26 declarations, the [plugin guide](https://opencode.ai/v2/docs/build/plugins/), the [V1 migration guide](https://opencode.ai/v2/docs/build/plugins/migrate-v1/), [plugin configuration](https://opencode.ai/v2/docs/plugins/), the [skills reference](https://opencode.ai/v2/docs/skills/), the published OpenAPI document (`https://opencode.ai/v2/openapi.json`), and `@opencode/schema` 2.0.26 for event names. - Qualification: a throwaway V2 plugin (`opencode-beads-2zs.1`) and the actual source plugin were loaded into disposable projects on OpenCode 2.0.26. Results were read with `opencode debug agents`, a JSONL trace from the plugin, and short `opencode run` sessions. Test sessions were deleted afterwards. diff --git a/docs/reviews/claude-opencode-22-final-recheck-tjd.md b/docs/reviews/claude-opencode-22-final-recheck-tjd.md new file mode 100644 index 0000000..292c4c8 --- /dev/null +++ b/docs/reviews/claude-opencode-22-final-recheck-tjd.md @@ -0,0 +1,33 @@ +# F1 recheck: final SOURCE+QUALIFICATION gate (`opencode-beads-tjd`, session `71dc3fce-9fb7-40a3-a7eb-c833ba95d080`) + +I read `f1-correction.patch`, grepped the current docs, and looked at the parent's test log. I ran nothing and wrote nothing. + +## Verdicts + +- **Standards: READY.** The correction only touches docs and one test. It adds no runtime change, no packed-file change and no new mechanism. +- **Spec (corrected final gate): READY.** F1 is resolved, and the correction introduced no material regression. + +## F1: resolved + +- **SDK contract:** `docs/opencode-sdk-contract.md:5` and `:169` now say exact OpenCode 2.2.6 qualification is parent-accepted for the full archive SHA-256 `072d9c29…ac836`. Both link to `opencode-22-qualification-5rj2.md` and say `opencode-beads-o66` is still unresolved. A grep of `docs/` finds no remaining "has not been qualified" sentence. The wording doesn't overclaim: it doesn't call the candidate released, and the published beta.5 range and "not a release or operator activation" are still stated in the same paragraph. +- **Tests:** in `tests/docs.test.ts`, the old assertion is replaced by checks for the accepted-status phrase, the digest and the link. A new negative check makes sure the obsolete wording can't come back. The README table assertion at `:215` is deliberately left as it was, which is correct because the README is packed and frozen. +- **Summary:** the new "Frozen README status" bullet at `docs/opencode-22-qualification-5rj2.md:37`: + - names `README.md:24` and `:202` exactly; + - says this summary supersedes them for that exact digest; + - assigns updating both README statements and their compatibility-row test to the separately authorized 2.0.0 metadata change; + - forbids changing or silently repackaging the qualified archive. + + That covers the task 2.3 duty to reconcile docs while leaving the remaining gap explicit. Nothing else in the summary changed. + +## Regression check + +- **Packed files and archive:** the three changed files are all outside the package. `package.json` `files` covers only `src`, `assets/oc-beads`, `README.md` and `LICENSE`. So the 31-file archive identity is unaffected, which matches the parent's byte-identity and SHA check. +- **Tests:** `f1-green-tests.log:245-248` shows 218 pass, 0 fail, 11,888 `expect()` calls, across 12 files. The parent's red log shows the updated assertion first failing at `docs.test.ts:212`, as expected. +- **Wording:** the new text doesn't contradict the README's o66 limitation, the published beta.5 range or the 2.0.0 release recommendation. + +## Optional (nonblocking) + +- The assertion count went up by 5, while the patch's net change is +3 explicit `expect` lines. This most likely comes from shared helpers or iterated checks. It doesn't affect the verdict. +- The earlier optional observations still stand and are still nonblocking. + +READY here closes the final review of this exact runtime and qualification candidate, so parent can now accept task 2.3. It does not approve publication, release, version or metadata changes, canonical sync or archive, CI, or operator activation, and it does not resolve `o66`. diff --git a/docs/reviews/claude-opencode-22-final-tjd.md b/docs/reviews/claude-opencode-22-final-tjd.md new file mode 100644 index 0000000..6d94920 --- /dev/null +++ b/docs/reviews/claude-opencode-22-final-tjd.md @@ -0,0 +1,50 @@ +# Final SOURCE+QUALIFICATION gate — `opencode-beads-tjd` (session `71dc3fce-9fb7-40a3-a7eb-c833ba95d080`) + +**Candidate:** runtime `ccc9851`, HEAD `f221779`, plus the uncommitted qualification docs and the task 2.2 marker. Archive SHA-256 is `072d9c29…ac836`. I only used Read, Grep and Glob. I did not run, edit or change anything. + +## Verdicts + +- **Standards: READY.** No findings. +- **Spec (final source+qualification gate): REVISE.** There is one finding, and it is documentation only. The runtime, archive and evidence need no change. + +## Material finding + +**F1 — Current docs still say the candidate is unqualified. Task 2.3 requires reconciling them.** + +- **What the contract says:** task 2.3 (`openspec/changes/adapt-opencode-2-2/tasks.md:14`) requires "Reconcile current docs and task markers from accepted evidence, leaving limitations/unexecuted checks explicit." +- **What the summary says:** `docs/opencode-22-qualification-5rj2.md:5` says the selected installed-host qualification on 2.2.6 is parent-accepted. +- **What the other current docs still say:** + - `docs/opencode-sdk-contract.md:5` — "The 2.2.6 runtime candidate has not been qualified." + - `docs/opencode-sdk-contract.md:169` — the same sentence again. + - `README.md:24` — "The candidate has not passed live host qualification." + - `README.md:202` — the compatibility table row reads "2.2.6 (source target; host gate pending)". + - `tests/docs.test.ts:212,215` pin both the SDK-contract sentence and the README table row as they are. +- **Consequence:** the repository's own SDK contract and README contradict the accepted qualification summary. If parent marks 2.3 complete now, the record would claim docs are reconciled when two current docs still say the opposite. Readers or the 2.0.0 release work could pick up a wrong "unqualified/pending" status. This understates the evidence rather than overclaiming it, so it is not a safety problem. It is still a gap against the task contract. +- **Packaging constraint:** `README.md` is one of the packed files (`package.json:44-49`). Editing it changes the archive, so it can't be fixed in this frozen candidate. `docs/` is not packed. +- **Smallest correction (no runtime or archive change):** + 1. Update `docs/opencode-sdk-contract.md:5,169` to say exact 2.2.6 qualification is parent-accepted for this archive digest, link the summary, and restate that `o66` is unresolved. Update `tests/docs.test.ts:212` to match. + 2. Add one line to the summary's limitations or next-steps section. It should say that the README text at lines 24 and 202 is frozen packed text, superseded by the summary for this digest, and will be updated in the separately authorized 2.0.0 metadata change, together with `tests/docs.test.ts:215`. + +A recheck only needs to confirm those two edits and that the 31-file archive identity is unchanged. + +## What I checked and found sound + +- **Pending-unload abort (completion pass).** `completion-pass/timeline.jsonl:4-8` shows this order: + 1. The callback was confirmed pending while the real child had already exited (`realChildExists:false`, `commandReturned:false`). + 2. Config removal, then "Beads workflow unloaded" about 271 ms later. + 3. The shim was gone before `hold-release`. + + `analyze.py:31-36` independently checks the order of cleanup completion and hold release, that no output was released or forwarded for the held PID, that it happened before the ordinary timeout, and that there was no late admission or provider frame. These checks are meaningful and are not a relabel of the earlier too-early sample. The initial analyzer at `analyze.py:52` still records that sample as unqualified. +- **Denied, child-session and Stop outcomes (initial pass).** The denied leaf was rejected and the independent read was acknowledged (`analyze.py:33-34`). The child session had a distinct actor (`:37`). The final fixture readback contains exactly `CONTROLLED_NOTE_5RJ2\nCHILD_NOTE_5RJ2` (`:49`), so neither the denied write nor the Stop-interrupted write landed. The failed permission-entry retirement is still recorded as a failure (`:48`) and was not turned into a repair requirement. That matches the owner-settled scope. +- **o66 limitation behavior.** After the A→B switch, old message IDs are absent, the notice appears exactly once, current snapshots expand twice, and nothing is reconstructed (`analyze.py:42-44`). This matches `specs/workflow-access/spec.md:205-209` and `specs/work-continuity/spec.md:30`. The malformed first compaction is kept as failed (`:38`). +- **Genuine sample.** `genuine-pass/readback-receipts.json:11` shows the target notes as `ORIGINAL_TARGET_NOTE\nGENUINE_NOTE_5RJ2_ONCE` with no assignee, and the creator kept. The sentinel at `:23` still has its original `updated_at`. The report keeps its evidence boundaries clear: pre-serializer contexts are not wire captures, there was one sample, and there was no retry. This fits the task 2.2 limit of one representative genuine workflow. +- **Frozen patch scope.** `tracked.patch` touches only the SDK/admission, workflow-transport, guidance, test, doc and AGENTS files, consistent with the accepted source review. Version stays `1.1.0-beta.5`. The Beads minimum stays 1.3.1. +- **Release recommendation.** It is proportionate and scoped correctly: stable 2.0.0, a new host floor, existing exact pins kept, OpenCode upgraded before the plugin, and npm tags unchanged (`qualification-5rj2.md:46-51`). It doesn't claim any future metadata, CI or archive work is done. + +## Optional observations (nonblocking) + +1. **Initial report's final verdict.** `analyze.py:55` still prints `BLOCKED/PARTIAL`, and `:54` asserts the genuine gate was blocked. That is correct historical output. Anyone rerunning it should read it together with the summary's integration paragraph (`qualification-5rj2.md:23`), which already explains it. +2. **Abort timing check.** `completion-pass/analyze.py:35` compares the abort timing against a hard-coded 1000 ms rather than the product's actual timeout value. That's fine for this evidence, since the observed ~271 ms is well inside it, but citing the real timeout would make the assertion clearer. +3. **File counts.** The summary's "197 tracked-file baseline" plus the 4 new docs adds up to the manifest's 201. They are consistent, but the summary doesn't state the 201 total, so a reader has to work it out. + +READY was not granted here. Even once granted, it would not authorize publication, release, a canonical sync or archive, or operator activation. diff --git a/docs/reviews/opencode-22-controlled-qualification-5rj2.md b/docs/reviews/opencode-22-controlled-qualification-5rj2.md new file mode 100644 index 0000000..6148869 --- /dev/null +++ b/docs/reviews/opencode-22-controlled-qualification-5rj2.md @@ -0,0 +1,80 @@ +# OpenCode 2.2.6 local qualification — BLOCKED/PARTIAL + +Executor: `codex:86c72145`; actual native Codex session UUID `01a1bd7e-5bd2-7720-88f3-870d909d432b`. The actor suffix is the parent-generated execution token, not a native session claim. Requested worker model/effort: Codex/Astra medium. This report does not independently attest the orchestration model banner. Native Codex performed qualification; it was not an OpenCode agent session. + +Work began 2026-11-08 23:29 UTC and completed within the 45-minute bound. Only `opencode-beads-5rj.2` was claimed/checkpointed, with the explicit actor and `--sandbox`; heartbeat used the supported command. It remains `in_progress` for parent review. Tasks 2.2/2.3 checkboxes, acceptance, closure, assignment and sync remain parent-owned. + +## Candidate and isolation + +- Branch `work/adapt-opencode-2-2-5rj1`; HEAD `f22177939b65ad95e6cf6b7cdb049c07447fd8aa`. +- Runtime source `ccc9851bce42ec3f5a5b348cfa4cc40e1d93a643`; source acceptance is `docs/reviews/opencode-22-source-acceptance-5rj1.md`. Evidence/tooling successors do not replace the runtime. +- Used the frozen archive `/opt/data/research/opencode-beads-2.2.6-impact/implementation-5rj1/snarkipus-opencode-beads-1.1.0-beta.5.tgz`, SHA256 `072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836`. Extracted the public package unchanged; all 31 installed files match the archive. No repack or source installation. The internal beta.5 version is not published beta.5 bytes. +- Actual executable `/usr/local/bin/opencode`, resolved to `/opt/hermes/image-toolchain/npm/node_modules/@opencode/cli/bin/opencode.exe`. CLI and owned server both report **2.2.6**; executable fingerprint is retained. SDK/client package is the existing installed 2.2.6 contract. +- Explicit isolated HOME/XDG config/cache/data/state and TMPDIR under `/opt/data/cache/scratch/oc-beads-22-5rj2`. Disposable committed Git project and embedded Beads 1.3.4 fixture at `tmp/project`; explicit `BEADS_DIR`, no tracker redirect or remote. `preflight-corrected.json` retains successful actual `bd context --json`, seed and prime before any provider request. Seed audit actor is `codex:86c72145`; fixture item `q22-r36` remained open/unassigned. +- Owned foreground/private server bound only to `127.0.0.1:54973`; controlled HTTP provider bound only to `127.0.0.1:44367`. No managed service, operator server, Zeus transport or global install was used. +- `harness/observer-plugin.ts` forwards and awaits the unmodified public plugin setup/cleanup; it records four lifecycle timestamps only. It does not replace dependencies, results or cancellation. + +The source suite (218 tests / 11,883 assertions), typecheck, exact packed smoke and strict 4/4 evidence were reused, not rerun. No per-command/argv matrix was added. + +## Controlled installed-host observations + +All provider frames are **CONTROLLED**, not genuine inference. Provider ID `controlled`, native package `@opencode/ai/providers/openai`; models `model-a` and `model-b`, native variant `default`, no reasoning effort configured. Twenty-eight actual outgoing HTTP requests were captured without headers; native tool calls/results and permission requests are retained. Fixture token counts are synthetic and support no usage, quality or cost claim. + +| Case | Outcome | Evidence | +| --- | --- | --- | +| Ordinary command admission and expansion | Passed: one compact persisted user invocation, one static snapshot in its first provider input, no serialized routing key | `ordinary-context.json`, `provider-001.json` | +| Diagnostics capture/replay | Passed transport: durable captured report reappears byte-identically in later primary input; no new diagnostics invocation on replay | `diagnostics-context.json`, frames 002–003; source once-capture evidence reused | +| Legacy/current snapshots | Passed: legacy v1 snapshot expands; new command uses current v1 metadata | `legacy-context.json`, frame 003 | +| Auxiliary generate exclusion | Passed: native session.generate request lacks action/diagnostic/legacy expansion | `generate-result.json`, frame 004 | +| Native title exclusion | Unrun; no independent title-provider frame was established | No title pass inferred from generate | +| Native code-mode catalog and operations | Passed: host search returned real signatures; composed get/note returned actual CLI outcomes | `catalog-context.json`, `composed-allowed-context.json`, frames 005–008 | +| Independent permissions | Passed: separate get/note asks, distinct permission IDs, exact CLI resources, source call correlation, `save: []`, once replies | `permissions.jsonl`, `native-events.jsonl` | +| Denied inner operation | Passed nonexecution: note rejected, independent read approved/completed; outer execute completed with inner error and host continued | `composed-denied-context.json`, frames 009–010; no whole-step-termination claim | +| Direct fallback | Passed: execute denied; real provider catalog exposes direct Beads leaves; actual beads_get succeeds under separate shell-equivalent permission | `direct-inventory-context.json`, `direct-get-context.json`, frames 011–013 | +| Scoped native child identity | Passed bounded observation: native child session with parentID executed direct note with a distinct derived actor; seed authorship and assignment preserved | `session-child.json`, `child-note-context.json`, frame 014; this is not a genuine delegated reasoning/handoff assessment | +| Native compaction and A replay | Passed corrected fixture: real host compaction hook/request, stored native replacement and provider replay | `corrected-native-compaction-context.json`, frames 022–023 | +| Native A → non-replayable B | **Verified limitation**: earlier plain command transcript restored; old snapshot IDs absent from native latest context; unavailable notice emitted, accessible current snapshots expanded | `corrected-native-switch-b-context.json`, frame 024 | +| Summary compaction/replay | Passed corrected fixture transport: stored summary and compact recent text, current snapshot expansion on replay | `corrected-summary-compaction-context.json`, frames 025–026; fixture summary is not semantic model evidence | +| Stop at permission wait | Passed selected nonexecution/no-restart behavior: actual pending ask, native interrupt, aborted tool/step, idle execution, no stopped note | `stop-*.json`, frame 027, final fixture readback | +| Permission request retirement after Stop | **Failed stronger observation**: the stopped request remained listed through session deletion | `stop-pending-after.json`, `after-session-removal-permissions.json`; no repair, no claim that the inventory was empty | +| Actual plugin disposal | Passed lifecycle observation: watched-config removal caused original cleanup entry/return, then native command registry lacked package commands | `lifecycle.jsonl`, `unloaded-commands.json`, `unloaded-plugins.json` | +| Pending diagnostics callback during unload | **Unrun/inconclusive timing**: command returned before cleanup; no pending-callback cancellation pass | `case-timeline.jsonl`; command return 1794181147283, cleanup start 1794181147474, completion 1794181147478 | +| Genuine model workflow | **Blocked/unrun**: no authorized account-managed path supporting the required isolated state | `genuine-readiness.json`; zero isolated accounts, zero genuine requests | + +Native sessions: + +- Main: `ses_e427c565bffeO1o7GcXCdTA5Zk`, actor `opencode:qualifier:ff5a4b12d522`. +- Direct/Stop: `ses_e427adc94ffeKTHBN4K0GHT1zX`, actor `opencode:qualifier:433d578cb780`. +- Child and unload IDs, exact native agents/models and derived child actor are retained in `session-child.json`, `child-note-context.json`, `session-unload.json` and `permissions.jsonl`. + +The native host executors legitimately use their derived OpenCode session actors on fixture writes. These do not replace the qualification worker's `codex:86c72145` coordination actor. Independent CLI readback shows exactly `CONTROLLED_NOTE_5RJ2\nCHILD_NOTE_5RJ2`, unchanged seed author, no assignee, and neither denied nor stopped note. + +The B-switch result is the accepted unresolved deficiency `opencode-beads-o66` / upstream #57424, **not recovery of old snapshots, a fix, or a new downstream repair blocker**. The retained native window is used only by A; B receives earlier ordinary user transcript plus explicit unavailable disclosure. No Beads data or stored history was rewritten to manufacture restoration. + +## Preserved setup failures and limits + +1. Initial `bd context` rejected the fixture outside TMPDIR because installed Beads blocks `/opt` except supported temporary roots. Moving the disposable project beneath the already configured isolated TMPDIR corrected the setup; no CLI/guard patch. Original preflight and narrow probes remain. The pinned reference inspected was `https://raw.githubusercontent.com/gastownhall/beads/7ef2fd05e/internal/beads/context.go`; a retained copy is in `harness/`. +2. Initial plugin config pointed to a file; this host expects a package directory. Before inference, the observer became a directory with a package `./server` export. Native registry then settled to 30 package commands plus 3 built-ins. A first snapshot before settlement showed only built-ins; it was not a dispatch pass. Bun was unavailable on PATH, so the existing generated Promise client ran with installed Node 26.12.1; no installation occurred. +3. Original provider fixture incorrectly answered native compaction with text and summary compaction without mandatory headings. Frames 016, 019–020 and native failed compaction records remain. The host's automatic summary nudge is retained. Only the controlled fixture response protocol was corrected, plus explicit `keep.tokens=0` to select the required missing-old-snapshot case. No product prompt/source was tuned. Original and corrected provider scripts remain; only the owned fixture provider process was replaced, not the OpenCode server. Later corrected frames are separately numbered. +4. Initial offline analyzer assumed one global show instruction copy despite four separate durable show invocations. The corrected assertion derives multiplicity from stored user snapshots. A second assertion expected permission retirement after Stop and failed against actual retained inventory. That stronger failed observation remains explicit; selected Stop nonexecution is verified independently. No host rerun or guarantee silently waived. +5. Session cleanup initially removed the parent before its child, so the host's cascading removal produced SessionNotFound for the child. The remaining cleanup recorded that absence and removed the unload session. The stopped permission entry remained in the host registry until owned process/state teardown. + +No authorized genuine route was available in isolated state. No operator credentials/database were copied, selected, imported, translated, refreshed or authenticated. No alternative public/free/paid account or provider was chosen. Inspecting the fresh isolated credential list found zero entries. Existing Codex inference is not OpenCode model qualification. The smallest missing prerequisite is an explicitly authorized account-managed genuine OpenCode route that supports these isolated-state constraints without changing credential custody. + +## Cleanup and preservation + +`cleanup.json` records terminated owned host PID 45210, controlled provider PID 46715, and retired provider PID 45209, all absent. Both loopback listeners refuse connections. `owned-process-census.json` finds no process with a cwd beneath the scratch root. Only owned process groups were signalled. Private native logs, transient server password and isolated OpenCode data/state/cache were removed. No real provider credential entered the fixture. The fixture/public dummy provider key was removed from final config; retained source contains only its clearly labelled nonsecret constant. + +All 197 protected repository paths match the supplied manifest. Captured operator executable and known JSON/JSONC config fingerprints match; no operator credential/database content was inspected or fingerprinted. This is a scoped fingerprint claim, not a census of every possible operator file. All 31 extracted package files match the exact archive. No existing tracked file changed; only the optional new concise repository qualification report is added. No commit, push, tag, release, publication, tracker sync, canonical spec sync/archive or upstream posting occurred. + +## Parent verification and remaining gates + +Run, offline and without restarting a host: + +```sh +python3 /opt/data/research/opencode-beads-2.2.6-impact/qualification-5rj2/analyze.py +``` + +The analyzer checks **33 assertions against raw evidence**, including preservation of the failed permission-retirement observation and the unqualified pending-unload timing. It prints **BLOCKED/PARTIAL** even when assertions succeed. `analysis.json`, `case-index.json`, `installed-manifest.json`, provider frames, native contexts/events, permission records and `harness/` support review. `result.md` is reserved for the parent CLI's final summary and is not this report. + +Task 2.2 is not complete: genuine workflow is blocked; pending diagnostics unload and native title exclusion are unrun. Parent should review the useful controlled mechanics and the retained host permission-inventory observation, then arrange the smallest authorized genuine route and explicitly scope any remaining live check. Task 2.3 final acceptance, marker reconciliation and issue closure remain parent-owned. Stable 2.0 preparation/publication or operator activation is not recommended as accepted from this partial gate; preserve the frozen source and existing pins pending that review. diff --git a/docs/reviews/opencode-22-genuine-workflow-5rj2.md b/docs/reviews/opencode-22-genuine-workflow-5rj2.md new file mode 100644 index 0000000..d333915 --- /dev/null +++ b/docs/reviews/opencode-22-genuine-workflow-5rj2.md @@ -0,0 +1,95 @@ +# Genuine workflow — GENUINE_WORKFLOW_READY + +The single authorized genuine workflow completed on actual OpenCode **2.2.6**, native **OpenAI subscription / gpt-6.1-sol / high**, using the exact frozen public package. Independent CLI readback confirms exactly one requested note append, preserved prior notes and target fields, and an unchanged sentinel. The completed nonempty model report agrees with the acknowledged operation. Cleanup and preservation checks pass. + +This is one bounded observation, not broad model reliability or parent acceptance of tasks 2.2/2.3. Parent owns marker reconciliation, final acceptance, closure and sync. `opencode-beads-5rj.2` remains in progress under `codex:86c72145`. No source publication, release or operator activation is authorized by this result. + +## Identity and custody + +- Qualification worker: same `codex:86c72145`, native Codex session `01a1bd7e-5bd2-7720-88f3-870d909d432b`, requested Astra/medium. Its actor suffix is the parent execution token, separate from the native session UUID. +- Native OpenCode sample session: `ses_e42640b98ffeC3CF3XhLJXcA5I`, agent `workflow_eval`, actual derived fixture executor `opencode:workflow_eval:b266ee9b97e7`. +- Runtime source `ccc9851bce42ec3f5a5b348cfa4cc40e1d93a643`; HEAD `f22177939b65ad95e6cf6b7cdb049c07447fd8aa`. +- Frozen archive `/opt/data/research/opencode-beads-2.2.6-impact/implementation-5rj1/snarkipus-opencode-beads-1.1.0-beta.5.tgz`, SHA256 `072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836`. All 31 installed files match. Internal beta.5 is not the published beta.5 artifact. +- Native model discovery reported enabled `openai/gpt-6.1-sol`, including variant `high`, package `@opencode/ai/providers/openai/responses`. Every observed request context and native assistant record uses that exact provider/model/variant. +- The parent's frozen `completion-pass/native-auth-ready.json` establishes fresh user-completed native `chatgpt-headless` OAuth in this own profile. No credential value was read, selected, hashed, exported, copied, translated or manually refreshed by this worker. The nullable optional method column was not changed. + +The workflow used `/opt/data/cache/scratch/oc-beads-22-genuine` with the exact authorized HOME/XDG/TMPDIR values and `OPENCODE_DB=.../data/qualification.db`. That DB was neither moved nor copied. An owned foreground server (PID 50153) listened on loopback port 52403; no concurrent server or operator service was used. + +The fresh committed Git/embedded Beads fixture is `.../tmp/workflow`, beneath declared TMPDIR, with explicit fixture BEADS_DIR. Actual `bd context --json` and prime passed before inference. Seed writes used `codex:86c72145`. Target `g22-1f2` began with `ORIGINAL_TARGET_NOTE`; sentinel `g22-mv0` began with `ORIGINAL_SENTINEL_NOTE`; neither was assigned. Exact before records are retained separately. + +## The one native workflow + +The existing comments purpose permits appending a requested comment/checkpoint without replacing prior notes. Before inference, the selected native command and exact user text were fixed as: + +```text +/oc-beads:comments Append exactly GENUINE_NOTE_5RJ2_ONCE to the notes/checkpoint of g22-1f2. Preserve its existing notes. Do not claim, close, sync, or change other fields or issues. Report the actual result and stop. +``` + +This was one ordinary public `session.command` invocation, not direct invocation of plugin hooks. The model was not given literal tool code or signatures. The session had an explicit title and the title agent was disabled; no auxiliary title inference or separate smoke request occurred. Eight steps were available; the workflow completed in five. + +Observed model choices: + +1. Loaded the installed `oc-beads` skill. +2. Used native code-mode namespace search to discover real Beads signatures. +3. Called `beads.get` on `g22-1f2`. +4. Called `beads.note` with exactly `GENUINE_NOTE_5RJ2_ONCE`. +5. Returned a completed, nonempty final report and stopped. + +The three native permission asks were independently handled with **once**, without saved grants: packaged skill access, exact target read, and exact target append. The get and note each had their own shell-equivalent permission ID and `save: []`; source call IDs match their actual native tool records. The note permission resource was: + +```text +bd update g22-1f2 --append-notes GENUINE_NOTE_5RJ2_ONCE --json --actor opencode:workflow_eval:b266ee9b97e7 --sandbox +``` + +The fixture permission evaluator parses shell tokens and allows only the exact target append with the actual actor, fixture-owned reads, packaged skill access and exact `git status --short`. It rejects other-target writes, claims, shell chaining and auth-DB reads. No broad auto-approval was enabled. No unrelated operation was requested or approved. The model did not request Git completion work in this sample. + +The native append result is **acknowledged**, with target, actor and updated issue. Independent post-run `bd --readonly show` verifies: + +```text +ORIGINAL_TARGET_NOTE +GENUINE_NOTE_5RJ2_ONCE +``` + +The new note occurs exactly once. All target fields except notes, `updated_at` and the expected record revision are identical to before, including audit author, absence of assignee/owner, status and priority. The sentinel's entire parsed record is identical. No claim, close, sync or additional issue write occurred. + +Actual final model text: + +> Appended exactly `GENUINE_NOTE_5RJ2_ONCE` to `g22-1f2`, preserving `ORIGINAL_TARGET_NOTE`. The append was acknowledged; no claim, close, or sync was performed. + +The native execution succeeded without a retry or failed-step event. The first valid outcome was retained; no prompt retuning, second attempt, model substitution, controlled response or source fix occurred. + +## Evidence boundaries + +`context.json` contains one compact native user message with its durable v1 instruction snapshot. The observation-only plugin records five actual primary request contexts after public package expansion; each contains that exact snapshot once and the actual model/agent/tool catalog. It does not modify requests or results, inspect headers/credential options, replace the provider, intercept TLS or use a relay. The public package itself is configured directly from the extracted archive directory, not through checkout source or replacement dependencies. + +These are pre-serializer context observations, **not genuine wire-body captures**. Already accepted controlled-host serializer/transport evidence is reused for that boundary. Native stored assistant records carry five distinct real provider response IDs, exact model/variant, tool results and usage. This genuine pass assesses the actual model-selected workflow and durable outcome; it does not repeat earlier title, compaction, Stop or disposal campaigns. + +Native usage fields total 19,211 input, 58,368 cache-read and 177 output tokens, with zero recorded reasoning/cache-write tokens. These are the host's reported fields, not independent billing measurements; no cost or savings inference is made. The five primary request/response steps are not five separately prompted samples. + +Evidence index: + +| Evidence | Purpose | +| --- | --- | +| `identity.json`, `model-readiness.json`, `server.json`, `session-*.json`, `executor.json` | Exact archive/host/model/session/actor identities | +| `environment.json`, `preflight.json`, `fixture-ids.json`, `*-before.json` | Isolated context and independent seeded baselines | +| `commands.json`, `plugins.json`, `admission.json` | Native public package activation and one command | +| `request-contexts.jsonl`, `context.json`, `native-events.jsonl` | Actual expanded contexts, genuine native messages/results and completion | +| `permissions.jsonl`, `permission.py` | Exact requests, source correlation and scoped decisions | +| `*-after.json`, `readback-receipts.json` | Independent durable outcomes | +| `cleanup.json`, `private-path-modes-*.json`, `fingerprints-*.json`, `installed-manifest.json` | Cleanup, retained auth custody and preservation | + +## Cleanup and preservation + +Owned host PID 50153 is absent; loopback port 52403 refuses connections; no process remains with a cwd under the fixture. Final execution and permission inventories are empty, with no saved grants. The transient server password and private startup logs were deleted. **The native authenticated DB and its session history remain in the authorized profile for the parent**; its contents were not read, hashed, exported, copied or deleted. Recorded private path modes are unchanged. + +All **196 earlier evidence files**, **197 protected repository files**, the existing untracked repository qualification summary, captured executable fingerprints and all **31 installed archive files** match. No repository file was edited. Earlier partial/completion findings remain immutable. No source gates or previous host campaigns were rerun; no siblings, sync, markers, close/reassign, commits/push, releases, upstream posting or operator changes. + +## Parent offline verification + +```sh +python3 /opt/data/research/opencode-beads-2.2.6-impact/qualification-5rj2/genuine-pass/analyze.py +``` + +The analyzer checks 29 assertions against raw native evidence, exact readbacks, identity, actual permission-evaluator boundaries, cleanup and preservation. It performs no inference, host startup or auth access. `analysis.json` preserves its result. `case-index.json` records the selected workflow outcome. `genuine-report.md` is this detailed handoff; `result.md` remains reserved for the CLI-owned final summary. + +**GENUINE_WORKFLOW_READY**, pending parent review and full task 2.2/2.3 acceptance. The accepted unresolved `o66` limitation and earlier host observations are neither fixed nor superseded by this append-only sample. diff --git a/docs/reviews/opencode-22-mechanics-completion-5rj2.md b/docs/reviews/opencode-22-mechanics-completion-5rj2.md new file mode 100644 index 0000000..9d9e46b --- /dev/null +++ b/docs/reviews/opencode-22-mechanics-completion-5rj2.md @@ -0,0 +1,70 @@ +# Two-mechanics completion — TWO_MECHANICS_READY + +This bounded continuation supplies the two missing controlled-host measurements: **native title-request snapshot exclusion** and **cancellation of an actually pending diagnostics invocation at product plugin disposal**. Both passed. This is not full task 2.2/2.3 acceptance: the genuine-model workflow remains separately pending with the parent. No genuine inference or account/authentication work occurred. + +Executor `codex:86c72145`, unchanged native Codex session UUID `01a1bd7e-5bd2-7720-88f3-870d909d432b`; the actor suffix is the parent execution token. Requested model/effort remains Astra/medium. Same actor claimed normally, read back `in_progress`, and used explicit `--sandbox` for checkpoints/heartbeat. Parent retains assignment, synchronization, markers, acceptance and closure. Work started 2026-11-08 23:50:36 UTC and finished inside the 20-minute bound. + +## Exact inputs and isolation + +HEAD `f22177939b65ad95e6cf6b7cdb049c07447fd8aa`; runtime `ccc9851bce42ec3f5a5b348cfa4cc40e1d93a643`. The unchanged frozen archive is `/opt/data/research/opencode-beads-2.2.6-impact/implementation-5rj1/snarkipus-opencode-beads-1.1.0-beta.5.tgz`, SHA256 `072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836`. All 31 extracted files match it. Internal beta.5 does not identify the published beta.5 bytes. No source installation, repack, installs or production changes. + +Actual installed CLI and owned foreground server report **OpenCode 2.2.6**. Used the existing Node 26 Promise client and public package `./server` directory export. A lifecycle-only observer forwards and awaits the archive's original setup and cleanup without changing context, dependencies, results, hooks or abort behavior. + +New scratch root only: `/opt/data/cache/scratch/oc-beads-22-5rj2-completion`. Explicit isolated HOME/XDG config/cache/data/state/TMPDIR; disposable committed Git and embedded Beads fixture at `.../tmp/project`, beneath declared `TMPDIR` (`.../tmp`). `BEADS_DIR` is explicitly `.../tmp/project/.beads`; actual `bd context --json` passed before inference. Fixture init used `codex:86c72145`; no issue writes were needed. `isolated-environment.json` and `preflight.json` record the exact environment and CLI outcomes. + +Owned host PID 48839 listened on `127.0.0.1:45205`; owned controlled provider PID 48838 on `127.0.0.1:44577`. Provider `controlled`, model `completion`, native package `@opencode/ai/providers/openai`, variant default, no reasoning effort configured. The native title agent was explicitly routed to this same controlled model, avoiding any implicit alternative provider selection. **Exactly two outgoing provider frames**, both CONTROLLED; fixture token values are synthetic and establish no cost, usage or model-quality claim. + +## Native title exclusion — passed + +Untitled native root session `ses_e426bc4d1ffenWOvPhD2ZPvMLg` received one actual `/oc-beads:show` command, with exact free text `Explain only the workflow; do not read or modify issues. TITLE_CASE_5RJ2`. No plugin hook was invoked directly. + +- `title-context.json` contains compact native user text and a durable current v1 workflow snapshot. +- `provider-001.json` is the actual native title request: stock “Here is the first message…” / “Reply with only the title” framing contains the compact command, zero workflow instruction expansions, no routing metadata and no tools. +- `provider-002.json` is the normal primary request: the identical durable snapshot appears exactly once. This provides a positive expansion comparison in the same native session. +- `provider-001-response.json` records the controlled title response. `native-events.jsonl` records native `session.renamed`, and `title-session-after.json` confirms `CONTROLLED completion title`. + +This closes the native title mechanics gap independently of the earlier accepted generate exclusion. It is a representative ordinary-command snapshot observation, not a semantic title-quality claim or a new all-command campaign. + +## Actually pending diagnostics at disposal — passed + +Native session `ses_e426bc367ffenwuNM2xvM112DT` received an actual `/oc-beads:diagnostics` invocation. A fixture-local PATH shim invokes the real installed `/usr/local/bin/bd`, captures its exact stdout/stderr/exit, and normally forwards them unchanged. For this one diagnostic `context --json`, it held output **after the real subprocess completed**. The shim has no signal handlers, abort mechanism, substitute result or suppression logic. It would forward the original bytes when released if still alive. + +The observed sequence (epoch milliseconds): + +| Observation | Time | +| --- | ---: | +| Native diagnostics command started | 1794181971099 | +| Real `bd context --json` child 48984 completed, exit 0 | 1794181971231.655 | +| Shim 48982 held completed real output | 1794181971231.7756 | +| Callback confirmed pending; shim live, real child absent | 1794181971251 | +| Watched config removed the plugin | 1794181971251 | +| Original product cleanup entered | 1794181971516 | +| Original product cleanup returned | 1794181971520 | +| Native command returned `CommandExecutionError: Beads workflow unloaded` | 1794181971522 | +| Shim confirmed absent, then harness released hold | 1794181971552 | + +`bd-subprocess.jsonl` preserves the real successful context JSON and its SHA256. The diagnostic's result was pending across actual cleanup. The shim disappeared before release, with neither output-forwarded nor output-released event; the product owns cancellation. This occurred roughly 290 ms after holding output, well below the unmodified 10-second subprocess timeout. No harness signal was sent to the shim. Later cleanup signalled only the owned host/provider groups, after this observation was complete. + +`disposal-context.json` and `disposal-inbox.json` are empty; no user prompt was admitted and no diagnostics provider frame followed. `commands-after.json` confirms package commands were removed from the native registry. `lifecycle.jsonl`, `timeline.jsonl`, `held-process.json` and `native-events.jsonl` retain the correlation. This is actual watched-config plugin-scope disposal, not location reload, a registry-only inference, Stop, or custom cancellation. + +The previous sample in the frozen partial report still remains timing-inconclusive; it has not been relabelled or edited. + +## Cleanup and preservation + +`cleanup.json` confirms all 12 recorded owned host/provider/shim/real-child PIDs absent, both listeners refusing connections, and no process with a cwd under the new scratch root. Owned sessions were removed; final active execution and permission inventories were empty. Private startup logs, the transient server password and isolated OpenCode data/state/cache were removed. No operator credential was inspected, selected, copied, imported, translated, refreshed or authenticated. No genuine call occurred. The fixture-only dummy API key is a nonsecret constant; it was removed from final scratch config. + +All **146 prior durable evidence files** match `prior-evidence-manifest.json`. All **197 protected repository files** match the parent manifest. Captured installed executable/CLI/config fingerprints and the existing repository qualification summary match before/after. All 31 installed archive files match. Old scratch fixtures and raw evidence were not written. No repository file was edited in this continuation. Original compaction failures, stale Stop permission observation and incomplete unload remain preserved historical evidence. + +No full source gates or earlier passing campaign were rerun; no source fixes, mitigation, upstream changes, expanded matrix, tracker sync, sibling work, markers, closure/reassignment, commits/push, releases or operator runtime changes. + +## Offline parent verification + +Run without a host, provider or credentials: + +```sh +python3 /opt/data/research/opencode-beads-2.2.6-impact/qualification-5rj2/completion-pass/analyze.py +``` + +The analyzer checks **22 assertions** against retained raw provider/native/subprocess/timing evidence, archive identity, installed file equality, cleanup and preservation. `analysis.json` retains its output. `harness/` contains the exact scripts and transparent observer/shim; `case-index.json` labels these two passed mechanics and the separately pending genuine gate. No `passed:true`-only surrogate is used. The detailed report is this file; `result.md` is reserved for the CLI final summary. + +**Handoff: TWO_MECHANICS_READY.** Leave `opencode-beads-5rj.2` in progress. Parent should review these two observations and integrate accepted status separately; the genuine workflow and full qualification/owner acceptance are not established by this pass. diff --git a/openspec/changes/adapt-opencode-2-2/tasks.md b/openspec/changes/adapt-opencode-2-2/tasks.md index 54e37e5..383feae 100644 --- a/openspec/changes/adapt-opencode-2-2/tasks.md +++ b/openspec/changes/adapt-opencode-2-2/tasks.md @@ -10,8 +10,8 @@ Planning accepted; execution mapping is epic `opencode-beads-5rj`, with source/c ## 2. Verify one candidate and accept the change - [x] 2.1 After 1.1–1.2, run typecheck, the existing full tests, packed-install smoke, strict change/global OpenSpec validation and diff hygiene. Compare command purposes/resources, CLI argv and agent definitions against the accepted baseline; explain only intended changes. Freeze one local package artifact with source identity/digest for 2.2. Completion: normal gates pass and the handoff lists actual checks, preserved surfaces and remaining live gates; no registry publication. -- [ ] 2.2 Qualify that same artifact on exact OpenCode 2.2.6 with isolated HOME/XDG/database, an isolated Beads fixture and an authorized provider route. Use one bounded scenario set: command/diagnostics through stored context and provider request, legacy/current snapshots, primary/compaction versus auxiliary exclusion, native composed/direct/denied operations, scoped worker identity and Stop/unload. Cover summary/native replay; specifically, after native compaction on A then switching to non-replayable B, pre-compaction plain transcript snapshots absent from latest context must be **disclosed unavailable**, while accessible snapshots expand. Record that as a verified limitation, not restored snapshots or a fixed deficiency. A controlled-provider fixture can exercise those model identities; do not add a second genuine-provider campaign. Reuse deterministic per-command/argv coverage; one representative genuine-model workflow is sufficient, not 30 model runs or a provider/variant matrix. Completion: retained evidence identifies passes, verified limitations, failures and unexecuted checks, without operator auth/data/config changes; unavailable provisioning leaves this gate pending. -- [ ] 2.3 Review the frozen delta and 2.1–2.2 evidence for contract correctness and proportionality; resolve demonstrated adapter defects only. Reconcile current docs and task markers from accepted evidence, leaving limitations/unexecuted checks explicit and `opencode-beads-o66` unresolved. Completion: owner receives the candidate, source/installed-host verdicts and concise stable-2.0 migration/release recommendation; source readiness is not publication, activation or deficiency-resolution approval. +- [x] 2.2 Qualify that same artifact on exact OpenCode 2.2.6 with isolated HOME/XDG/database, an isolated Beads fixture and an authorized provider route. Use one bounded scenario set: command/diagnostics through stored context and provider request, legacy/current snapshots, primary/compaction versus auxiliary exclusion, native composed/direct/denied operations, scoped worker identity and Stop/unload. Cover summary/native replay; specifically, after native compaction on A then switching to non-replayable B, pre-compaction plain transcript snapshots absent from latest context must be **disclosed unavailable**, while accessible snapshots expand. Record that as a verified limitation, not restored snapshots or a fixed deficiency. A controlled-provider fixture can exercise those model identities; do not add a second genuine-provider campaign. Reuse deterministic per-command/argv coverage; one representative genuine-model workflow is sufficient, not 30 model runs or a provider/variant matrix. Completion: retained evidence identifies passes, verified limitations, failures and unexecuted checks, without operator auth/data/config changes; unavailable provisioning leaves this gate pending. +- [x] 2.3 Review the frozen delta and 2.1–2.2 evidence for contract correctness and proportionality; resolve demonstrated adapter defects only. Reconcile current docs and task markers from accepted evidence, leaving limitations/unexecuted checks explicit and `opencode-beads-o66` unresolved. Completion: owner receives the candidate, source/installed-host verdicts and concise stable-2.0 migration/release recommendation; source readiness is not publication, activation or deficiency-resolution approval. ## Workflow follow-up diff --git a/tests/docs.test.ts b/tests/docs.test.ts index 6348b2a..634922c 100644 --- a/tests/docs.test.ts +++ b/tests/docs.test.ts @@ -209,7 +209,10 @@ describe("documentation contracts", () => { expect(sdkContract).toContain("`2.2.6` for development typing only"); expect(sdkContract).toContain("`/server`, then ``"); expect(sdkContract).toContain("`session.compaction.ended`"); - expect(sdkContract).toContain("The 2.2.6 runtime candidate has not been qualified"); + expect(sdkContract).toContain("Exact OpenCode 2.2.6 qualification is parent-accepted"); + expect(sdkContract).toContain("072d9c29ef89fa2697109be072798e79cf4af622111266de40ef65e7950ac836"); + expect(sdkContract).toContain("[current qualification](opencode-22-qualification-5rj2.md)"); + expect(sdkContract).not.toContain("The 2.2.6 runtime candidate has not been qualified"); expect(sdkContract).not.toContain("1.18."); expect(readme).toContain("| OpenCode (published beta.5) | `>=2.1.1 <2.2` | 2.1.1 (bounded accepted evidence) |"); expect(readme).toContain("| OpenCode (unreleased source candidate) | `>=2.2.6 <2.3` | 2.2.6 (source target; host gate pending) |"); From 631e683f26a07de644fd5fa91d5fcda3cf47de5e Mon Sep 17 00:00:00 2001 From: Matt Jackson Date: Mon, 9 Nov 2026 04:52:46 +0000 Subject: [PATCH 7/7] docs: sync canonical specs and archive OpenCode 2.2 adaptation --- docs/opencode-22-qualification-5rj2.md | 9 ++-- .../.openspec.yaml | 0 .../2026-11-09-adapt-opencode-2-2}/design.md | 0 .../proposal.md | 0 .../specs/work-continuity/spec.md | 0 .../specs/workflow-access/spec.md | 0 .../2026-11-09-adapt-opencode-2-2}/tasks.md | 2 + openspec/specs/work-continuity/spec.md | 5 ++- openspec/specs/workflow-access/spec.md | 43 ++++++++++++++----- 9 files changed, 43 insertions(+), 16 deletions(-) rename openspec/changes/{adapt-opencode-2-2 => archive/2026-11-09-adapt-opencode-2-2}/.openspec.yaml (100%) rename openspec/changes/{adapt-opencode-2-2 => archive/2026-11-09-adapt-opencode-2-2}/design.md (100%) rename openspec/changes/{adapt-opencode-2-2 => archive/2026-11-09-adapt-opencode-2-2}/proposal.md (100%) rename openspec/changes/{adapt-opencode-2-2 => archive/2026-11-09-adapt-opencode-2-2}/specs/work-continuity/spec.md (100%) rename openspec/changes/{adapt-opencode-2-2 => archive/2026-11-09-adapt-opencode-2-2}/specs/workflow-access/spec.md (100%) rename openspec/changes/{adapt-opencode-2-2 => archive/2026-11-09-adapt-opencode-2-2}/tasks.md (93%) diff --git a/docs/opencode-22-qualification-5rj2.md b/docs/opencode-22-qualification-5rj2.md index 2e47023..1cd20bf 100644 --- a/docs/opencode-22-qualification-5rj2.md +++ b/docs/opencode-22-qualification-5rj2.md @@ -6,7 +6,7 @@ - **Standards: READY.** Qualification used the public frozen package and supported host interfaces; production code, permissions and attribution were not modified to make probes pass. No upstream patch, retry/routing framework or expanded host/provider matrix was added. - **Spec: READY for the five selected tasks.** Source checks, controlled native mechanics and one genuine model-selected workflow cover their respective boundaries. Known limitations remain explicit rather than being relabelled fixes. The [original final review](reviews/claude-opencode-22-final-tjd.md) retains its documentation-only REVISE; the [same-session recheck](reviews/claude-opencode-22-final-recheck-tjd.md) records F1 resolved and final READY. Actual reviewer model/session and no-error/no-denial completion were verified; all 201 corrected candidate file hashes remained unchanged across recheck. The status/marker/report additions in this local closeout are evidence-only successors, not runtime changes. -- The execution epic remains open for canonical-spec synchronization/archive and its separately authorized publication. This verdict does not authorize source push/merge, a registry release, changing npm tags or operator activation. +- The owner subsequently authorized canonical-spec synchronization/archive and source publication. The accepted deltas are synchronized into the canonical specs and the completed change is [archived](../openspec/changes/archive/2026-11-09-adapt-opencode-2-2/proposal.md). Source publication follows the normal PR/CI path; registry release, npm-tag changes and operator activation remain separately gated. Publication identities and execution-epic closure are recorded in Beads, not inferred from the earlier review verdict. ## Exact candidate @@ -45,11 +45,12 @@ Durable raw evidence and offline analyzers are under `/opt/data/research/opencod Owned host/provider/fixture processes and loopback listeners were removed; transient server secrets/private startup logs were deleted. Source/archive and scoped operator fingerprints were preserved. The fresh authenticated native test profile remains private at `/opt/data/cache/scratch/oc-beads-22-genuine` for parent custody; its real credential database was not copied into evidence or deleted. No Zeus or operator service/configuration change occurred. -Recommended next steps, under their own authority: +Specification closeout is complete: the archive operation synchronized one added and six modified requirements, with no removals or renames. All five implementation/qualification tasks remain checked in the archived package. The archive-closeout successor to the accepted candidate changes specification/evidence documentation only; the qualified packed files and archive remain unchanged. + +Remaining release steps, under their own authority: -- Synchronize the accepted active deltas into canonical specs, rehearse/validate archive, then publish the reviewed source and specification closeout. - Prepare **stable 2.0.0**, not a silent 1.x promotion: new host admission is `>=2.2.6 <2.3`; there is no parallel 2.1 compatibility layer. Document upgrading OpenCode first and installing the new plugin deliberately. - Preserve existing stable/beta exact pins for users staying on older hosts. Until a trusted release is authorized and verified, npm `latest=1.0.2` and `next=1.1.0-beta.5` remain the previously published releases, not this local candidate. - A future version/migration-note change creates a new release archive identity and needs ordinary release checks/provenance; it does not erase this exact-candidate qualification or silently authorize another live campaign. -Parent acceptance is not package publication, operator activation, canonical archival or resolution of the known deficiency. +Parent acceptance and source publication are not package publication, operator activation or resolution of the known deficiency. Canonical synchronization and archival follow the owner's separate closeout authorization, not an implied release approval. diff --git a/openspec/changes/adapt-opencode-2-2/.openspec.yaml b/openspec/changes/archive/2026-11-09-adapt-opencode-2-2/.openspec.yaml similarity index 100% rename from openspec/changes/adapt-opencode-2-2/.openspec.yaml rename to openspec/changes/archive/2026-11-09-adapt-opencode-2-2/.openspec.yaml diff --git a/openspec/changes/adapt-opencode-2-2/design.md b/openspec/changes/archive/2026-11-09-adapt-opencode-2-2/design.md similarity index 100% rename from openspec/changes/adapt-opencode-2-2/design.md rename to openspec/changes/archive/2026-11-09-adapt-opencode-2-2/design.md diff --git a/openspec/changes/adapt-opencode-2-2/proposal.md b/openspec/changes/archive/2026-11-09-adapt-opencode-2-2/proposal.md similarity index 100% rename from openspec/changes/adapt-opencode-2-2/proposal.md rename to openspec/changes/archive/2026-11-09-adapt-opencode-2-2/proposal.md diff --git a/openspec/changes/adapt-opencode-2-2/specs/work-continuity/spec.md b/openspec/changes/archive/2026-11-09-adapt-opencode-2-2/specs/work-continuity/spec.md similarity index 100% rename from openspec/changes/adapt-opencode-2-2/specs/work-continuity/spec.md rename to openspec/changes/archive/2026-11-09-adapt-opencode-2-2/specs/work-continuity/spec.md diff --git a/openspec/changes/adapt-opencode-2-2/specs/workflow-access/spec.md b/openspec/changes/archive/2026-11-09-adapt-opencode-2-2/specs/workflow-access/spec.md similarity index 100% rename from openspec/changes/adapt-opencode-2-2/specs/workflow-access/spec.md rename to openspec/changes/archive/2026-11-09-adapt-opencode-2-2/specs/workflow-access/spec.md diff --git a/openspec/changes/adapt-opencode-2-2/tasks.md b/openspec/changes/archive/2026-11-09-adapt-opencode-2-2/tasks.md similarity index 93% rename from openspec/changes/adapt-opencode-2-2/tasks.md rename to openspec/changes/archive/2026-11-09-adapt-opencode-2-2/tasks.md index 383feae..bc9f694 100644 --- a/openspec/changes/adapt-opencode-2-2/tasks.md +++ b/openspec/changes/archive/2026-11-09-adapt-opencode-2-2/tasks.md @@ -1,5 +1,7 @@ # Tasks +**Archived completion:** All five mapped tasks are accepted, including the final Claude Opus 5.5 READY gate. Source, qualification and retained limitations are summarized in [the acceptance record](../../../../docs/opencode-22-qualification-5rj2.md). The mapping below records implementation custody; it is not a new execution or release authorization. + Planning accepted; execution mapping is epic `opencode-beads-5rj`, with source/checks child `opencode-beads-5rj.1` owning 1.1, 1.2 and 2.1, and qualification/acceptance child `opencode-beads-5rj.2` owning 2.2 and 2.3, blocked by the source child. Shared edit limits and stop conditions are in [design.md](design.md). Mapping is not execution authority; discuss additional corner-case mitigation before implementation and do not begin from artifact-complete status alone. ## 1. Adapt the two host seams diff --git a/openspec/specs/work-continuity/spec.md b/openspec/specs/work-continuity/spec.md index 74a9a12..73bd1dc 100644 --- a/openspec/specs/work-continuity/spec.md +++ b/openspec/specs/work-continuity/spec.md @@ -91,9 +91,10 @@ The selected integration SHALL distinguish awaited interception from observation #### Scenario: Host retains and replays compacted history -- **WHEN** 2.1.1 summary/native history is recovered or forked, or its provider/model changes -- **THEN** the integration preserves host-owned native replay and metadata.keepID/legacy metadata.recent semantics without substituting a provider replacement window for a summary +- **WHEN** summary/native history is recovered or forked on the selected 2.2 host, or its provider/model changes +- **THEN** the integration preserves host-owned native replay and summary retention without substituting a provider replacement window for a summary - **AND** tool execution state and UI metadata are not treated as renamed provider replay payloads +- **AND** workflow snapshots present in native latest-compaction context follow workflow-access expansion; earlier ordinary user messages restored after a non-replayable native-window model switch receive explicit unavailable-guidance disclosure when their snapshots are absent from that context ### Requirement: Honest persistence acknowledgement diff --git a/openspec/specs/workflow-access/spec.md b/openspec/specs/workflow-access/spec.md index c467314..6c01a86 100644 --- a/openspec/specs/workflow-access/spec.md +++ b/openspec/specs/workflow-access/spec.md @@ -138,7 +138,7 @@ Selected workflows SHALL preserve the user's requested execution scope, distingu ### Requirement: Explicit native availability and fallback -The redesign SHALL use N1 with exact OpenCode 2.1.1 qualification and `>=2.1.1 <2.2` admission through opt-in beta/next delivery. It SHALL NOT silently migrate stable consumers, promise arbitrary minor/major compatibility or substitute N0. Concrete incompatibilities SHALL block the affected path with diagnostics. Publication remains separately authorized. +The integration SHALL target exact OpenCode 2.2.6 qualification and `>=2.2.6 <2.3` admission. It SHALL distinguish admission from measured compatibility, preserve existing version pins and refuse unsupported host registration with diagnostics. It SHALL NOT retain a 2.1 compatibility layer or silently migrate installations. Publication remains separately authorized. #### Scenario: A different host version is considered @@ -169,7 +169,7 @@ Typed tools and direct shell fallback SHALL respect shell visibility and operati #### Scenario: Agent wholly denies shell - **WHEN** a restricted agent's effective rules wholly deny shell -- **THEN** typed tools are absent from its code-mode inventory, not merely denied after selection; default typed tools have no direct registration and native shell is never callable inside execute +- **THEN** typed tools are absent from both its native code-mode inventory and direct-tool fallback, not merely denied after selection; native shell is never callable inside execute #### Scenario: Invoking agent differs from session default @@ -187,7 +187,7 @@ Typed tools and direct shell fallback SHALL respect shell visibility and operati - **WHEN** the host permission assertion returns user correction with feedback or explicit decline - **THEN** the adapter propagates the assertion rejection unchanged and preserves correction feedback without executing the rejected target operation - **AND** it does not acknowledge target success or automatically retry the operation -- **AND** code-mode composition remains available despite the owner-accepted OpenCode 2.1.1 limitation that a plain decline may become an inner error in an outer completed result and permit subsequent model steps or independently permissioned work +- **AND** the host owns outer code-mode decline/continuation behavior; qualification records actual target-host behavior without downstream repair or relabeling historical observations - **AND** rejection of one operation does not promise whole-step termination; every later operation retains its own permission checks, and explicit Stop/Cancel remains a separate cancellation guarantee #### Scenario: External target or held workspace @@ -244,7 +244,7 @@ The package SHALL expose effective discovery, compatibility, ownership conflicts #### Scenario: Compact diagnostic history - **WHEN** diagnostics is invoked with optional free text and native attachments/delivery -- **THEN** history shows the compact command and exact free text, while the dynamic report is captured once in durable metadata and expanded only through existing primary/compaction hooks +- **THEN** history shows the compact command and exact free text, while the dynamic report is captured once in a durable invocation snapshot and expanded only through primary/compaction hooks - **AND** replay preserves the captured observation without regenerating diagnostics, duplicating instructions, mutating persisted input or expanding auxiliary requests #### Scenario: Diagnostics is overridden or unloaded @@ -259,8 +259,8 @@ The integration SHALL render actual executor identity and request capabilities, #### Scenario: Execute is denied - **WHEN** execute is wholly denied for the actual request -- **THEN** code-mode-only Beads tools are unavailable and guidance offers direct shell only if separately available and not held -- **AND** when both capabilities are unavailable the limitation is reported without bypass or invented tool access +- **THEN** composition is reported unavailable, while actually exposed direct Beads tools remain discoverable under their own permissions; direct shell is offered only if separately available and not held +- **AND** when composed tools, direct typed tools and shell are unavailable the limitation is reported without bypass or invented tool access #### Scenario: Typed and shell writes share an executor @@ -309,14 +309,14 @@ The integration SHALL present all 29 WORKFLOWS callbacks as compact native user - **WHEN** any WORKFLOWS command is invoked with bare text or literal details, files, agents, skills and queue or steer delivery - **THEN** exactly one ordinary native prompt preserves those inputs and native prompt-hook edits -- **AND** visible text contains `/oc-beads:` and any exact untruncated literal details, while durable versioned ocBeadsWorkflow v1 message metadata retains the static instructions and installed resource paths +- **AND** visible text contains `/oc-beads:` and any exact untruncated literal details, while a durable versioned ocBeadsWorkflow v1 invocation snapshot retains the static instructions and installed resource paths - **AND** single-line free text follows the command on the same line; multiline free text follows it on a new line, preserving its whitespace without generated headings or typed-argument parsing - **AND** no synthetic pairing, retry, auxiliary model call or pending-command queue is introduced #### Scenario: Request expansion and replay -- **WHEN** a retained tagged user message reaches primary or compaction context, including after native-window replay or reload -- **THEN** its request copy receives exactly one instruction snapshot, preserving content, foreign metadata, IDs and role without changing stored history +- **WHEN** a retained tagged user message whose durable snapshot is present in the native latest-compaction context reaches primary or compaction context, including retained messages after replay or reload +- **THEN** its durable invocation snapshot is resolved through supported native session history and its request copy receives exactly one expansion, preserving content, foreign data, IDs and role without changing stored history or relying on model-message metadata - **AND** outbound provider input contains no routing marker or serialized duplicate snapshot, with stable expansion across identical requests and current-turn instructions present - **AND** missing, unknown or non-user metadata and unmarked historical messages remain untouched @@ -343,7 +343,7 @@ The integration SHALL present all 29 WORKFLOWS callbacks as compact native user - **WHEN** retained user history contains a valid version-1 ocBeadsSync snapshot, a valid version-1 ocBeadsWorkflow snapshot, or both - **THEN** the generalized snapshot wins when both are valid, otherwise the valid snapshot is expanded -- **AND** both recognized valid routing keys are stripped from the transient copy so repeated expansion is stable, while malformed, unsupported, foreign and non-user metadata remains untouched +- **AND** recognized routing keys and serialized duplicate snapshots are excluded from provider input, expansion is stable across repeated requests, and malformed, unsupported, foreign and non-user metadata remains untouched in durable history - **AND** new invocations write only ocBeadsWorkflow, without reconstructing old instructions from current command definitions or rewriting stored history #### Scenario: Existing workflow authority and resource contracts @@ -351,3 +351,26 @@ The integration SHALL present all 29 WORKFLOWS callbacks as compact native user - **WHEN** any migrated workflow is invoked with action, help, guide, missing-input or explicit mode details - **THEN** its existing purpose, common instructions, all installed skill/resource paths, required targets and acceptance ownership remain controlling with actual execution-derived actor/capabilities/native permission - **AND** restore remains display-only absent --apply, compact --auto and --dolt each need separate authority, QuickStart uses current installed guidance, stats means issue-database overview, and no implicit administration, cascade, initialization or repair is inferred + +#### Scenario: Snapshot retrieval is unavailable + +- **WHEN** a retained workflow invocation cannot resolve its durable snapshot through the supported native history path +- **THEN** the request exposes the missing-guidance condition rather than silently inventing or reconstructing its instructions +- **AND** the integration does not re-admit the prompt, replay the operation or reinterpret stored text as new authority + +#### Scenario: Model switch restores older transcript beyond the readable context + +- **WHEN** native compaction on model A is followed by switching to model B that cannot replay A's native window, and the host restores earlier ordinary user messages whose snapshots are absent from the native latest-compaction context +- **THEN** affected historical workflow instructions or captured diagnostic reports are explicitly reported unavailable rather than claimed restored +- **AND** accessible current snapshots still expand normally; host-supplied replay content and Beads issues, notes and checkpoints are not rewritten or deleted + +### Requirement: Documented snapshot-recovery deficiency + +The package SHALL document unavailable historical workflow-snapshot recovery as an unresolved plugin deficiency with an upstream API blocker. User-facing documentation SHALL explain its trigger, impact and limits and link the upstream request. Accepting the limitation for a release SHALL NOT be described as resolving the deficiency. + +#### Scenario: User evaluates the documented limitation + +- **WHEN** a user reads the README's applicable compatibility/limitations section +- **THEN** it describes native compaction followed by an incompatible model switch, missing historical workflow instructions or captured reports, and unaffected Beads tracker data +- **AND** it links OpenCode #57424 and identifies the unresolved local follow-up without requiring Beads access to understand the limitation +- **AND** unreleased adaptation behavior is distinguished from the published beta's narrower host support