fix(2137): skip worktree isolation when .gitmodules detected (#2144)

* feat(sdk): add typed query foundation and gsd-sdk query (Phase 1)

Add sdk/src/query registry and handlers with tests, GSDQueryError, CLI query wiring, and supporting type/tool-scoping hooks. Update CHANGELOG. Vitest 4 constructor mock fixes in milestone-runner tests.

Made-with: Cursor

* fix(2137): skip worktree isolation when .gitmodules detected

When a project contains git submodules, worktree isolation cannot
correctly handle submodule commits — three separate gaps exist in
worktree setup, executor commit protocol, and merge-back. Rather
than patch each gap individually, detect .gitmodules at phase start
and fall back to sequential execution, which handles submodules
transparently (Option B).

Affected workflows: execute-phase.md, quick.md

---------

Co-authored-by: David Sienkowski <dave@sienkowski.com>
This commit is contained in:
Tom Boucher
2026-04-12 08:33:04 -04:00
committed by GitHub
parent 58632e0718
commit 002bcf2a8a
63 changed files with 17872 additions and 23 deletions

121
sdk/src/query/registry.ts Normal file
View File

@@ -0,0 +1,121 @@
/**
* Query command registry — routes commands to native SDK handlers.
*
* The registry is a flat `Map<string, QueryHandler>` that maps command names
* to handler functions. Unknown commands throw GSDError — the gsd-tools.cjs
* fallback was removed in v3.0 when all commands were migrated to native handlers.
*
* Also exports `extractField` — a TypeScript port of the `--pick` field
* extraction logic from gsd-tools.cjs (lines 365-382).
*
* @example
* ```typescript
* import { QueryRegistry, extractField } from './registry.js';
*
* const registry = new QueryRegistry();
* registry.register('generate-slug', generateSlug);
* const result = await registry.dispatch('generate-slug', ['My Phase'], '/project');
* const slug = extractField(result.data, 'slug'); // 'my-phase'
* ```
*/
import type { QueryResult, QueryHandler } from './utils.js';
import { GSDError, ErrorClassification } from '../errors.js';
// ─── extractField ──────────────────────────────────────────────────────────
/**
* Extract a nested field from an object using dot-notation and bracket syntax.
*
* Direct port of `extractField()` from gsd-tools.cjs (lines 365-382).
* Supports `a.b.c` dot paths, `items[0]` array indexing, and `items[-1]`
* negative indexing.
*
* @param obj - The object to extract from
* @param fieldPath - Dot-separated path with optional bracket notation
* @returns The extracted value, or undefined if the path doesn't resolve
*/
export function extractField(obj: unknown, fieldPath: string): unknown {
const parts = fieldPath.split('.');
let current: unknown = obj;
for (const part of parts) {
if (current === null || current === undefined) return undefined;
const bracketMatch = part.match(/^(.+?)\[(-?\d+)]$/);
if (bracketMatch) {
const key = bracketMatch[1];
const index = parseInt(bracketMatch[2], 10);
current = (current as Record<string, unknown>)[key];
if (!Array.isArray(current)) return undefined;
current = index < 0 ? current[current.length + index] : current[index];
} else {
current = (current as Record<string, unknown>)[part];
}
}
return current;
}
// ─── QueryRegistry ─────────────────────────────────────────────────────────
/**
* Flat command registry that routes query commands to native handlers.
*
* Unknown commands throw `GSDError` from `dispatch()` — there is no fallback
* to gsd-tools.cjs (bridge removed in v3.0). All supported commands must be
* registered via `register()`.
*/
export class QueryRegistry {
private handlers = new Map<string, QueryHandler>();
/**
* Register a native handler for a command name.
*
* @param command - The command name (e.g., 'generate-slug', 'state.load')
* @param handler - The handler function to invoke
*/
register(command: string, handler: QueryHandler): void {
this.handlers.set(command, handler);
}
/**
* Check if a command has a registered native handler.
*
* @param command - The command name to check
* @returns True if the command has a native handler
*/
has(command: string): boolean {
return this.handlers.has(command);
}
/**
* Get the handler for a command without dispatching.
*
* @param command - The command name to look up
* @returns The handler function, or undefined if not registered
*/
getHandler(command: string): QueryHandler | undefined {
return this.handlers.get(command);
}
/**
* Dispatch a command to its registered native handler.
*
* Throws GSDError for unknown commands — the gsd-tools.cjs fallback was
* removed in v3.0. All commands must be registered as native handlers (T-14-13).
*
* @param command - The command name to dispatch
* @param args - Arguments to pass to the handler
* @param projectDir - The project directory for context
* @returns The query result from the handler
* @throws GSDError if no handler is registered for the command
*/
async dispatch(command: string, args: string[], projectDir: string): Promise<QueryResult> {
const handler = this.handlers.get(command);
if (!handler) {
throw new GSDError(
`Unknown command: "${command}". No native handler registered.`,
ErrorClassification.Validation,
);
}
return handler(args, projectDir);
}
}