From 3a277f8ba8dd3fabaacdc40a3584845e592f3a87 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sun, 5 Apr 2026 14:05:06 -0700 Subject: [PATCH] feat(next): add hard stop safety gates and consecutive-call guard (#1784) Add three hard-stop checks to /gsd-next that prevent blind advancement: 1. Unresolved .continue-here.md checkpoint from a previous session 2. Error/failed state in STATE.md 3. Unresolved FAIL items in VERIFICATION.md Also add a consecutive-call budget guard that prompts after 6 consecutive /gsd-next calls, preventing runaway automation loops. All gates are bypassed with --force (prints a one-line warning). Gates run in order and exit on the first hit to give clear, actionable diagnostics. Closes #1732 Co-authored-by: Claude Opus 4.6 --- commands/gsd/next.md | 2 + get-shit-done/workflows/next.md | 56 ++++++++++++++ tests/next-safety-gates.test.cjs | 129 +++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 tests/next-safety-gates.test.cjs diff --git a/commands/gsd/next.md b/commands/gsd/next.md index e7d81c747..a3793485c 100644 --- a/commands/gsd/next.md +++ b/commands/gsd/next.md @@ -13,6 +13,8 @@ Detect the current project state and automatically invoke the next logical GSD w No arguments needed — reads STATE.md, ROADMAP.md, and phase directories to determine what comes next. Designed for rapid multi-project workflows where remembering which phase/step you're on is overhead. + +Supports `--force` flag to bypass safety gates (checkpoint, error state, verification failures). diff --git a/get-shit-done/workflows/next.md b/get-shit-done/workflows/next.md index c436820ac..7ede7211f 100644 --- a/get-shit-done/workflows/next.md +++ b/get-shit-done/workflows/next.md @@ -34,6 +34,62 @@ No GSD project detected. Run `/gsd-new-project` to get started. Exit. + +Run hard-stop checks before routing. Exit on first hit unless `--force` was passed. + +If `--force` flag was passed, skip all gates and the consecutive guard. +Print a one-line warning: `⚠ --force: skipping safety gates` +Then proceed directly to `determine_next_action`. + +**Gate 1: Unresolved checkpoint** +Check if `.planning/.continue-here.md` exists: +```bash +[ -f .planning/.continue-here.md ] +``` +If found: +``` +⛔ Hard stop: Unresolved checkpoint + +`.planning/.continue-here.md` exists — a previous session left +unfinished work that needs manual review before advancing. + +Read the file, resolve the issue, then delete it to continue. +Use `--force` to bypass this check. +``` +Exit (do not route). + +**Gate 2: Error state** +Check if STATE.md contains `status: error` or `status: failed`: +If found: +``` +⛔ Hard stop: Project in error state + +STATE.md shows status: {status}. Resolve the error before advancing. +Run `/gsd-health` to diagnose, or manually fix STATE.md. +Use `--force` to bypass this check. +``` +Exit. + +**Gate 3: Unchecked verification** +Check if the current phase has a VERIFICATION.md with any `FAIL` items that don't have overrides: +If found: +``` +⛔ Hard stop: Unchecked verification failures + +VERIFICATION.md for phase {N} has {count} unresolved FAIL items. +Address the failures or add overrides before advancing to the next phase. +Use `--force` to bypass this check. +``` +Exit. + +**Consecutive-call guard:** +After passing all gates, check a counter file `.planning/.next-call-count`: +- If file exists and count >= 6: prompt "You've called /gsd-next {N} times consecutively. Continue? [y/N]" +- If user says no, exit +- Increment the counter +- The counter file is deleted by any non-`/gsd-next` command (convention — other workflows don't need to implement this, the note here is sufficient) + + Apply routing rules based on state: diff --git a/tests/next-safety-gates.test.cjs b/tests/next-safety-gates.test.cjs new file mode 100644 index 000000000..afe7a22be --- /dev/null +++ b/tests/next-safety-gates.test.cjs @@ -0,0 +1,129 @@ +/** + * GSD Tools Tests - /gsd-next safety gates and consecutive-call guard + * + * Validates that the next workflow includes three hard-stop safety gates + * (checkpoint, error state, verification), a consecutive-call budget guard, + * and a --force bypass flag. + * + * Closes: #1732 + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +describe('/gsd-next safety gates (#1732)', () => { + const workflowPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'next.md'); + const commandPath = path.join(__dirname, '..', 'commands', 'gsd', 'next.md'); + + test('workflow contains safety_gates step', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok( + content.includes(''), + 'workflow should have a safety_gates step' + ); + }); + + test('safety_gates step appears between detect_state and determine_next_action', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + const detectIdx = content.indexOf('name="detect_state"'); + const gatesIdx = content.indexOf('name="safety_gates"'); + const routeIdx = content.indexOf('name="determine_next_action"'); + assert.ok(detectIdx > -1, 'detect_state step should exist'); + assert.ok(gatesIdx > -1, 'safety_gates step should exist'); + assert.ok(routeIdx > -1, 'determine_next_action step should exist'); + assert.ok( + detectIdx < gatesIdx && gatesIdx < routeIdx, + 'safety_gates must appear between detect_state and determine_next_action' + ); + }); + + test('Gate 1: unresolved checkpoint (.continue-here.md)', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok( + content.includes('.continue-here.md'), + 'Gate 1 should check for .planning/.continue-here.md' + ); + assert.ok( + content.includes('Unresolved checkpoint'), + 'Gate 1 should display "Unresolved checkpoint" message' + ); + }); + + test('Gate 2: error state in STATE.md', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok( + content.includes('status: error') || content.includes('status: failed'), + 'Gate 2 should check for error/failed status in STATE.md' + ); + assert.ok( + content.includes('Project in error state'), + 'Gate 2 should display "Project in error state" message' + ); + }); + + test('Gate 3: unchecked verification failures', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok( + content.includes('VERIFICATION.md'), + 'Gate 3 should check VERIFICATION.md' + ); + assert.ok( + content.includes('FAIL'), + 'Gate 3 should look for FAIL items' + ); + assert.ok( + content.includes('Unchecked verification failures'), + 'Gate 3 should display "Unchecked verification failures" message' + ); + }); + + test('consecutive-call budget guard', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok( + content.includes('.next-call-count'), + 'workflow should reference .next-call-count counter file' + ); + assert.ok( + content.includes('6'), + 'consecutive guard should trigger at count >= 6' + ); + assert.ok( + content.includes('consecutively'), + 'guard should mention consecutive calls' + ); + }); + + test('--force flag bypasses all gates', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok( + content.includes('--force'), + 'workflow should document --force flag' + ); + assert.ok( + content.includes('skipping safety gates'), + 'workflow should print warning when --force is used' + ); + }); + + test('command definition documents --force flag', () => { + const content = fs.readFileSync(commandPath, 'utf8'); + assert.ok( + content.includes('--force'), + 'command definition should mention --force flag' + ); + assert.ok( + content.includes('bypass safety gates'), + 'command definition should explain that --force bypasses safety gates' + ); + }); + + test('gates exit on first hit', () => { + const content = fs.readFileSync(workflowPath, 'utf8'); + assert.ok( + content.includes('Exit on first hit'), + 'safety gates should exit on first hit' + ); + }); +});