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:
@@ -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.
|
||||
|
||||
84
tests/bug-1818-unknown-flags.test.cjs
Normal file
84
tests/bug-1818-unknown-flags.test.cjs
Normal 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/);
|
||||
});
|
||||
});
|
||||
@@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user