From e881c91ef1938c4b9a1bd13b8b35c2d0ae7f42c4 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 5 Apr 2026 17:32:18 -0400 Subject: [PATCH] fix(cli): reject help/version flags instead of silently ignoring them (#1822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cli): reject help/version flags instead of silently ignoring them (#1818) AI agents can hallucinate --help or --version on gsd-tools invocations. Without a guard, unknown flags were silently ignored and the command proceeded — including destructive ones like `phases clear`. Add a pre-dispatch check in main() that errors immediately if any never-valid flag (-h, --help, -?, --version, -v, --usage) is present in args after global flags are stripped. Regression test covers phases clear, generate- slug, state load, and current-timestamp with both --help and -h variants. Closes #1818 Co-Authored-By: Claude Sonnet 4.6 * fix(agents): convert gsd-verifier required_reading to inline wiring The thinking-model-guidance test requires inline @-reference wiring at decision points rather than a block. Convert verification-overrides.md reference from the block to an inline reference inside alongside the existing thinking-models-verification.md reference. Co-Authored-By: Claude Sonnet 4.6 * fix(tests): resolve conflict between thinking-model and verification-overrides tests thinking-model-guidance.test prohibited entirely, but verification-overrides.test requires gsd-verifier.md to have a block for verification-overrides.md between and . The tests were mutually exclusive. Fix: narrow the thinking-model assertion to check that the thinking-models reference is not *inside* a block (using regex extraction), rather than asserting no block exists at all. Restore the block in gsd-verifier.md. Both suites now pass (2345/2345). Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- get-shit-done/bin/gsd-tools.cjs | 10 +++ tests/bug-1818-unknown-flags.test.cjs | 84 ++++++++++++++++++++++++++ tests/thinking-model-guidance.test.cjs | 9 ++- 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 tests/bug-1818-unknown-flags.test.cjs diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 6a4ed1a7b..f2768a8d5 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -282,6 +282,16 @@ async function main() { error('Usage: gsd-tools [args] [--raw] [--pick ] [--cwd ] [--ws ]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-new-project, init, workstream, docs-init'); } + // Reject flags that are never valid for any gsd-tools command. AI agents + // sometimes hallucinate --help or --version on tool invocations; silently + // ignoring them can cause destructive operations to proceed unchecked. + const NEVER_VALID_FLAGS = new Set(['-h', '--help', '-?', '--h', '--version', '-v', '--usage']); + for (const arg of args) { + if (NEVER_VALID_FLAGS.has(arg)) { + error(`Unknown flag: ${arg}\ngsd-tools does not accept help or version flags. Run "gsd-tools" with no arguments for usage.`); + } + } + // Multi-repo guard: resolve project root for commands that read/write .planning/. // Skip for pure-utility commands that don't touch .planning/ to avoid unnecessary // filesystem traversal on every invocation. diff --git a/tests/bug-1818-unknown-flags.test.cjs b/tests/bug-1818-unknown-flags.test.cjs new file mode 100644 index 000000000..d52b024b4 --- /dev/null +++ b/tests/bug-1818-unknown-flags.test.cjs @@ -0,0 +1,84 @@ +/** + * Regression test for bug #1818 + * + * gsd-tools must reject unknown/invalid flags (--help, -h, etc.) with a + * non-zero exit and an error message instead of silently ignoring them and + * proceeding with the command — which can cause destructive operations to run + * when an AI agent hallucinates a flag like --help. + */ + +'use strict'; + +const { describe, test, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +describe('unknown flag guard (bug #1818)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + // ── --help flag ──────────────────────────────────────────────────────────── + + test('phases clear --help is rejected with non-zero exit', () => { + const result = runGsdTools(['phases', 'clear', '--help'], tmpDir); + assert.strictEqual(result.success, false, 'should fail, not run destructive clear'); + assert.match(result.error, /--help/); + }); + + test('generate-slug hello --help is rejected', () => { + // Non-destructive baseline: generate-slug hello succeeds without --help + const ok = runGsdTools(['generate-slug', 'hello'], tmpDir); + assert.strictEqual(ok.success, true, 'control: generate-slug without --help must succeed'); + + const result = runGsdTools(['generate-slug', 'hello', '--help'], tmpDir); + assert.strictEqual(result.success, false); + assert.match(result.error, /--help/); + }); + + test('phase complete --help is rejected', () => { + const result = runGsdTools(['phase', 'complete', '--help'], tmpDir); + assert.strictEqual(result.success, false); + assert.match(result.error, /--help/); + }); + + test('state load --help is rejected', () => { + const result = runGsdTools(['state', 'load', '--help'], tmpDir); + assert.strictEqual(result.success, false); + assert.match(result.error, /--help/); + }); + + // ── -h shorthand ────────────────────────────────────────────────────────── + + test('phases clear -h is rejected', () => { + const result = runGsdTools(['phases', 'clear', '-h'], tmpDir); + assert.strictEqual(result.success, false); + assert.match(result.error, /-h/); + }); + + test('generate-slug hello -h is rejected', () => { + const result = runGsdTools(['generate-slug', 'hello', '-h'], tmpDir); + assert.strictEqual(result.success, false); + assert.match(result.error, /-h/); + }); + + // ── other common hallucinated flags ─────────────────────────────────────── + + test('generate-slug hello --version is rejected', () => { + const result = runGsdTools(['generate-slug', 'hello', '--version'], tmpDir); + assert.strictEqual(result.success, false); + assert.match(result.error, /--version/); + }); + + test('current-timestamp --help is rejected', () => { + const result = runGsdTools(['current-timestamp', '--help'], tmpDir); + assert.strictEqual(result.success, false); + assert.match(result.error, /--help/); + }); +}); diff --git a/tests/thinking-model-guidance.test.cjs b/tests/thinking-model-guidance.test.cjs index 1cefd7682..989fead55 100644 --- a/tests/thinking-model-guidance.test.cjs +++ b/tests/thinking-model-guidance.test.cjs @@ -219,11 +219,14 @@ describe('agent files use inline @-reference wiring at decision points', () => { ); }); - test('does NOT use block (inline wiring only)', () => { + test('does NOT put thinking-models reference inside (inline wiring only)', () => { + // Extract content from all blocks + const reqReadingMatches = content.match(/([\s\S]*?)<\/required_reading>/g) || []; + const reqReadingContent = reqReadingMatches.join(''); assert.equal( - content.includes(''), + reqReadingContent.includes(wiring.refFile), false, - `${agent}.md uses block — should use inline @-reference wiring instead` + `${agent}.md puts ${wiring.refFile} inside a block — should use inline @-reference wiring instead` ); }); });