feat(#1452): add workflow.context_guard_mode to guard execute-phase against context exhaustion

Proactive checkpoint guard fires at each wave boundary before spawning agents.
Self-assesses context pressure against context-budget.md degradation tiers and
warns (warn, default) or auto-invokes /gsd:pause-work (auto) when POOR tier
(70%+) is detected. Config key validated; defaults to \"warn\".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-20 18:10:49 -04:00
parent 9d057882e9
commit 4eca5ac96c
9 changed files with 247 additions and 9 deletions

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 1452
---
**`workflow.context_guard_mode` config key** — proactive context-exhaustion guard for `execute-phase`. Before each wave, the orchestrator self-assesses context pressure using the degradation signals defined in `context-budget.md`. Values: `warn` (default — emit warning and recommend `/gsd:pause-work` when POOR tier detected), `auto` (automatically invoke `/gsd:pause-work` before next wave), `off` (disable). Set via `gsd config-set workflow.context_guard_mode auto` for fully autonomous checkpoint behaviour. (#1452)

View File

@@ -53,7 +53,8 @@
"post_planning_gaps": true,
"security_enforcement": true,
"security_asvs_level": 1,
"security_block_on": "high"
"security_block_on": "high",
"context_guard_mode": "warn"
},
"planning": {
"commit_docs": true,

View File

@@ -56,6 +56,7 @@
"workflow.test_command",
"workflow.build_command",
"workflow.mvp_mode",
"workflow.context_guard_mode",
"executor.stall_detect_interval_minutes",
"executor.stall_threshold_minutes",
"workflow.inline_plan_threshold",

View File

@@ -29,14 +29,14 @@ Every workflow that spawns agents or reads significant content must follow these
## Context Degradation Tiers
Monitor context usage and adjust behavior accordingly:
Monitor context usage and adjust behavior accordingly. The `workflow.context_guard_mode` config key (values: `auto`, `warn`, `off`; default `warn`) controls how `execute-phase.md` responds when the guard fires at a wave boundary.
| Tier | Usage | Behavior |
|------|-------|----------|
| PEAK | 0-30% | Full operations. Read bodies, spawn multiple agents, inline results. |
| GOOD | 30-50% | Normal operations. Prefer frontmatter reads, delegate aggressively. |
| DEGRADING | 50-70% | Economize. Frontmatter-only reads, minimal inlining, warn user about budget. |
| POOR | 70%+ | Emergency mode. Checkpoint progress immediately. No new reads unless critical. |
| Tier | Usage | Behavior | Trigger Action (execute-phase) |
|------|-------|----------|-------------------------------|
| PEAK | 0-30% | Full operations. Read bodies, spawn multiple agents, inline results. | None |
| GOOD | 30-50% | Normal operations. Prefer frontmatter reads, delegate aggressively. | None |
| DEGRADING | 50-70% | Economize. Frontmatter-only reads, minimal inlining, warn user about budget. | Emit warning, continue |
| POOR | 70%+ | Emergency mode. Checkpoint progress immediately. No new reads unless critical. | `warn`: emit warning + recommend `/gsd:pause-work`. `auto`: invoke pause-work before next wave. `off`: proceed anyway. |
## Context Degradation Warning Signs

View File

@@ -267,6 +267,7 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
| `workflow.test_command` | string\|null | `null` | Any shell command | Regression/test gate command run by verify-phase, execute-phase, audit-fix, and post-merge-gate. Unset → GSD auto-detects (Makefile / package.json / Cargo.toml / go.mod / pyproject.toml). |
| `workflow.build_command` | string\|null | `null` | Any shell command | Build gate command run by the post-merge gate. Unset → build step auto-detected/skipped. |
| `workflow.mvp_mode` | boolean | `false` | `true`, `false` | Persist the MVP-mode flag in config so every phase defaults to MVP framing without requiring `--mvp` on the CLI. Resolved via the chain: `--mvp` CLI flag → ROADMAP.md `**Mode:** mvp` field → this config value → `false`. When `true`, the planner, executor, verifier, and discovery surfaces (progress, stats, graphify) all treat the phase as an MVP vertical slice (UI → API → DB) of one user-visible capability. |
| `workflow.context_guard_mode` | string | `"warn"` | `"auto"`, `"warn"`, `"off"` | Context exhaustion guard mode for `execute-phase`. Before each wave, the orchestrator self-assesses context pressure using degradation signals from `context-budget.md`. `"warn"` (default): emit a warning and recommend `/gsd:pause-work` when POOR tier is detected. `"auto"`: automatically invoke `/gsd:pause-work` before the next wave when POOR tier is detected. `"off"`: disable the guard. The guard is heuristic — no programmatic context-% API exists. |
| `workflow.plan_chunked` | boolean | `false` | `true`, `false` | Enable chunked planning mode. When `true`, the plan-phase orchestrator splits the single long-lived planner Task into a short outline Task followed by N short per-plan Tasks (~3–5 min each). Each plan is committed individually for crash resilience. Particularly useful on Windows where long-lived Tasks may hang on stdio. Also activated by the `--chunked` flag. |
| `workflow.code_review_command` | string\|null | `null` | Any shell command | External code-review command integrated into `/gsd:ship`. The diff is piped to the command via stdin; the command must output JSON with a `verdict` field (`"APPROVED"` or `"REVISE"`). Non-zero exit or `"REVISE"` verdict blocks the ship workflow. When unset, the built-in review flow runs. Example: `my-review-tool --review`. |
| `workflow.inline_plan_threshold` | number | `2` | `0`–`10` | Plans with ≤N tasks execute inline instead of spawning a subagent |

View File

@@ -493,6 +493,23 @@ increases monotonically across waves. `{status}` is `complete` (success),
@~/.claude/gsd-core/references/execute-phase-wave-guard.md
0. **Context exhaustion guard — `context_guard` (BEFORE spawning, #1452):**
Before spawning any agents for this wave, self-assess context pressure using the
degradation signals in `references/context-budget.md`. Signs of POOR tier (70%+):
increasing vagueness, skipped steps, silent partial completion.
Read `workflow.context_guard_mode` from `.planning/config.json` (default `warn`).
| Tier | `warn` (default) | `auto` | `off` |
|------|-----------------|--------|-------|
| PEAK / GOOD | No output | No output | No output |
| DEGRADING (50-70%) | Emit: "⚠ Context pressure DEGRADING — switching to frontmatter-only reads for remaining waves." Continue. | Same as warn | Skip |
| POOR (70%+) | Emit: "🛑 Context pressure POOR — risk of context exhaustion. Run `/gsd:pause-work` to checkpoint before this wave, then resume in a fresh session." Continue (user decides). | Invoke `/gsd:pause-work` immediately and halt. Do NOT spawn wave agents. | Skip |
The guard is heuristic — no programmatic context-percentage API exists. Use your
assessment of degradation signals, not a fixed token count.
1. **Intra-wave files_modified overlap check (BEFORE spawning):**
Before spawning any agents for this wave, inspect the `files_modified` list of all plans

View File

@@ -239,6 +239,7 @@ function buildNewProjectConfig(userChoices: Record<string, unknown>): Record<str
ui_safety_gate: true,
ai_integration_phase: true,
human_verify_mode: 'end-of-phase',
context_guard_mode: 'warn',
text_mode: false,
research_before_questions: false,
discuss_mode: 'discuss',
@@ -613,6 +614,12 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
error(`Invalid workflow.human_verify_mode '${val}'. Valid values: ${VALID_HUMAN_VERIFY_MODES.join(', ')}`);
}
// Context exhaustion guard mode (#1452)
const VALID_CONTEXT_GUARD_MODES = ['auto', 'warn', 'off'];
if (kp === 'workflow.context_guard_mode' && !VALID_CONTEXT_GUARD_MODES.includes(String(parsedValue))) {
error(`Invalid workflow.context_guard_mode '${val}'. Valid values: ${VALID_CONTEXT_GUARD_MODES.join(', ')}`);
}
// Context position enum validation (#2937)
const VALID_CONTEXT_POSITIONS = ['front', 'end'];
if (kp === 'statusline.context_position' && !VALID_CONTEXT_POSITIONS.includes(String(parsedValue))) {

View File

@@ -0,0 +1,206 @@
// allow-test-rule: source-text-is-the-product
// The execute-phase.md workflow and context-budget.md reference ARE the runtime
// contract loaded by AI runtimes. Asserting that the canonical wording for
// `workflow.context_guard_mode` is present in those files is the only way to
// verify runtimes will respect the flag at runtime.
/**
* Enhancement #1452: workflow.context_guard_mode
*
* Guards long execute-phase workflows from driving the host session to context
* exhaustion (ctx 100%). Before each wave, the orchestrator self-assesses
* context pressure using the degradation signals defined in context-budget.md.
*
* Modes:
* "warn" (default) — emit a structured warning + recommend /gsd:pause-work
* "auto" — auto-invoke pause-work before the next wave
* "off" — disable the guard entirely
*
* The check fires at wave boundaries ONLY (before spawning), never mid-wave.
*/
'use strict';
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
function readConfig(tmpDir) {
const configPath = path.join(tmpDir, '.planning', 'config.json');
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
const REPO_ROOT = path.join(__dirname, '..');
// ─── Schema registration ──────────────────────────────────────────────────────
describe('workflow.context_guard_mode in VALID_CONFIG_KEYS', () => {
test('is a recognized config key', () => {
const { VALID_CONFIG_KEYS } = require('../gsd-core/bin/lib/config.cjs');
assert.ok(
VALID_CONFIG_KEYS.has('workflow.context_guard_mode'),
'workflow.context_guard_mode should be in VALID_CONFIG_KEYS',
);
});
});
// ─── Default value ────────────────────────────────────────────────────────────
describe('workflow.context_guard_mode default value', () => {
let tmpDir;
beforeEach(() => { tmpDir = createTempProject(); });
afterEach(() => { cleanup(tmpDir); });
test('defaults to warn in new project config', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir });
assert.ok(result.success, `config-ensure-section failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(
config.workflow.context_guard_mode,
'warn',
'workflow.context_guard_mode should default to "warn" — proactive checkpoint warning without auto-pausing workflows',
);
});
});
// ─── Round-trip ──────────────────────────────────────────────────────────────
describe('workflow.context_guard_mode config round-trip', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir });
});
afterEach(() => { cleanup(tmpDir); });
test('config-set warn persists to config.json', () => {
const setResult = runGsdTools('config-set workflow.context_guard_mode warn', tmpDir);
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.context_guard_mode, 'warn');
});
test('config-set auto persists to config.json', () => {
const setResult = runGsdTools('config-set workflow.context_guard_mode auto', tmpDir);
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.context_guard_mode, 'auto');
});
test('config-set off persists to config.json', () => {
const setResult = runGsdTools('config-set workflow.context_guard_mode off', tmpDir);
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.context_guard_mode, 'off');
});
test('persists in config.json as string', () => {
runGsdTools('config-set workflow.context_guard_mode warn', tmpDir);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.context_guard_mode, 'warn');
assert.strictEqual(typeof config.workflow.context_guard_mode, 'string');
});
test('rejects unknown mode values with clear error', () => {
const result = runGsdTools('config-set workflow.context_guard_mode aggressive', tmpDir);
assert.strictEqual(result.success, false);
assert.match(result.error, /Invalid workflow\.context_guard_mode 'aggressive'/);
assert.match(result.error, /auto, warn, off/);
});
test('rejects partial match values', () => {
const result = runGsdTools('config-set workflow.context_guard_mode warnmode', tmpDir);
assert.strictEqual(result.success, false);
assert.match(result.error, /Invalid workflow\.context_guard_mode 'warnmode'/);
});
});
// ─── execute-phase contract ───────────────────────────────────────────────────
describe('execute-phase.md documents the context_guard step', () => {
let executePhase;
beforeEach(() => {
executePhase = fs.readFileSync(
path.join(REPO_ROOT, 'gsd-core', 'workflows', 'execute-phase.md'),
'utf-8',
);
});
test('references workflow.context_guard_mode by canonical name', () => {
assert.ok(
executePhase.includes('workflow.context_guard_mode'),
'execute-phase.md must reference workflow.context_guard_mode so runtimes resolve the config-driven behavior',
);
});
test('defines context_guard step at wave boundaries', () => {
assert.ok(
executePhase.includes('context_guard') || executePhase.includes('context-guard'),
'execute-phase.md must define a context_guard step that fires before each wave',
);
});
test('references context-budget.md tiers in the guard step', () => {
assert.ok(
executePhase.includes('context-budget') || executePhase.includes('POOR') || executePhase.includes('DEGRADING'),
'execute-phase.md context_guard must reference context-budget.md degradation tiers',
);
});
});
// ─── context-budget.md contract ──────────────────────────────────────────────
describe('context-budget.md documents POOR-tier trigger action', () => {
let contextBudget;
beforeEach(() => {
contextBudget = fs.readFileSync(
path.join(REPO_ROOT, 'gsd-core', 'references', 'context-budget.md'),
'utf-8',
);
});
test('defines POOR tier', () => {
assert.ok(
contextBudget.includes('POOR'),
'context-budget.md must define the POOR tier',
);
});
test('connects POOR tier to pause-work', () => {
assert.ok(
contextBudget.includes('pause-work') || contextBudget.includes('pause_work'),
'context-budget.md POOR-tier rule must reference pause-work as the trigger action',
);
});
test('documents context_guard_mode values', () => {
assert.ok(
contextBudget.includes('context_guard_mode'),
'context-budget.md must document the workflow.context_guard_mode config key',
);
});
});
// ─── planning-config.md reference parity ─────────────────────────────────────
describe('planning-config.md documents workflow.context_guard_mode', () => {
test('includes the key in the reference table', () => {
const planningConfig = fs.readFileSync(
path.join(REPO_ROOT, 'gsd-core', 'references', 'planning-config.md'),
'utf-8',
);
assert.ok(
planningConfig.includes('workflow.context_guard_mode'),
'planning-config.md reference must include workflow.context_guard_mode so users know the config knob exists',
);
});
});

View File

@@ -24,7 +24,7 @@
"docs-update.md": 55662,
"edit-phase.md": 12883,
"eval-review.md": 9923,
"execute-phase.md": 92851,
"execute-phase.md": 93995,
"execute-plan.md": 31365,
"explore.md": 10497,
"extract-learnings.md": 12849,