Merge pull request #1410 from Tibsfox/feat/manager-passthrough-flags-1400

feat(manager): add per-step passthrough flags via config
This commit is contained in:
Tom Boucher
2026-04-03 11:16:54 -04:00
committed by GitHub
6 changed files with 119 additions and 10 deletions

View File

@@ -28,6 +28,7 @@ const VALID_CONFIG_KEYS = new Set([
'workflow.subagent_timeout',
'hooks.context_warnings',
'project_code', 'phase_naming',
'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
]);
/**

View File

@@ -314,6 +314,7 @@ function loadConfig(cwd) {
subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
model_overrides: parsed.model_overrides || null,
agent_skills: parsed.agent_skills || {},
manager: parsed.manager || {},
};
} catch {
return defaults;

View File

@@ -1026,6 +1026,27 @@ function cmdInitManager(cwd, raw) {
});
const completedCount = phases.filter(p => p.disk_status === 'complete').length;
// Read manager flags from config (passthrough flags for each step)
// Validate: flags must be CLI-safe (only --flags, alphanumeric, hyphens, spaces)
const sanitizeFlags = (raw) => {
const val = typeof raw === 'string' ? raw : '';
if (!val) return '';
// Allow only --flag patterns with alphanumeric/hyphen values separated by spaces
const tokens = val.split(/\s+/).filter(Boolean);
const safe = tokens.every(t => /^--[a-zA-Z0-9][-a-zA-Z0-9]*$/.test(t) || /^[a-zA-Z0-9][-a-zA-Z0-9_.]*$/.test(t));
if (!safe) {
process.stderr.write(`gsd-tools: warning: manager.flags contains invalid tokens, ignoring: ${val}\n`);
return '';
}
return val;
};
const managerFlags = {
discuss: sanitizeFlags(config.manager && config.manager.flags && config.manager.flags.discuss),
plan: sanitizeFlags(config.manager && config.manager.flags && config.manager.flags.plan),
execute: sanitizeFlags(config.manager && config.manager.flags && config.manager.flags.execute),
};
const result = {
milestone_version: milestone.version,
milestone_name: milestone.name,
@@ -1039,6 +1060,7 @@ function cmdInitManager(cwd, raw) {
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
roadmap_exists: true,
state_exists: true,
manager_flags: managerFlags,
};
output(withProjectRoot(cwd, result), raw);

View File

@@ -14,6 +14,13 @@ Configuration options for `.planning/` directory behavior.
"phase_branch_template": "gsd/phase-{phase}-{slug}",
"milestone_branch_template": "gsd/{milestone}-{slug}",
"quick_branch_template": null
},
"manager": {
"flags": {
"discuss": "",
"plan": "",
"execute": ""
}
}
```
@@ -28,6 +35,9 @@ 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). |
| `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 |
</config_schema>
<commit_docs_behavior>

View File

@@ -23,7 +23,14 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init manager)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Parse JSON for: `milestone_version`, `milestone_name`, `phase_count`, `completed_count`, `in_progress_count`, `phases`, `recommended_actions`, `all_complete`, `waiting_signal`.
Parse JSON for: `milestone_version`, `milestone_name`, `phase_count`, `completed_count`, `in_progress_count`, `phases`, `recommended_actions`, `all_complete`, `waiting_signal`, `manager_flags`.
`manager_flags` contains per-step passthrough flags from config:
- `manager_flags.discuss` — appended to `/gsd:discuss-phase` args (e.g. `"--auto --analyze"`)
- `manager_flags.plan` — appended to plan agent init command
- `manager_flags.execute` — appended to execute agent init command
These are empty strings by default. Set via: `gsd-tools config-set manager.flags.discuss "--auto --analyze"`
**If error:** Display the error message and exit.
@@ -193,24 +200,24 @@ When the user selects a compound option:
2. **Then run the inline discuss:**
```
Skill(skill="gsd:discuss-phase", args="{PHASE_NUM}")
Skill(skill="gsd:discuss-phase", args="{PHASE_NUM} {manager_flags.discuss}")
```
After discuss completes, loop back to dashboard step (background agents continue running).
### Discuss Phase N
Discussion is interactive — needs user input. Run inline:
Discussion is interactive — needs user input. Run inline with any configured flags:
```
Skill(skill="gsd:discuss-phase", args="{PHASE_NUM}")
Skill(skill="gsd:discuss-phase", args="{PHASE_NUM} {manager_flags.discuss}")
```
After discuss completes, loop back to dashboard step.
### Plan Phase N
Planning runs autonomously. Spawn a background agent that delegates to the Skill pipeline:
Planning runs autonomously. Spawn a background agent that delegates to the Skill pipeline with any configured flags:
```
Task(
@@ -221,9 +228,10 @@ Task(
Working directory: {cwd}
Phase: {N} — {phase_name}
Goal: {goal}
Manager flags: {manager_flags.plan}
Run the plan-phase Skill:
Skill(skill=\"gsd:plan-phase\", args=\"{N} --auto\")
Run the plan-phase Skill with any configured manager flags:
Skill(skill=\"gsd:plan-phase\", args=\"{N} --auto {manager_flags.plan}\")
This delegates to the full plan-phase pipeline including local patches, research, plan-checker, and all quality gates.
@@ -241,7 +249,7 @@ Loop back to dashboard step.
### Execute Phase N
Execution runs autonomously. Spawn a background agent that delegates to the Skill pipeline:
Execution runs autonomously. Spawn a background agent that delegates to the Skill pipeline with any configured flags:
```
Task(
@@ -252,9 +260,10 @@ Task(
Working directory: {cwd}
Phase: {N} — {phase_name}
Goal: {goal}
Manager flags: {manager_flags.execute}
Run the execute-phase Skill:
Skill(skill=\"gsd:execute-phase\", args=\"{N}\")
Run the execute-phase Skill with any configured manager flags:
Skill(skill=\"gsd:execute-phase\", args=\"{N} {manager_flags.execute}\")
This delegates to the full execute-phase pipeline including local patches, branching, wave-based execution, verification, and all quality gates.

View File

@@ -413,4 +413,70 @@ describe('init manager', () => {
// macOS resolves /var → /private/var; normalize both sides
assert.strictEqual(fs.realpathSync(output.project_root), fs.realpathSync(tmpDir));
});
test('output includes manager_flags defaults when not configured', () => {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Test' }]);
const result = runGsdTools('init manager', tmpDir);
const output = JSON.parse(result.output);
assert.ok(output.manager_flags, 'should include manager_flags');
assert.strictEqual(output.manager_flags.discuss, '');
assert.strictEqual(output.manager_flags.plan, '');
assert.strictEqual(output.manager_flags.execute, '');
});
test('output includes manager_flags from config when set', () => {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Test' }]);
// Write config with manager flags
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({
manager: {
flags: {
discuss: '--auto --analyze',
plan: '--skip-research',
execute: '--interactive',
}
}
})
);
const result = runGsdTools('init manager', tmpDir);
const output = JSON.parse(result.output);
assert.ok(output.manager_flags, 'should include manager_flags');
assert.strictEqual(output.manager_flags.discuss, '--auto --analyze');
assert.strictEqual(output.manager_flags.plan, '--skip-research');
assert.strictEqual(output.manager_flags.execute, '--interactive');
});
test('sanitizes invalid manager_flags to prevent injection (#1410)', () => {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Test' }]);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({
manager: {
flags: {
discuss: '; rm -rf /',
plan: '--valid-flag',
execute: '$(whoami)',
}
}
})
);
const result = runGsdTools('init manager', tmpDir);
const output = JSON.parse(result.output);
// Invalid flags should be sanitized to empty string
assert.strictEqual(output.manager_flags.discuss, '', 'injection attempt should be sanitized');
assert.strictEqual(output.manager_flags.plan, '--valid-flag', 'valid flag should pass through');
assert.strictEqual(output.manager_flags.execute, '', 'command substitution should be sanitized');
});
});