From 14fd090e4708fa67934ae7c8115f666630a27a9c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 7 Apr 2026 17:36:47 -0400 Subject: [PATCH] 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) * docs(config): add missing config fields to planning-config.md (#1880) Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Tibsfox Co-authored-by: Claude Opus 4.6 (1M context) --- get-shit-done/bin/gsd-tools.cjs | 22 +++++++- get-shit-done/references/planning-config.md | 12 +++++ tests/bug-1891-file-resolution.test.cjs | 58 +++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/bug-1891-file-resolution.test.cjs diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index fd699446..d0bfb19f 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -366,7 +366,27 @@ async function main() { return; } - await runCommand(command, args, cwd, raw, defaultValue); + // Intercept stdout to transparently resolve @file: references (#1891). + // core.cjs output() writes @file: 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); } /** diff --git a/get-shit-done/references/planning-config.md b/get-shit-done/references/planning-config.md index 3d3fb2bf..34db2749 100644 --- a/get-shit-done/references/planning-config.md +++ b/get-shit-done/references/planning-config.md @@ -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" } }`). diff --git a/tests/bug-1891-file-resolution.test.cjs b/tests/bug-1891-file-resolution.test.cjs new file mode 100644 index 00000000..c781916b --- /dev/null +++ b/tests/bug-1891-file-resolution.test.cjs @@ -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'); + }); +});