diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 93ca845ee..834ae06a9 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -33,6 +33,7 @@ const VALID_CONFIG_KEYS = new Set([ 'planning.commit_docs', 'planning.search_gitignored', 'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout', 'workflow.subagent_timeout', + 'workflow.inline_plan_threshold', 'hooks.context_warnings', 'features.thinking_partner', 'context', diff --git a/get-shit-done/references/planning-config.md b/get-shit-done/references/planning-config.md index 50691fb4c..99f45bac7 100644 --- a/get-shit-done/references/planning-config.md +++ b/get-shit-done/references/planning-config.md @@ -35,6 +35,7 @@ Configuration options for `.planning/` directory behavior. | `git.quick_branch_template` | `null` | Optional branch template for quick-task runs | | `workflow.use_worktrees` | `true` | Whether executor agents run in isolated git worktrees. Set to `false` to disable worktrees — agents execute sequentially on the main working tree instead. Recommended for solo developers or when worktree merges cause issues. | | `workflow.subagent_timeout` | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes). | +| `workflow.inline_plan_threshold` | `2` | Plans with this many tasks or fewer execute inline (Pattern C) instead of spawning a subagent. Avoids ~14K token spawn overhead for small plans. Set to `0` to always spawn subagents. | | `manager.flags.discuss` | `""` | Flags passed to `/gsd-discuss-phase` when dispatched from manager (e.g. `"--auto --analyze"`) | | `manager.flags.plan` | `""` | Flags passed to plan workflow when dispatched from manager | | `manager.flags.execute` | `""` | Flags passed to execute workflow when dispatched from manager | @@ -259,6 +260,7 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research": | `workflow.skip_discuss` | boolean | `false` | `true`, `false` | Skip discuss phase entirely | | `workflow.use_worktrees` | boolean | `true` | `true`, `false` | Run executor agents in isolated git worktrees | | `workflow.subagent_timeout` | number | `300000` | Any positive integer (ms) | Timeout for parallel subagent tasks (default: 5 minutes) | +| `workflow.inline_plan_threshold` | number | `2` | `0`–`10` | Plans with ≤N tasks execute inline instead of spawning a subagent | | `workflow.code_review` | boolean | `true` | `true`, `false` | Enable built-in code review step in the ship workflow | | `workflow.code_review_depth` | string | `"standard"` | `"light"`, `"standard"`, `"deep"` | Depth level for code review analysis in the ship workflow | | `workflow._auto_chain_active` | boolean | `false` | `true`, `false` | Internal: tracks whether autonomous chaining is active | diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 99aa9da1c..0da90b469 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -61,10 +61,19 @@ PLAN_START_EPOCH=$(date +%s) ```bash +# 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 ``` -**Routing by checkpoint type:** +**Primary routing: task count threshold (#1979)** + +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). + +Otherwise: Apply checkpoint-based routing below. + +**Checkpoint-based routing (plans with > threshold tasks):** | Checkpoints | Pattern | Execution | |-------------|---------|-----------| 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 = [ + '', + '', + '//