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` ); }); });