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 <noreply@anthropic.com>
This commit is contained in:
Tibsfox
2026-04-05 14:05:06 -07:00
committed by GitHub
parent 4c8719d84a
commit 3a277f8ba8
3 changed files with 187 additions and 0 deletions

View File

@@ -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).
</objective>
<execution_context>

View File

@@ -34,6 +34,62 @@ No GSD project detected. Run `/gsd-new-project` to get started.
Exit.
</step>
<step name="safety_gates">
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)
</step>
<step name="determine_next_action">
Apply routing rules based on state:

View File

@@ -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('<step name="safety_gates">'),
'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'
);
});
});