* fix(#2598): declare OpenCode subagent dispatch synchronous, not background capabilities/opencode/capability.json advertised dispatch.background: true and dispatch.backgroundDispatch: true. negotiateHostCapabilities and every degradationFor / shouldFlattenDispatch consumer trusts these per-field values, so declaring a capability the host lacks OVERSTATES it — the opposite of the fail-closed posture the negotiation is built for. The issue's own citations needed checking before acting: the host-integration matrix (ADR-1239's designated deployment source-of-truth) documented `true` with NEWER evidence than the issue cited, and explicitly marked the issue's sst/opencode#5887 reference as a stale snapshot superseded by #2087. git log confirms #2087 deliberately flipped these from false to true, citing OpenCode v1.15.0/v1.17 as "background subagents enabled by default in all modes". Applying the issue as filed would, on that evidence, have REGRESSED a deliberate update. So the claim was verified against current upstream rather than either document. `packages/opencode/src/effect/runtime-flags.ts` on `dev` today reads: experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS") `enabledByExperimental` falls back to the `experimental` flag and `bool()` defaults to false — the parameter is hidden from the model unless an operator opts in by env var. Upstream #29638 is still OPEN and confirms the session loop `tasks.pop()`s one subtask at a time. #2087's "default-on in all modes" reading does not hold against current dev. The issue's CONCLUSION is therefore right even though part of its evidence was superseded: concurrent dispatch cannot be relied on, so both fields are false. The matrix rows are corrected with the verified citation rather than reverted to the old #5887 quote, so the record shows why the value is false TODAY rather than re-asserting evidence that was legitimately superseded. Neighbouring sub-fields are untouched and pinned by test: namedDispatch, subagentToolkit, and isolation:'orchestrator-worktree' (which works via `opencode run --dir` at the OS process level and is unaffected — #2584 does not depend on this value either way). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ * fix(#2598): re-pin the dispatch contract tests to synchronous OpenCode dispatch gsd-test on the descriptor change came back FAILED (5 unique, both node versions). The failures were not incidental — they were deliberate contract-pin tests encoding #2087's decision, one named literally "background UPGRADE": tests/host-integration-descriptors.test.cjs - EXPECTED_FLATTEN[opencode] === false (background-eligible) - the derived background-eligible set pin tests/opencode-imperative-reference.test.cjs - "descriptor declares background dispatch true/true (v1.15/v1.17 upgrade)" - "background UPGRADE changes shouldFlattenDispatch: false now" So this is a recorded decision being reversed, not drift being corrected, and it is reversed on evidence: current upstream `dev` gates the capability behind OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS (default false) and upstream #29638 (OPEN) confirms the session loop still handles one subtask at a time. The issue is filed by the maintainer and explicitly directs "update golden-parity / validator fixtures as needed", which sanctions re-pinning. Behavioral consequence, verified: shouldFlattenDispatch(opencode) now returns TRUE, so GSD serializes opencode dispatch instead of trusting concurrency it cannot get. That is the correct fail-closed direction and is safe today — no shipped GSD flow drives OpenCode background waves (per the issue), and isolation:'orchestrator-worktree' is unaffected because it works at the OS process level via `opencode run --dir`, not via the native subagent. Each re-pinned test now asserts the retracted contract in the opposite direction — feeding the #2087 axes back in must still yield "would not flatten" — so a silent re-flip of either field is caught rather than merely un-asserted. lint:ci exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ * chore(#2598): backfill changeset pr number (#2682) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
92 lines
4.1 KiB
JavaScript
92 lines
4.1 KiB
JavaScript
/**
|
|
* #2598 — the OpenCode descriptor declared background/concurrent subagent
|
|
* dispatch that OpenCode does not actually provide by default.
|
|
*
|
|
* `capabilities/opencode/capability.json` carried
|
|
* `runtime.hostIntegration.dispatch.background: true` and
|
|
* `dispatch.backgroundDispatch: true`. OpenCode's native subagent dispatch
|
|
* (Task tool / `@`-mention / `subtask`) is synchronous: the `background`
|
|
* parameter is hidden from the model behind the opt-in
|
|
* `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` flag, which defaults to false
|
|
* (`enabledByExperimental(...)` over a `bool()` that defaults false), and the
|
|
* session loop still `tasks.pop()`s one subtask at a time (upstream #14195,
|
|
* #29638 — the latter still open).
|
|
*
|
|
* `negotiateHostCapabilities` and every `degradationFor`/`shouldFlattenDispatch`
|
|
* consumer TRUSTS these per-field values, so declaring a capability the host
|
|
* lacks overstates it — the opposite of the fail-closed posture the negotiation
|
|
* exists to enforce.
|
|
*
|
|
* History note: these fields were flipped to `true` by #2087 citing a reading of
|
|
* OpenCode v1.17 as "background subagents enabled by default in all modes".
|
|
* That reading does not hold against current upstream `dev`, where the flag is
|
|
* opt-in. This test pins the corrected values so a future descriptor edit cannot
|
|
* silently re-assert an unsupported capability.
|
|
*/
|
|
|
|
// allow-test-rule: runtime-contract-is-the-product #2598 — the descriptor JSON and the
|
|
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
|
|
|
|
'use strict';
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
const DESCRIPTOR = path.join(ROOT, 'capabilities', 'opencode', 'capability.json');
|
|
const MATRIX = path.join(ROOT, 'docs', 'reference', 'host-integration-capability-matrix.md');
|
|
|
|
function opencodeDispatch() {
|
|
const parsed = JSON.parse(fs.readFileSync(DESCRIPTOR, 'utf8'));
|
|
return parsed.runtime.hostIntegration.dispatch;
|
|
}
|
|
|
|
describe('#2598: OpenCode does not declare background/concurrent subagent dispatch', () => {
|
|
test('descriptor declares background: false', () => {
|
|
assert.equal(
|
|
opencodeDispatch().background,
|
|
false,
|
|
'OpenCode subagent dispatch is synchronous unless an experimental opt-in flag is set',
|
|
);
|
|
});
|
|
|
|
test('descriptor declares backgroundDispatch: false', () => {
|
|
assert.equal(
|
|
opencodeDispatch().backgroundDispatch,
|
|
false,
|
|
'concurrent dispatch requires an opt-in flag, so it must not be declared as available',
|
|
);
|
|
});
|
|
|
|
test('the capabilities that ARE real are left intact', () => {
|
|
// Narrow the blast radius: this fix must not quietly downgrade neighbouring
|
|
// sub-fields that were never in question.
|
|
const d = opencodeDispatch();
|
|
assert.equal(d.namedDispatch, true, 'named subagent dispatch is genuinely supported');
|
|
assert.equal(d.subagentToolkit, 'full', 'the general subagent has full tool access');
|
|
assert.equal(d.isolation, 'orchestrator-worktree',
|
|
'isolation is orchestrator-managed via `opencode run --dir`, unaffected by #2598');
|
|
});
|
|
|
|
test('the host-integration matrix agrees with the descriptor', () => {
|
|
// ADR-1239 designates the matrix the deployment source-of-truth; a
|
|
// descriptor/matrix disagreement is how this defect survived in the first
|
|
// place (the matrix said true, the ADR binding table said false).
|
|
const matrix = fs.readFileSync(MATRIX, 'utf8');
|
|
const section = matrix.slice(matrix.indexOf('## opencode'));
|
|
const end = section.indexOf('\n## ');
|
|
const opencodeSection = end === -1 ? section : section.slice(0, end);
|
|
|
|
for (const field of ['dispatch.background', 'dispatch.backgroundDispatch']) {
|
|
const row = opencodeSection.split('\n').find((l) => l.startsWith(`| ${field} |`));
|
|
assert.ok(row, `matrix must document ${field} for opencode`);
|
|
const value = row.split('|')[2].trim();
|
|
assert.equal(value, 'false', `matrix ${field} must match the descriptor`);
|
|
}
|
|
});
|
|
});
|