From 4bc36535789af378610a474140b10e01fc34996d Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 24 May 2026 14:37:10 -0400 Subject: [PATCH] fix(#167): support query meta-command in gsd-tools (#202) * fix(#167): support query meta-command in gsd-tools * chore(#167): add changeset for query meta-command fix * fix(#167): pin claude runtime in local-agent regression tests * test(#3751): stabilize local-agent CI assertions --- .changeset/167-query-meta-command.md | 5 ++ get-shit-done/bin/gsd-tools.cjs | 7 +++ tests/bug-167-query-meta-command.test.cjs | 20 +++++++ tests/bug-3751-init-local-agents.test.cjs | 68 +++++++++++++++++------ 4 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 .changeset/167-query-meta-command.md create mode 100644 tests/bug-167-query-meta-command.test.cjs diff --git a/.changeset/167-query-meta-command.md b/.changeset/167-query-meta-command.md new file mode 100644 index 000000000..fad45d0dd --- /dev/null +++ b/.changeset/167-query-meta-command.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 167 +--- +**`gsd-tools` query meta-command parity** — direct invocations like `node gsd-tools.cjs query init.progress` now behave the same as `node gsd-tools.cjs init.progress` instead of failing with `Unknown command: query`. This unblocks workflow preflight paths that call the CJS entrypoint directly with the `query` prefix. diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 85940a2bb..57e6d31ea 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -419,6 +419,13 @@ async function main() { let command = args[0]; + // Accept `query` meta-prefix parity with `gsd-sdk query ...`. + // Workflows may call `node gsd-tools.cjs query ` directly. + if (command === 'query') { + args.shift(); + command = args[0]; + } + // #3243: accept dotted canonical form (e.g. `state.update`) as well as the // spaced form (`state update`). Workflow files and stale SDK binaries pass // the dotted canonical form directly; any caller that bypasses the SDK diff --git a/tests/bug-167-query-meta-command.test.cjs b/tests/bug-167-query-meta-command.test.cjs new file mode 100644 index 000000000..62a9c553b --- /dev/null +++ b/tests/bug-167-query-meta-command.test.cjs @@ -0,0 +1,20 @@ +'use strict'; + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); + +const { runGsdTools } = require('./helpers.cjs'); + +test('bug #167: query meta-command prefixes direct gsd-tools calls', () => { + const direct = runGsdTools(['init.progress']); + assert.equal(direct.success, true, `init.progress failed: ${direct.error || direct.output}`); + + const meta = runGsdTools(['query', 'init.progress']); + assert.equal(meta.success, true, `query init.progress failed: ${meta.error || meta.output}`); + + assert.deepEqual( + JSON.parse(meta.output), + JSON.parse(direct.output), + 'query-prefixed and direct invocations should return identical init.progress payloads' + ); +}); diff --git a/tests/bug-3751-init-local-agents.test.cjs b/tests/bug-3751-init-local-agents.test.cjs index cab31a0f4..9cad9a557 100644 --- a/tests/bug-3751-init-local-agents.test.cjs +++ b/tests/bug-3751-init-local-agents.test.cjs @@ -30,6 +30,30 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); +const MODEL_PROFILES = require('../get-shit-done/bin/lib/model-profiles.cjs').MODEL_PROFILES; +const EXPECTED_AGENTS = Object.keys(MODEL_PROFILES); + +function writeRequiredAgents(agentsDir) { + for (const agentName of EXPECTED_AGENTS) { + fs.writeFileSync( + path.join(agentsDir, `${agentName}.md`), + `---\nname: ${agentName}\ndescription: test\ntools: Read\n---\nAgent content.\n`, + ); + } +} + +function normalizePathForAssert(targetPath) { + if (typeof targetPath !== 'string') return targetPath; + const resolved = path.resolve(targetPath); + try { + if (typeof fs.realpathSync.native === 'function') { + return fs.realpathSync.native(resolved); + } + return fs.realpathSync(resolved); + } catch { + return resolved; + } +} // ─── Source-file structural assertions (no build required) ─────────────────── @@ -152,11 +176,14 @@ describe('#3751: resolveAgentsDir() repo-local fallback — runtime behaviour', tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3751-')); savedEnv = { GSD_AGENTS_DIR: process.env.GSD_AGENTS_DIR, + GSD_RUNTIME: process.env.GSD_RUNTIME, CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR, HOME: process.env.HOME, }; // Clear explicit overrides so we exercise the fallback path delete process.env.GSD_AGENTS_DIR; + // Make runtime deterministic: this suite validates Claude local-agent semantics. + process.env.GSD_RUNTIME = 'claude'; }); afterEach(() => { @@ -167,6 +194,11 @@ describe('#3751: resolveAgentsDir() repo-local fallback — runtime behaviour', } else { delete process.env.GSD_AGENTS_DIR; } + if (savedEnv.GSD_RUNTIME !== undefined) { + process.env.GSD_RUNTIME = savedEnv.GSD_RUNTIME; + } else { + delete process.env.GSD_RUNTIME; + } if (savedEnv.CLAUDE_CONFIG_DIR !== undefined) { process.env.CLAUDE_CONFIG_DIR = savedEnv.CLAUDE_CONFIG_DIR; } else { @@ -187,14 +219,12 @@ describe('#3751: resolveAgentsDir() repo-local fallback — runtime behaviour', // DO NOT create fakeGlobalConfig/agents/ — simulates absent global agents process.env.CLAUDE_CONFIG_DIR = fakeGlobalConfig; - // Set up repo-local .claude/agents with a GSD agent definition + // Set up repo-local .claude/agents with all required GSD agent files. + // `agents_installed` is only true when every MODEL_PROFILES key exists. const repoRoot = path.join(tmpDir, 'repo'); const repoLocalAgentsDir = path.join(repoRoot, '.claude', 'agents'); fs.mkdirSync(repoLocalAgentsDir, { recursive: true }); - fs.writeFileSync( - path.join(repoLocalAgentsDir, 'gsd-project-researcher.md'), - '---\nname: gsd-project-researcher\ndescription: test\ntools: Read\n---\nAgent content.\n', - ); + writeRequiredAgents(repoLocalAgentsDir); // Dynamically require helpers so CLAUDE_CONFIG_DIR is picked up // (Node caches modules, so we clear the cache first) @@ -314,15 +344,20 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3799-')); savedEnv = { GSD_AGENTS_DIR: process.env.GSD_AGENTS_DIR, + GSD_RUNTIME: process.env.GSD_RUNTIME, CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR, }; delete process.env.GSD_AGENTS_DIR; + // Keep runtime fixed across suite-order changes and leaked test env. + process.env.GSD_RUNTIME = 'claude'; }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); if (savedEnv.GSD_AGENTS_DIR !== undefined) process.env.GSD_AGENTS_DIR = savedEnv.GSD_AGENTS_DIR; else delete process.env.GSD_AGENTS_DIR; + if (savedEnv.GSD_RUNTIME !== undefined) process.env.GSD_RUNTIME = savedEnv.GSD_RUNTIME; + else delete process.env.GSD_RUNTIME; if (savedEnv.CLAUDE_CONFIG_DIR !== undefined) process.env.CLAUDE_CONFIG_DIR = savedEnv.CLAUDE_CONFIG_DIR; else delete process.env.CLAUDE_CONFIG_DIR; }); @@ -346,14 +381,11 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour // global agents/ exists but is EMPTY — simulates Claude auto-creating the dir process.env.CLAUDE_CONFIG_DIR = fakeGlobalConfig; - // Set up project-local .claude/agents with a GSD agent definition + // Set up project-local .claude/agents with all required GSD agent files const repoRoot = path.join(tmpDir, 'repo'); const repoLocalAgentsDir = path.join(repoRoot, '.claude', 'agents'); fs.mkdirSync(repoLocalAgentsDir, { recursive: true }); - fs.writeFileSync( - path.join(repoLocalAgentsDir, 'gsd-project-researcher.md'), - '---\nname: gsd-project-researcher\ndescription: test\ntools: Read\n---\nAgent content.\n', - ); + writeRequiredAgents(repoLocalAgentsDir); const result = runGsdTools( ['query', 'init.new-project', '--raw'], @@ -369,8 +401,8 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour if (parsed && typeof parsed.agents_dir !== 'undefined') { // agents_dir must point to the local dir, not the empty global dir assert.strictEqual( - parsed.agents_dir, - repoLocalAgentsDir, + normalizePathForAssert(parsed.agents_dir), + normalizePathForAssert(repoLocalAgentsDir), `agents_dir must resolve to project-local path, got: ${parsed.agents_dir} (#3799)`, ); } @@ -410,8 +442,8 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour try { parsed = JSON.parse(result.output); } catch { return; } if (parsed && typeof parsed.agents_dir !== 'undefined') { assert.strictEqual( - parsed.agents_dir, - fakeGlobalAgents, + normalizePathForAssert(parsed.agents_dir), + normalizePathForAssert(fakeGlobalAgents), `agents_dir must resolve to global path when no local dir exists, got: ${parsed.agents_dir} (#3799)`, ); } @@ -426,13 +458,13 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour const fakeGlobalConfig = path.join(tmpDir, 'fake-global-both'); const fakeGlobalAgents = path.join(fakeGlobalConfig, 'agents'); fs.mkdirSync(fakeGlobalAgents, { recursive: true }); - fs.writeFileSync(path.join(fakeGlobalAgents, 'gsd-project-researcher.md'), '# global agent\n'); + writeRequiredAgents(fakeGlobalAgents); process.env.CLAUDE_CONFIG_DIR = fakeGlobalConfig; const repoRoot = path.join(tmpDir, 'repo-both'); const repoLocalAgentsDir = path.join(repoRoot, '.claude', 'agents'); fs.mkdirSync(repoLocalAgentsDir, { recursive: true }); - fs.writeFileSync(path.join(repoLocalAgentsDir, 'gsd-project-researcher.md'), '# local agent\n'); + writeRequiredAgents(repoLocalAgentsDir); const result = runGsdTools( ['query', 'init.new-project', '--raw'], @@ -447,8 +479,8 @@ describe('#3799: resolveAgentsDir() local-first resolution — runtime behaviour try { parsed = JSON.parse(result.output); } catch { return; } if (parsed && typeof parsed.agents_dir !== 'undefined') { assert.strictEqual( - parsed.agents_dir, - repoLocalAgentsDir, + normalizePathForAssert(parsed.agents_dir), + normalizePathForAssert(repoLocalAgentsDir), `agents_dir must resolve to local path when both exist, got: ${parsed.agents_dir} (#3799)`, ); }