docs(config): document missing config keys in planning-config.md (#1947)

* fix(core): resolve @file: references in gsd-tools stdout (#1891)

Workflows used bash-specific `if [[ "$INIT" == @file:* ]]` to detect
when large JSON was written to a temp file. This syntax breaks on
PowerShell and other non-bash shells.

Intercept stdout in gsd-tools.cjs to transparently resolve @file:
references before they reach the caller, matching the existing --pick
path behavior. The bash checks in workflow files become harmless
no-ops and can be removed over time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(config): add missing config fields to planning-config.md (#1880)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Tibsfox <tibsfox@tibsfox.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-07 17:36:47 -04:00
committed by GitHub
parent 13faf66132
commit 14fd090e47
3 changed files with 91 additions and 1 deletions

View File

@@ -366,7 +366,27 @@ async function main() {
return;
}
// Intercept stdout to transparently resolve @file: references (#1891).
// core.cjs output() writes @file:<path> when JSON > 50KB. The --pick path
// already resolves this, but the normal path wrote @file: to stdout, forcing
// every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
// that breaks on PowerShell and other non-bash shells.
const origWriteSync2 = fs.writeSync;
const outChunks = [];
fs.writeSync = function (fd, data, ...rest) {
if (fd === 1) { outChunks.push(String(data)); return; }
return origWriteSync2.call(fs, fd, data, ...rest);
};
try {
await runCommand(command, args, cwd, raw, defaultValue);
} finally {
fs.writeSync = origWriteSync2;
}
let captured = outChunks.join('');
if (captured.startsWith('@file:')) {
captured = fs.readFileSync(captured.slice(6), 'utf-8');
}
origWriteSync2.call(fs, 1, captured);
}
/**

View File

@@ -234,6 +234,7 @@ Generated from `CONFIG_DEFAULTS` (core.cjs) and `VALID_CONFIG_KEYS` (config.cjs)
| `response_language` | string\|null | `null` | Any language name | Language for user-facing prompts (e.g., `"Portuguese"`, `"Japanese"`) |
| `context_window` | number | `200000` | `200000`, `1000000` | Context window size; set `1000000` for 1M-context models |
| `resolve_model_ids` | boolean\|string | `false` | `false`, `true`, `"omit"` | Map model aliases to full Claude IDs; `"omit"` returns empty string |
| `context` | string\|null | `null` | `"dev"`, `"research"`, `"review"` | Execution context profile that adjusts agent behavior: `"dev"` for development tasks, `"research"` for investigation/exploration, `"review"` for code review workflows |
### Workflow Fields
@@ -256,6 +257,8 @@ 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.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 |
### Git Fields
@@ -287,6 +290,7 @@ Set via `features.*` namespace (e.g., `"features": { "thinking_partner": true }`
| Key | Type | Default | Allowed Values | Description |
|-----|------|---------|----------------|-------------|
| `features.thinking_partner` | boolean | `false` | `true`, `false` | Enable conditional extended thinking at workflow decision points (used by discuss-phase and plan-phase for architectural tradeoff analysis) |
| `features.global_learnings` | boolean | `false` | `true`, `false` | Enable injection of global learnings from `~/.gsd/learnings/` into agent prompts |
### Hook Fields
@@ -296,6 +300,14 @@ Set via `hooks.*` namespace (e.g., `"hooks": { "context_warnings": true }`).
|-----|------|---------|----------------|-------------|
| `hooks.context_warnings` | boolean | `true` | `true`, `false` | Show warnings when context budget is exceeded |
### Learnings Fields
Set via `learnings.*` namespace (e.g., `"learnings": { "max_inject": 5 }`). Used together with `features.global_learnings`.
| Key | Type | Default | Allowed Values | Description |
|-----|------|---------|----------------|-------------|
| `learnings.max_inject` | number | `10` | Any positive integer | Maximum number of global learning entries to inject into agent prompts per session |
### Manager Fields
Set via `manager.*` namespace (e.g., `"manager": { "flags": { "discuss": "--auto" } }`).

View File

@@ -0,0 +1,58 @@
/**
* Regression tests for bug #1891
*
* gsd-tools.cjs must transparently resolve @file: references in stdout
* so that workflows never see the @file: prefix. This eliminates the
* bash-specific `if [[ "$INIT" == @file:* ]]` check that breaks on
* PowerShell and other non-bash shells.
*/
'use strict';
const { describe, test, before } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const GSD_TOOLS_SRC = path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
describe('bug #1891: @file: resolution in gsd-tools.cjs', () => {
let src;
before(() => {
src = fs.readFileSync(GSD_TOOLS_SRC, 'utf-8');
});
test('main() intercepts stdout and resolves @file: references', () => {
// The non-pick path should have @file: resolution, just like the --pick path
assert.ok(
src.includes("captured.startsWith('@file:')") ||
src.includes('captured.startsWith(\'@file:\')'),
'main() should check for @file: prefix in captured output'
);
});
test('@file: resolution reads file content via readFileSync', () => {
// Verify the resolution reads the actual file
assert.ok(
src.includes("readFileSync(captured.slice(6)") ||
src.includes('readFileSync(captured.slice(6)'),
'@file: resolution should read file at the path after the prefix'
);
});
test('stdout interception wraps runCommand in the non-pick path', () => {
// The main function should intercept fs.writeSync for fd=1
// in BOTH the pick path AND the normal path
const mainFunc = src.slice(src.indexOf('async function main()'));
const pickInterception = mainFunc.indexOf('// When --pick is active');
const fileResolution = mainFunc.indexOf('@file:');
// There should be at least two @file: resolution points:
// one in the --pick path and one in the normal path
const firstAt = mainFunc.indexOf("'@file:'");
const secondAt = mainFunc.indexOf("'@file:'", firstAt + 1);
assert.ok(secondAt > firstAt,
'Both --pick and normal paths should resolve @file: references');
});
});