diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 39baccdd7..0da90b469 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -61,16 +61,17 @@ PLAN_START_EPOCH=$(date +%s) ```bash -TASK_COUNT=$(grep -c "^/dev/null || echo "0") +# Count tasks — match ]' .planning/phases/XX-name/{phase}-{plan}-PLAN.md 2>/dev/null || echo "0") INLINE_THRESHOLD=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.inline_plan_threshold --default 2 2>/dev/null || echo "2") grep -n "type=\"checkpoint" .planning/phases/XX-name/{phase}-{plan}-PLAN.md ``` **Primary routing: task count threshold (#1979)** -If `TASK_COUNT <= INLINE_THRESHOLD`: Use Pattern C (inline) regardless of checkpoint type. Small plans execute faster inline — avoids ~14K token subagent spawn overhead and preserves prompt cache. Configure threshold via `workflow.inline_plan_threshold` (default: 2). +If `INLINE_THRESHOLD > 0` AND `TASK_COUNT <= INLINE_THRESHOLD`: Use Pattern C (inline) regardless of checkpoint type. Small plans execute faster inline — avoids ~14K token subagent spawn overhead and preserves prompt cache. Configure threshold via `workflow.inline_plan_threshold` (default: 2, set to `0` to always spawn subagents). -If `TASK_COUNT > INLINE_THRESHOLD`: Apply checkpoint-based routing below. +Otherwise: Apply checkpoint-based routing below. **Checkpoint-based routing (plans with > threshold tasks):** diff --git a/tests/inline-plan-threshold.test.cjs b/tests/inline-plan-threshold.test.cjs new file mode 100644 index 000000000..01f684c89 --- /dev/null +++ b/tests/inline-plan-threshold.test.cjs @@ -0,0 +1,131 @@ +/** + * Tests for workflow.inline_plan_threshold config key and routing logic (#1979). + * + * Verifies: + * 1. The config key is accepted by config-set (VALID_CONFIG_KEYS contains it) + * 2. The key is documented in planning-config.md + * 3. The execute-plan.md routing instruction uses the correct grep pattern + * (matches { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('config-set accepts workflow.inline_plan_threshold', () => { + const result = runGsdTools('config-set workflow.inline_plan_threshold 3', tmpDir); + assert.ok(result.success, `config-set should accept workflow.inline_plan_threshold: ${result.error}`); + }); + + test('config-set accepts threshold=0 to disable inline routing', () => { + const result = runGsdTools('config-set workflow.inline_plan_threshold 0', tmpDir); + assert.ok(result.success, `config-set should accept 0: ${result.error}`); + }); + + test('VALID_CONFIG_KEYS in config.cjs contains workflow.inline_plan_threshold', () => { + const content = fs.readFileSync(configCjsPath, 'utf-8'); + assert.match( + content, + /['"]workflow\.inline_plan_threshold['"]/, + 'workflow.inline_plan_threshold must be in VALID_CONFIG_KEYS' + ); + }); + + test('planning-config.md documents workflow.inline_plan_threshold', () => { + const content = fs.readFileSync(planningConfigPath, 'utf-8'); + assert.match( + content, + /workflow\.inline_plan_threshold/, + 'planning-config.md must document workflow.inline_plan_threshold' + ); + }); +}); + +describe('execute-plan.md routing instruction (#1979)', () => { + test('grep pattern matches { + const content = fs.readFileSync(executePlanPath, 'utf-8'); + + // The new pattern should use \s* for leading whitespace, not ^ anchor alone + // Must match both " { + // Simulate how the grep pattern would behave against sample PLAN.md content + // Extract the pattern from execute-plan.md + const content = fs.readFileSync(executePlanPath, 'utf-8'); + const patternMatch = content.match(/TASK_COUNT=\$\(grep -cE '([^']+)'/); + assert.ok(patternMatch, 'must find TASK_COUNT grep pattern'); + + const regexSource = patternMatch[1].replace(/\\s/g, '\\s').replace(/\[\[:space:\]>\]/, '[\\s>]'); + const re = new RegExp(regexSource, 'gm'); + + // Test cases: should match all of these as single tasks + const samples = [ + '', + ' ', + ' ', + '\t', + ]; + for (const sample of samples) { + const matches = sample.match(re); + assert.ok(matches && matches.length > 0, `Pattern must match: ${JSON.stringify(sample)}`); + } + + // Non-task lines should not match + const nonMatches = [ + '', + '', + '//