From bd7048985d23e8633e44e71d86d6bc68e7a6e0b2 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Fri, 10 Apr 2026 11:56:12 -0700 Subject: [PATCH 1/2] perf(workflow): default to inline execution for small plans (#1979) Plans with 1-2 tasks now execute inline (Pattern C) instead of spawning a subagent (Pattern A). This avoids ~14K token subagent spawn overhead and preserves the orchestrator's prompt cache for small plans. The threshold is configurable via workflow.inline_plan_threshold (default: 2). Set to 0 to always spawn subagents. Plans above the threshold continue to use checkpoint-based routing as before. Closes #1979 Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/lib/config.cjs | 1 + get-shit-done/references/planning-config.md | 2 ++ get-shit-done/workflows/execute-plan.md | 10 +++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 07da8c4e7..58ef53b23 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -28,6 +28,7 @@ const VALID_CONFIG_KEYS = new Set([ 'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template', 'planning.commit_docs', 'planning.search_gitignored', '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..39baccdd7 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -61,10 +61,18 @@ PLAN_START_EPOCH=$(date +%s) ```bash +TASK_COUNT=$(grep -c "^/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 `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 `TASK_COUNT > INLINE_THRESHOLD`: Apply checkpoint-based routing below. + +**Checkpoint-based routing (plans with > threshold tasks):** | Checkpoints | Pattern | Execution | |-------------|---------|-----------| From e5cc0bb48b45d70f620365ceb92af8e6c7ca9837 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sat, 11 Apr 2026 03:33:29 -0700 Subject: [PATCH 2/2] fix(workflow): correct grep anchor and add threshold=0 guard (#1979) Two correctness bugs from @trek-e review: 1. Grep pattern `^]` which matches at any indentation level and avoids false positives on or . 2. Threshold=0 was documented to disable inline routing but the condition `TASK_COUNT <= INLINE_THRESHOLD` evaluated 0<=0 as true, routing empty plans inline even when the feature was disabled. Fixed by guarding with `INLINE_THRESHOLD > 0`. Added tests/inline-plan-threshold.test.cjs (8 tests) covering: - config-set accepts the key and threshold=0 - VALID_CONFIG_KEYS and planning-config.md contain the entry - Routing pattern matches indented tasks and rejects / - Inline routing is guarded by INLINE_THRESHOLD > 0 Review feedback on #2061 from @trek-e. --- get-shit-done/workflows/execute-plan.md | 7 +- tests/inline-plan-threshold.test.cjs | 131 ++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 tests/inline-plan-threshold.test.cjs 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 = [ + '', + '', + '//