fix(init): embed auto_advance/auto_chain_active/mode in init plan-phase output (#2228)

Prevents infinite config-get loops on Kimi K2.5 and other models that
re-execute bash tool calls when they encounter config-get subshell patterns.
Values are now bundled into the init plan-phase JSON so step 15 of
plan-phase.md can read them directly without separate shell calls.

Closes #2192

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-15 14:59:15 -04:00
committed by GitHub
parent 7b85d9e689
commit 875b257c18
4 changed files with 84 additions and 7 deletions

View File

@@ -377,6 +377,9 @@ function loadConfig(cwd) {
exa_search: get('exa_search') ?? defaults.exa_search,
tdd_mode: get('tdd_mode', { section: 'workflow', field: 'tdd_mode' }) ?? false,
text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode,
auto_advance: get('auto_advance', { section: 'workflow', field: 'auto_advance' }) ?? false,
_auto_chain_active: get('_auto_chain_active', { section: 'workflow', field: '_auto_chain_active' }) ?? false,
mode: get('mode') ?? 'interactive',
sub_repos: get('sub_repos', { section: 'planning', field: 'sub_repos' }) ?? defaults.sub_repos,
resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
context_window: get('context_window') ?? defaults.context_window,

View File

@@ -238,6 +238,12 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
nyquist_validation_enabled: config.nyquist_validation,
commit_docs: config.commit_docs,
text_mode: config.text_mode,
// Auto-advance config — included so workflows don't need separate config-get
// calls for these values, which causes infinite config-read loops on some models
// (e.g. Kimi K2.5). See #2192.
auto_advance: !!(config.auto_advance),
auto_chain_active: !!(config._auto_chain_active),
mode: config.mode || 'interactive',
// Phase info
phase_found: !!phaseInfo,

View File

@@ -1145,20 +1145,20 @@ Route to `<offer_next>` OR `auto_advance` depending on flags/config.
## 15. Auto-Advance Check
Check for auto-advance trigger:
Check for auto-advance trigger using values already loaded in step 1:
1. Parse `--auto` and `--chain` flags from $ARGUMENTS
2. **Sync chain flag with intent** — if user invoked manually (no `--auto` and no `--chain`), clear the ephemeral chain flag from any previous interrupted `--auto` chain. This does NOT touch `workflow.auto_advance` (the user's persistent settings preference):
2. Use `auto_chain_active` and `auto_advance` from the INIT JSON parsed in step 1 — **do not issue additional `config-get` calls for these values** (they are already present in the init output). Issuing redundant `config-get` calls for values already in INIT can cause infinite read loops on some runtimes.
3. **Sync chain flag with intent** — if user invoked manually (no `--auto` and no `--chain`), clear the ephemeral chain flag from any previous interrupted `--auto` chain. This does NOT touch `workflow.auto_advance` (the user's persistent settings preference):
```bash
if [[ ! "$ARGUMENTS" =~ --auto ]] && [[ ! "$ARGUMENTS" =~ --chain ]]; then
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-set workflow._auto_chain_active false 2>/dev/null
fi
```
3. Read both the chain flag and user preference:
```bash
AUTO_CHAIN=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow._auto_chain_active 2>/dev/null || echo "false")
AUTO_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.auto_advance 2>/dev/null || echo "false")
```
Set local variables from INIT (parsed once in step 1):
- `AUTO_CHAIN` = `auto_chain_active` from INIT JSON (boolean, default false)
- `AUTO_CFG` = `auto_advance` from INIT JSON (boolean, default false)
**If `--auto` or `--chain` flag present AND `AUTO_CHAIN` is not true:** Persist chain flag to config (handles direct invocation without prior discuss-phase):
```bash

View File

@@ -1358,6 +1358,74 @@ describe('findProjectRoot integration via --cwd', () => {
});
});
// ─────────────────────────────────────────────────────────────────────────────
// #2192: init plan-phase must include auto_advance, auto_chain_active, and mode
// so workflows don't need separate config-get calls that loop on Kimi K2.5
// ─────────────────────────────────────────────────────────────────────────────
describe('#2192: init plan-phase includes auto-advance config to prevent separate config-get loops', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-auth'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
['# Roadmap', '', '## Milestone v1', '', '### Phase 1: Auth', '**Goal:** Auth'].join('\n')
);
});
afterEach(() => {
cleanup(tmpDir);
});
test('init plan-phase includes auto_advance field (defaults false)', () => {
const result = runGsdTools('init plan-phase 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok('auto_advance' in output, 'init plan-phase must include auto_advance field');
assert.strictEqual(output.auto_advance, false, 'auto_advance should default to false');
});
test('init plan-phase includes auto_chain_active field (defaults false)', () => {
const result = runGsdTools('init plan-phase 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok('auto_chain_active' in output, 'init plan-phase must include auto_chain_active field');
assert.strictEqual(output.auto_chain_active, false, 'auto_chain_active should default to false');
});
test('init plan-phase includes mode field (defaults to interactive)', () => {
const result = runGsdTools('init plan-phase 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok('mode' in output, 'init plan-phase must include mode field');
assert.strictEqual(output.mode, 'interactive', 'mode should default to interactive');
});
test('init plan-phase reflects auto_advance true when set in config', () => {
const configPath = path.join(tmpDir, '.planning', 'config.json');
const cfg = { workflow: { auto_advance: true } };
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
const result = runGsdTools('init plan-phase 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.auto_advance, true, 'auto_advance should reflect config value');
});
test('init plan-phase reflects auto_chain_active true when set in config', () => {
const configPath = path.join(tmpDir, '.planning', 'config.json');
const cfg = { workflow: { _auto_chain_active: true } };
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
const result = runGsdTools('init plan-phase 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.auto_chain_active, true, 'auto_chain_active should reflect config value');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// roadmap analyze command
// ─────────────────────────────────────────────────────────────────────────────