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:
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
129
tests/next-safety-gates.test.cjs
Normal file
129
tests/next-safety-gates.test.cjs
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user