fix(cli): reject help/version flags instead of silently ignoring them (#1822)

* 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 <noreply@anthropic.com>

* 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 <required_reading> block. Convert
verification-overrides.md reference from the <required_reading> block
to an inline reference inside <verification_process> alongside the
existing thinking-models-verification.md reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): resolve conflict between thinking-model and verification-overrides tests

thinking-model-guidance.test prohibited <required_reading> entirely, but
verification-overrides.test requires gsd-verifier.md to have a
<required_reading> block for verification-overrides.md between </role>
and <project_context>. The tests were mutually exclusive.

Fix: narrow the thinking-model assertion to check that the thinking-models
reference is not *inside* a <required_reading> block (using regex extraction),
rather than asserting no <required_reading> block exists at all. Restore the
<required_reading> block in gsd-verifier.md. Both suites now pass (2345/2345).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-05 17:32:18 -04:00
committed by GitHub
parent 3a277f8ba8
commit e881c91ef1
3 changed files with 100 additions and 3 deletions

View File

@@ -282,6 +282,16 @@ async function main() {
error('Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>]\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.

View File

@@ -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/);
});
});

View File

@@ -219,11 +219,14 @@ describe('agent files use inline @-reference wiring at decision points', () => {
);
});
test('does NOT use <required_reading> block (inline wiring only)', () => {
test('does NOT put thinking-models reference inside <required_reading> (inline wiring only)', () => {
// Extract content from all <required_reading> blocks
const reqReadingMatches = content.match(/<required_reading>([\s\S]*?)<\/required_reading>/g) || [];
const reqReadingContent = reqReadingMatches.join('');
assert.equal(
content.includes('<required_reading>'),
reqReadingContent.includes(wiring.refFile),
false,
`${agent}.md uses <required_reading> block — should use inline @-reference wiring instead`
`${agent}.md puts ${wiring.refFile} inside a <required_reading> block — should use inline @-reference wiring instead`
);
});
});