createGSDToolsRuntime accepted opts.workstream and forwarded it to the
QuerySubprocessAdapter (line 38) but the QueryNativeDirectAdapter's
dispatch closure dropped it:
dispatch: (registryCommand, registryArgs) =>
registry.dispatch(registryCommand, registryArgs, opts.projectDir)
`registry.dispatch(command, args, projectDir, workstream?)` accepts a
4th workstream argument and forwards it to handlers. When a GSDTools
instance was created with a workstream, the native fast-path silently
routed planning-path queries to the root `.planning/` tree instead of
`.planning/workstreams/<name>/`. Subprocess dispatch correctly carried
the workstream; native dispatch did not — runtime-bridge mode parity
broke for any workstream-aware GSDTools consumer using the native path.
One-line fix: pass opts.workstream as the 4th arg to registry.dispatch.
Regression test exercises three paths:
1. Constructor-seam unit test: spy on QueryNativeDirectAdapter,
capture the dispatch closure, verify it reaches a registry that
reports the unknown-command error message.
2. Back-compat: same with workstream omitted — closure still reaches
the registry.
3. End-to-end: spy on createRegistry to inject a probe registry with a
registered handler that records its args. Invoke through
runtime.bridge.dispatchHotpath(). Assert the handler observed
workstream='frontend-ws' as its 3rd arg.
RED verified: end-to-end probe fails on pre-fix tree with
`expected undefined to be 'frontend-ws'`. GREEN after the fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
import type { GSDEventStream } from './event-stream.js';
|
|
import { createRegistry } from './query/index.js';
|
|
import { GSDTransport } from './gsd-transport.js';
|
|
import { QueryExecutionPolicy } from './query-execution-policy.js';
|
|
import { QuerySubprocessAdapter } from './query-subprocess-adapter.js';
|
|
import { QueryNativeDirectAdapter } from './query-native-direct-adapter.js';
|
|
import { QueryNativeHotpathAdapter } from './query-native-hotpath-adapter.js';
|
|
import { formatQueryRawOutput } from './query-raw-output-projection.js';
|
|
import { createQueryNativeErrorFactory, createQueryToolsErrorFactory } from './query-tools-error-factory.js';
|
|
import { QueryRuntimeBridge, type RuntimeBridgeOptions } from './query-runtime-bridge.js';
|
|
|
|
export interface GSDToolsRuntime {
|
|
bridge: QueryRuntimeBridge;
|
|
}
|
|
|
|
export function createGSDToolsRuntime(opts: {
|
|
projectDir: string;
|
|
gsdToolsPath: string;
|
|
timeoutMs: number;
|
|
workstream?: string;
|
|
eventStream?: GSDEventStream;
|
|
sessionId?: string;
|
|
shouldUseNativeQuery: () => boolean;
|
|
execJsonFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<unknown>;
|
|
execRawFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<string>;
|
|
strictSdk?: boolean;
|
|
allowFallbackToSubprocess?: boolean;
|
|
onDispatchEvent?: RuntimeBridgeOptions['onDispatchEvent'];
|
|
}): GSDToolsRuntime {
|
|
const registry = createRegistry(opts.eventStream, opts.sessionId);
|
|
|
|
const queryToolsErrorFactory = createQueryToolsErrorFactory();
|
|
|
|
const subprocessAdapter = new QuerySubprocessAdapter({
|
|
projectDir: opts.projectDir,
|
|
gsdToolsPath: opts.gsdToolsPath,
|
|
timeoutMs: opts.timeoutMs,
|
|
workstream: opts.workstream,
|
|
...queryToolsErrorFactory,
|
|
});
|
|
|
|
const nativeErrorFactory = createQueryNativeErrorFactory(opts.timeoutMs);
|
|
|
|
const nativeDirectAdapter = new QueryNativeDirectAdapter({
|
|
timeoutMs: opts.timeoutMs,
|
|
// #3591: forward opts.workstream to the registry so native dispatch
|
|
// routes planning-path queries to .planning/workstreams/<name>/
|
|
// instead of the root .planning tree. createGSDToolsRuntime accepts
|
|
// workstream and the QuerySubprocessAdapter already forwards it
|
|
// (line 38); the native dispatch closure was the only seam that
|
|
// dropped it, silently routing GSDTools-native queries to root.
|
|
dispatch: (registryCommand, registryArgs) => registry.dispatch(registryCommand, registryArgs, opts.projectDir, opts.workstream),
|
|
...nativeErrorFactory,
|
|
});
|
|
|
|
const transport = new GSDTransport(registry, {
|
|
dispatchNative: (request) => nativeDirectAdapter.dispatchResult(
|
|
request.legacyCommand,
|
|
request.legacyArgs,
|
|
request.registryCommand,
|
|
request.registryArgs,
|
|
),
|
|
execSubprocessJson: (legacyCommand, legacyArgs) => subprocessAdapter.execJson(legacyCommand, legacyArgs),
|
|
execSubprocessRaw: (legacyCommand, legacyArgs) => subprocessAdapter.execRaw(legacyCommand, legacyArgs),
|
|
formatNativeRaw: (registryCommand, data) => formatQueryRawOutput(registryCommand, data),
|
|
});
|
|
|
|
const executionPolicy = new QueryExecutionPolicy(transport);
|
|
const nativeHotpathAdapter = new QueryNativeHotpathAdapter(
|
|
opts.shouldUseNativeQuery,
|
|
nativeDirectAdapter,
|
|
opts.execJsonFallback,
|
|
opts.execRawFallback,
|
|
);
|
|
|
|
const bridge = new QueryRuntimeBridge(
|
|
registry,
|
|
executionPolicy,
|
|
nativeHotpathAdapter,
|
|
opts.shouldUseNativeQuery,
|
|
{
|
|
strictSdk: opts.strictSdk,
|
|
allowFallbackToSubprocess: opts.allowFallbackToSubprocess,
|
|
onDispatchEvent: opts.onDispatchEvent,
|
|
},
|
|
);
|
|
|
|
return { bridge };
|
|
}
|