diff --git a/.changeset/sturdy-geese-roam.md b/.changeset/sturdy-geese-roam.md new file mode 100644 index 000000000..0043c5ed1 --- /dev/null +++ b/.changeset/sturdy-geese-roam.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 356 +--- +Accept JSON object input for state.patch so workflow state updates survive SDK retirement. diff --git a/get-shit-done/bin/lib/state-command-router.cjs b/get-shit-done/bin/lib/state-command-router.cjs index d0a435def..c0762b326 100644 --- a/get-shit-done/bin/lib/state-command-router.cjs +++ b/get-shit-done/bin/lib/state-command-router.cjs @@ -73,11 +73,28 @@ function routeStateCommand({ state, args, cwd, raw, error }) { null, () => { const patches = {}; - for (let i = 2; i < args.length; i += 2) { - const key = args[i].replace(/^--/, ''); - const value = args[i + 1]; - if (key && value !== undefined) { - patches[key] = value; + if (args.length === 3 && typeof args[2] === 'string' && args[2].trim().startsWith('{')) { + let parsed; + try { + parsed = JSON.parse(args[2]); + } catch (err) { + error(`state patch: invalid JSON object: ${err.message}`); + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + error('state patch: JSON input must be an object of field/value pairs.'); + } + for (const [key, value] of Object.entries(parsed)) { + if (key && value !== undefined) { + patches[key] = String(value); + } + } + } else { + for (let i = 2; i < args.length; i += 2) { + const key = args[i].replace(/^--/, ''); + const value = args[i + 1]; + if (key && value !== undefined) { + patches[key] = value; + } } } state.cmdStatePatch(cwd, patches, raw); diff --git a/tests/state.test.cjs b/tests/state.test.cjs index 42c06f4a8..98aab11b8 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -922,6 +922,27 @@ describe('cmdStatePatch and cmdStateUpdate (state patch, state update)', () => { assert.ok(updated.includes('**Last Activity:** 2024-01-15'), 'Last Activity should be unchanged'); }); + test('state patch accepts JSON object input from workflows', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd); + + const result = runGsdTools([ + 'query', + 'state.patch', + JSON.stringify({ + Status: 'Complete', + 'Current Phase': '04', + }), + ], tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.deepEqual(output.updated.sort(), ['Current Phase', 'Status'].sort()); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(updated.includes('**Status:** Complete'), 'Status should be updated to Complete'); + assert.ok(updated.includes('**Current Phase:** 04'), 'Current Phase should be updated to 04'); + }); + test('state patch reports failed fields that do not exist', () => { fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);