From 7dd31e6d202cfde647b90b847033c65907f687c0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 11:59:39 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20signal=20file=20for=20decision=20points?= =?UTF-8?q?=20=E2=80=94=20WAITING.json=20(#1034)=20(#1133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When GSD hits a blocking decision point (AskUserQuestion, next action prompt), external watchers have no way to detect it. Users monitoring multiple auto sessions must visually check each terminal. Added: - state signal-waiting: writes .planning/WAITING.json (or .gsd/WAITING.json) with type, question, options, timestamp, and phase info - state signal-resume: removes WAITING.json when user answers Signal file format: { status, type, question, options[], since, phase } External tools can watch for this file via fswatch, polling, or inotify. Complements the existing remote-questions extension (Slack/Discord). Fixes #1034 --- get-shit-done/bin/gsd-tools.cjs | 17 ++++++++++++ get-shit-done/bin/lib/state.cjs | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 16975e8f2..cdee42566 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -15,6 +15,8 @@ * state get [section] Get STATE.md content or section * state patch --field val ... Batch update STATE.md fields * state begin-phase --phase N --name S --plans C Update STATE.md for new phase start + * state signal-waiting --type T --question Q --options "A|B" --phase P Write WAITING.json signal + * state signal-resume Remove WAITING.json signal * resolve-model Get model for agent based on profile * find-phase Find phase directory by number * commit [--files f1 f2] Commit planning docs @@ -255,6 +257,21 @@ async function main() { plansIdx !== -1 ? parseInt(args[plansIdx + 1], 10) : null, raw ); + } else if (subcommand === 'signal-waiting') { + const typeIdx = args.indexOf('--type'); + const qIdx = args.indexOf('--question'); + const optIdx = args.indexOf('--options'); + const phaseIdx = args.indexOf('--phase'); + state.cmdSignalWaiting( + cwd, + typeIdx !== -1 ? args[typeIdx + 1] : null, + qIdx !== -1 ? args[qIdx + 1] : null, + optIdx !== -1 ? args[optIdx + 1] : null, + phaseIdx !== -1 ? args[phaseIdx + 1] : null, + raw + ); + } else if (subcommand === 'signal-resume') { + state.cmdSignalResume(cwd, raw); } else { state.cmdStateLoad(cwd, raw); } diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 40bf8d2cc..78797694a 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -778,6 +778,53 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) { output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false'); } +/** + * Write a WAITING.json signal file when GSD hits a decision point. + * External watchers (fswatch, polling, orchestrators) can detect this. + * File is written to .planning/WAITING.json (or .gsd/WAITING.json if .gsd exists). + * Fixes #1034. + */ +function cmdSignalWaiting(cwd, type, question, options, phase, raw) { + const gsdDir = fs.existsSync(path.join(cwd, '.gsd')) ? path.join(cwd, '.gsd') : path.join(cwd, '.planning'); + const waitingPath = path.join(gsdDir, 'WAITING.json'); + + const signal = { + status: 'waiting', + type: type || 'decision_point', + question: question || null, + options: options ? options.split('|').map(o => o.trim()) : [], + since: new Date().toISOString(), + phase: phase || null, + }; + + try { + fs.mkdirSync(gsdDir, { recursive: true }); + fs.writeFileSync(waitingPath, JSON.stringify(signal, null, 2), 'utf-8'); + output({ signaled: true, path: waitingPath }, raw, 'true'); + } catch (e) { + output({ signaled: false, error: e.message }, raw, 'false'); + } +} + +/** + * Remove the WAITING.json signal file when user answers and agent resumes. + */ +function cmdSignalResume(cwd, raw) { + const paths = [ + path.join(cwd, '.gsd', 'WAITING.json'), + path.join(cwd, '.planning', 'WAITING.json'), + ]; + + let removed = false; + for (const p of paths) { + if (fs.existsSync(p)) { + try { fs.unlinkSync(p); removed = true; } catch {} + } + } + + output({ resumed: true, removed }, raw, removed ? 'true' : 'false'); +} + module.exports = { stateExtractField, stateReplaceField, @@ -796,4 +843,6 @@ module.exports = { cmdStateSnapshot, cmdStateJson, cmdStateBeginPhase, + cmdSignalWaiting, + cmdSignalResume, };