From 3a56a207a58299bb8749c69f807240bb2ae2ad6d Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Mon, 23 Feb 2026 09:31:06 -0600 Subject: [PATCH] fix(gsd-tools): support --cwd override for state-snapshot Add --cwd / --cwd= support so sandboxed subagents running outside the project root can target a specific directory. Invalid paths return a clear error. Tests ported to tests/state.test.cjs (the old monolithic test file was split into domain files on main). Closes #622 Co-Authored-By: Colin Johnson Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 3 +++ get-shit-done/bin/gsd-tools.cjs | 26 ++++++++++++++++++++++++-- tests/state.test.cjs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 631a353a5..3d8146bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed +- `gsd-tools state-snapshot` supports `--cwd ` so tooling can target a project directory when invoked from outside the repo + ### Changed - `/gsd:debug` flow now requires a `human-verify` checkpoint after self-verification before marking debug sessions `resolved` and moving files to `.planning/debug/resolved/` diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index a83b62928..8c721c39d 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -123,6 +123,8 @@ * init progress All context for progress workflow */ +const fs = require('fs'); +const path = require('path'); const { error } = require('./lib/core.cjs'); const state = require('./lib/state.cjs'); const phase = require('./lib/phase.cjs'); @@ -139,15 +141,35 @@ const frontmatter = require('./lib/frontmatter.cjs'); async function main() { const args = process.argv.slice(2); + + // Optional cwd override for sandboxed subagents running outside project root. + let cwd = process.cwd(); + const cwdEqArg = args.find(arg => arg.startsWith('--cwd=')); + const cwdIdx = args.indexOf('--cwd'); + if (cwdEqArg) { + const value = cwdEqArg.slice('--cwd='.length).trim(); + if (!value) error('Missing value for --cwd'); + args.splice(args.indexOf(cwdEqArg), 1); + cwd = path.resolve(value); + } else if (cwdIdx !== -1) { + const value = args[cwdIdx + 1]; + if (!value || value.startsWith('--')) error('Missing value for --cwd'); + args.splice(cwdIdx, 2); + cwd = path.resolve(value); + } + + if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) { + error(`Invalid --cwd: ${cwd}`); + } + const rawIndex = args.indexOf('--raw'); const raw = rawIndex !== -1; if (rawIndex !== -1) args.splice(rawIndex, 1); const command = args[0]; - const cwd = process.cwd(); if (!command) { - error('Usage: gsd-tools [args] [--raw]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init'); + error('Usage: gsd-tools [args] [--raw] [--cwd ]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init'); } switch (command) { diff --git a/tests/state.test.cjs b/tests/state.test.cjs index 6b22460ca..95c853633 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -148,6 +148,36 @@ describe('state-snapshot command', () => { const output = JSON.parse(result.output); assert.strictEqual(output.paused_at, 'Phase 3, Plan 1, Task 2 - mid-implementation', 'paused_at extracted'); }); + + test('supports --cwd override when command runs outside project root', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# Session State + +**Current Phase:** 03 +**Status:** Ready to plan +` + ); + const outsideDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gsd-test-outside-')); + + try { + const result = runGsdTools(`state-snapshot --cwd "${tmpDir}"`, outsideDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.current_phase, '03', 'should read STATE.md from overridden cwd'); + assert.strictEqual(output.status, 'Ready to plan', 'should parse status from overridden cwd'); + } finally { + cleanup(outsideDir); + } + }); + + test('returns error for invalid --cwd path', () => { + const invalid = path.join(tmpDir, 'does-not-exist'); + const result = runGsdTools(`state-snapshot --cwd "${invalid}"`, tmpDir); + assert.ok(!result.success, 'should fail for invalid --cwd'); + assert.ok(result.error.includes('Invalid --cwd'), 'error should mention invalid --cwd'); + }); }); // ─────────────────────────────────────────────────────────────────────────────