gsd-pi 2.3.5 → 2.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +26 -12
  2. package/dist/cli.js +24 -1
  3. package/dist/wizard.js +16 -0
  4. package/package.json +1 -1
  5. package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +60 -0
  6. package/scripts/postinstall.js +5 -3
  7. package/src/resources/extensions/ask-user-questions.ts +54 -5
  8. package/src/resources/extensions/gsd/auto.ts +17 -3
  9. package/src/resources/extensions/gsd/commands.ts +16 -3
  10. package/src/resources/extensions/gsd/index.ts +17 -1
  11. package/src/resources/extensions/gsd/preferences.ts +17 -1
  12. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
  13. package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
  14. package/src/resources/extensions/gsd/worktree.ts +11 -0
  15. package/src/resources/extensions/remote-questions/config.ts +81 -0
  16. package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
  17. package/src/resources/extensions/remote-questions/format.ts +163 -0
  18. package/src/resources/extensions/remote-questions/manager.ts +192 -0
  19. package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
  20. package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
  21. package/src/resources/extensions/remote-questions/status.ts +31 -0
  22. package/src/resources/extensions/remote-questions/store.ts +77 -0
  23. package/src/resources/extensions/remote-questions/types.ts +75 -0
  24. package/src/resources/extensions/github/formatters.ts +0 -207
  25. package/src/resources/extensions/github/gh-api.ts +0 -553
  26. package/src/resources/extensions/github/index.ts +0 -778
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  # GSD 2
4
4
 
5
- **The evolution of [Get Shit Done](https://github.com/glittercowboy/get-shit-done) — now a real coding agent.**
5
+ **The evolution of [Get Shit Done](https://github.com/gsd-build/get-shit-done) — now a real coding agent.**
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/gsd-pi?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/gsd-pi)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/gsd-pi?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/gsd-pi)
9
- [![GitHub stars](https://img.shields.io/github/stars/glittercowboy/gsd-pi?style=for-the-badge&logo=github&color=181717)](https://github.com/glittercowboy/gsd-pi)
9
+ [![GitHub stars](https://img.shields.io/github/stars/gsd-build/GSD-2?style=for-the-badge&logo=github&color=181717)](https://github.com/gsd-build/GSD-2)
10
10
  [![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE)
11
11
 
12
12
  The original GSD went viral as a prompt framework for Claude Code. It worked, but it was fighting the tool — injecting prompts through slash commands, hoping the LLM would follow instructions, with no actual control over context windows, sessions, or execution.
@@ -122,16 +122,18 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
122
122
 
123
123
  9. **Escape hatch** — Press Escape to pause. The conversation is preserved. Interact with the agent, inspect what happened, or just `/gsd auto` to resume from disk state.
124
124
 
125
- ### The `/gsd` Wizard
125
+ ### `/gsd` and `/gsd next` — Step Mode
126
126
 
127
- When you're not in auto mode, `/gsd` reads disk state and shows contextual options:
127
+ By default, `/gsd` runs in **step mode**: the same state machine as auto mode, but it pauses between units with a wizard showing what completed and what's next. You advance one step at a time, review the output, and continue when ready.
128
128
 
129
129
  - **No `.gsd/` directory** → Start a new project. Discussion flow captures your vision, constraints, and preferences.
130
130
  - **Milestone exists, no roadmap** → Discuss or research the milestone.
131
- - **Roadmap exists, slices pending** → Plan the next slice, or jump straight to auto.
131
+ - **Roadmap exists, slices pending** → Plan the next slice, execute one task, or switch to auto.
132
132
  - **Mid-task** → Resume from where you left off.
133
133
 
134
- The wizard is the on-ramp. Auto mode is the highway.
134
+ `/gsd next` is an explicit alias for step mode. You can switch from step → auto mid-session via the wizard.
135
+
136
+ Step mode is the on-ramp. Auto mode is the highway.
135
137
 
136
138
  ---
137
139
 
@@ -170,7 +172,7 @@ gsd
170
172
 
171
173
  GSD opens an interactive agent session. From there, you have two ways to work:
172
174
 
173
- **`/gsd` — guided mode.** Type `/gsd` and GSD reads your project state and walks you through whatever's next. No project yet? It helps you describe what you want to build. Roadmap exists? It plans the next slice. Mid-task? It resumes. This is the hands-on mode where you work *with* the agent step by step.
175
+ **`/gsd` — step mode.** Type `/gsd` and GSD executes one unit of work at a time, pausing between each with a wizard showing what completed and what's next. Same state machine as auto mode, but you stay in the loop. No project yet? It starts the discussion flow. Roadmap exists? It plans or executes the next step.
174
176
 
175
177
  **`/gsd auto` — autonomous mode.** Type `/gsd auto` and walk away. GSD researches, plans, executes, verifies, commits, and advances through every slice until the milestone is complete. Fresh context window per task. No babysitting.
176
178
 
@@ -196,13 +198,14 @@ Both terminals read and write the same `.gsd/` files on disk. Your decisions in
196
198
 
197
199
  ### First launch
198
200
 
199
- On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) for web research and documentation tools. All optional — press Enter to skip any.
201
+ On first run, GSD prompts for optional API keys (Brave Search, Google Gemini, Context7, Jina) for web research and documentation tools. All optional — press Enter to skip any.
200
202
 
201
203
  ### Commands
202
204
 
203
205
  | Command | What it does |
204
206
  |---------|-------------|
205
- | `/gsd` | Guided mode — reads project state, walks you through what's next |
207
+ | `/gsd` | Step mode — executes one unit at a time, pauses between each |
208
+ | `/gsd next` | Explicit step mode (same as bare `/gsd`) |
206
209
  | `/gsd auto` | Autonomous mode — researches, plans, executes, commits, repeats |
207
210
  | `/gsd stop` | Stop auto mode gracefully |
208
211
  | `/gsd discuss` | Discuss architecture and decisions (works alongside auto mode) |
@@ -211,7 +214,13 @@ On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) f
211
214
  | `/gsd prefs` | Model selection, timeouts, budget ceiling |
212
215
  | `/gsd migrate` | Migrate a v1 `.planning` directory to `.gsd` format |
213
216
  | `/gsd doctor` | Validate `.gsd/` integrity, find and fix issues |
217
+ | `/worktree` (`/wt`) | Git worktree lifecycle — create, switch, merge, remove |
218
+ | `/voice` | Toggle real-time speech-to-text (macOS only) |
219
+ | `/exit` | Kill GSD process immediately |
220
+ | `/clear` | Start a new session (alias for `/new`) |
214
221
  | `Ctrl+Alt+G` | Toggle dashboard overlay |
222
+ | `Ctrl+Alt+V` | Toggle voice transcription |
223
+ | `Ctrl+Alt+B` | Show background shell processes |
215
224
 
216
225
  ---
217
226
 
@@ -311,16 +320,20 @@ budget_ceiling: 50.00
311
320
 
312
321
  ### Bundled Tools
313
322
 
314
- GSD ships with 9 extensions, all loaded automatically:
323
+ GSD ships with 13 extensions, all loaded automatically:
315
324
 
316
325
  | Extension | What it provides |
317
326
  |-----------|-----------------|
318
327
  | **GSD** | Core workflow engine, auto mode, commands, dashboard |
319
328
  | **Browser Tools** | Playwright-based browser for UI verification |
320
329
  | **Search the Web** | Brave Search + Jina page extraction |
330
+ | **Google Search** | Gemini-powered web search with AI-synthesized answers |
321
331
  | **Context7** | Up-to-date library/framework documentation |
322
332
  | **Background Shell** | Long-running process management with readiness detection |
323
333
  | **Subagent** | Delegated tasks with isolated context windows |
334
+ | **Mac Tools** | macOS native app automation via Accessibility APIs |
335
+ | **MCPorter** | Lazy on-demand MCP server integration |
336
+ | **Voice** | Real-time speech-to-text transcription (macOS) |
324
337
  | **Slash Commands** | Custom command creation |
325
338
  | **Ask User Questions** | Structured user input with single/multi-select |
326
339
  | **Secure Env Collect** | Masked secret collection without manual .env editing |
@@ -345,12 +358,12 @@ GSD is a TypeScript application that embeds the Pi coding agent SDK.
345
358
  gsd (CLI binary)
346
359
  └─ loader.ts Sets PI_PACKAGE_DIR, GSD env vars, dynamic-imports cli.ts
347
360
  └─ cli.ts Wires SDK managers, loads extensions, starts InteractiveMode
348
- ├─ wizard.ts First-run API key collection (Brave/Context7/Jina)
361
+ ├─ wizard.ts First-run API key collection (Brave/Gemini/Context7/Jina)
349
362
  ├─ app-paths.ts ~/.gsd/agent/, ~/.gsd/sessions/, auth.json
350
363
  ├─ resource-loader.ts Syncs bundled extensions + agents to ~/.gsd/agent/
351
364
  └─ src/resources/
352
365
  ├─ extensions/gsd/ Core GSD extension (auto, state, commands, ...)
353
- ├─ extensions/... 10 supporting extensions
366
+ ├─ extensions/... 12 supporting extensions
354
367
  ├─ agents/ scout, researcher, worker
355
368
  ├─ AGENTS.md Agent routing instructions
356
369
  └─ GSD-WORKFLOW.md Manual bootstrap protocol
@@ -373,6 +386,7 @@ gsd (CLI binary)
373
386
 
374
387
  Optional:
375
388
  - Brave Search API key (web research)
389
+ - Google Gemini API key (web research via Gemini Search grounding)
376
390
  - Context7 API key (library docs)
377
391
  - Jina API key (page extraction)
378
392
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, } from '@mariozechner/pi-coding-agent';
2
- import { readFileSync } from 'node:fs';
2
+ import { existsSync, readdirSync, renameSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
5
5
  import { initResources } from './resource-loader.js';
@@ -144,6 +144,29 @@ if (isPrintMode) {
144
144
  const cwd = process.cwd();
145
145
  const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
146
146
  const projectSessionsDir = join(sessionsDir, safePath);
147
+ // Migrate legacy flat sessions: before per-directory scoping, all .jsonl session
148
+ // files lived directly in ~/.gsd/sessions/. Move them into the correct per-cwd
149
+ // subdirectory so /resume can find them.
150
+ if (existsSync(sessionsDir)) {
151
+ try {
152
+ const entries = readdirSync(sessionsDir);
153
+ const flatJsonl = entries.filter(f => f.endsWith('.jsonl'));
154
+ if (flatJsonl.length > 0) {
155
+ const { mkdirSync } = await import('node:fs');
156
+ mkdirSync(projectSessionsDir, { recursive: true });
157
+ for (const file of flatJsonl) {
158
+ const src = join(sessionsDir, file);
159
+ const dst = join(projectSessionsDir, file);
160
+ if (!existsSync(dst)) {
161
+ renameSync(src, dst);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ catch {
167
+ // Non-fatal — don't block startup if migration fails
168
+ }
169
+ }
147
170
  const sessionManager = SessionManager.create(cwd, projectSessionsDir);
148
171
  initResources(agentDir);
149
172
  const resourceLoader = new DefaultResourceLoader({ agentDir });
package/dist/wizard.js CHANGED
@@ -81,6 +81,8 @@ export function loadStoredEnvKeys(authStorage) {
81
81
  ['brave_answers', 'BRAVE_ANSWERS_KEY'],
82
82
  ['context7', 'CONTEXT7_API_KEY'],
83
83
  ['jina', 'JINA_API_KEY'],
84
+ ['slack_bot', 'SLACK_BOT_TOKEN'],
85
+ ['discord_bot', 'DISCORD_BOT_TOKEN'],
84
86
  ];
85
87
  for (const [provider, envVar] of providers) {
86
88
  if (!process.env[envVar]) {
@@ -120,6 +122,20 @@ const API_KEYS = [
120
122
  hint: '(clean page extraction)',
121
123
  description: 'High-quality web page content extraction',
122
124
  },
125
+ {
126
+ provider: 'slack_bot',
127
+ envVar: 'SLACK_BOT_TOKEN',
128
+ label: 'Slack Bot',
129
+ hint: '(remote questions in auto-mode)',
130
+ description: 'Bot token for remote questions via Slack',
131
+ },
132
+ {
133
+ provider: 'discord_bot',
134
+ envVar: 'DISCORD_BOT_TOKEN',
135
+ label: 'Discord Bot',
136
+ hint: '(remote questions in auto-mode)',
137
+ description: 'Bot token for remote questions via Discord',
138
+ },
123
139
  ];
124
140
  /**
125
141
  * Check for missing optional tool API keys and prompt for them if on a TTY.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.3.5",
3
+ "version": "2.3.7",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,3 +1,63 @@
1
+ diff --git a/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js b/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js
2
+ index 90622c2..cff094b 100644
3
+ --- a/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js
4
+ +++ b/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js
5
+ @@ -1007,7 +1007,7 @@ export class AgentSession {
6
+ * Validates API key, saves to session and settings.
7
+ * @throws Error if no API key available for the model
8
+ */
9
+ - async setModel(model) {
10
+ + async setModel(model, options) {
11
+ const apiKey = await this._modelRegistry.getApiKey(model);
12
+ if (!apiKey) {
13
+ throw new Error(`No API key for ${model.provider}/${model.id}`);
14
+ @@ -1016,7 +1016,9 @@ export class AgentSession {
15
+ const thinkingLevel = this._getThinkingLevelForModelSwitch();
16
+ this.agent.setModel(model);
17
+ this.sessionManager.appendModelChange(model.provider, model.id);
18
+ - this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
19
+ + if (options?.persist !== false) {
20
+ + this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
21
+ + }
22
+ // Re-clamp thinking level for new model's capabilities
23
+ this.setThinkingLevel(thinkingLevel);
24
+ await this._emitModelSelect(model, previousModel, "set");
25
+ @@ -1067,7 +1069,9 @@ export class AgentSession {
26
+ // Apply model
27
+ this.agent.setModel(next.model);
28
+ this.sessionManager.appendModelChange(next.model.provider, next.model.id);
29
+ - this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
30
+ + if (options?.persist !== false) {
31
+ + this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
32
+ + }
33
+ // Apply thinking level.
34
+ // - Explicit scoped model thinking level overrides current session level
35
+ // - Undefined scoped model thinking level inherits the current session preference
36
+ @@ -1094,7 +1098,9 @@ export class AgentSession {
37
+ const thinkingLevel = this._getThinkingLevelForModelSwitch();
38
+ this.agent.setModel(nextModel);
39
+ this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
40
+ - this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
41
+ + if (options?.persist !== false) {
42
+ + this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
43
+ + }
44
+ // Re-clamp thinking level for new model's capabilities
45
+ this.setThinkingLevel(thinkingLevel);
46
+ await this._emitModelSelect(nextModel, currentModel, "cycle");
47
+ @@ -1659,11 +1665,11 @@ export class AgentSession {
48
+ setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
49
+ refreshTools: () => this._refreshToolRegistry(),
50
+ getCommands,
51
+ - setModel: async (model) => {
52
+ + setModel: async (model, options) => {
53
+ const key = await this.modelRegistry.getApiKey(model);
54
+ if (!key)
55
+ return false;
56
+ - await this.setModel(model);
57
+ + await this.setModel(model, options);
58
+ return true;
59
+ },
60
+ getThinkingLevel: () => this.thinkingLevel,
1
61
  diff --git a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js b/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
2
62
  index 27fe820..68f277f 100644
3
63
  --- a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
@@ -44,10 +44,12 @@ try {
44
44
  }
45
45
 
46
46
  // Install Playwright chromium for browser tools (non-fatal)
47
- const args = os.platform() === 'linux' ? '--with-deps' : ''
48
47
  try {
49
- execSync(`npx playwright install chromium ${args}`, { stdio: 'inherit' })
48
+ execSync('npx playwright install chromium', { stdio: 'inherit' })
50
49
  process.stderr.write(`\n ${green}✓${reset} Browser tools ready\n\n`)
51
50
  } catch {
52
- process.stderr.write(`\n ${yellow}⚠${reset} Browser tools unavailable run ${cyan}npx playwright install chromium${reset} to enable\n\n`)
51
+ const hint = os.platform() === 'linux'
52
+ ? `${cyan}npx playwright install --with-deps chromium${reset}`
53
+ : `${cyan}npx playwright install chromium${reset}`
54
+ process.stderr.write(`\n ${yellow}⚠${reset} Browser tools unavailable — run ${hint} to enable\n\n`)
53
55
  }
@@ -21,12 +21,27 @@ import {
21
21
 
22
22
  // ─── Types ────────────────────────────────────────────────────────────────────
23
23
 
24
- interface AskUserQuestionsDetails {
24
+ interface LocalResultDetails {
25
+ remote?: false;
25
26
  questions: Question[];
26
27
  response: RoundResult | null;
27
28
  cancelled: boolean;
28
29
  }
29
30
 
31
+ interface RemoteResultDetails {
32
+ remote: true;
33
+ channel: string;
34
+ timed_out: boolean;
35
+ promptId?: string;
36
+ threadUrl?: string;
37
+ status?: string;
38
+ questions?: Question[];
39
+ response?: import("./remote-questions/types.js").RemoteAnswer;
40
+ error?: boolean;
41
+ }
42
+
43
+ type AskUserQuestionsDetails = LocalResultDetails | RemoteResultDetails;
44
+
30
45
  // ─── Schema ───────────────────────────────────────────────────────────────────
31
46
 
32
47
  const OptionSchema = Type.Object({
@@ -104,7 +119,7 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
104
119
  ],
105
120
  parameters: AskUserQuestionsParams,
106
121
 
107
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
122
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
108
123
  // Validation
109
124
  if (params.questions.length === 0 || params.questions.length > 3) {
110
125
  return errorResult("Error: questions must contain 1-3 items", params.questions);
@@ -120,6 +135,9 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
120
135
  }
121
136
 
122
137
  if (!ctx.hasUI) {
138
+ const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
139
+ const remoteResult = await tryRemoteQuestions(params.questions, signal);
140
+ if (remoteResult) return remoteResult;
123
141
  return errorResult("Error: UI not available (non-interactive mode)", params.questions);
124
142
  }
125
143
 
@@ -131,13 +149,13 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
131
149
  if (!hasAnswers) {
132
150
  return {
133
151
  content: [{ type: "text", text: "ask_user_questions was cancelled before receiving a response" }],
134
- details: { questions: params.questions, response: null, cancelled: true } as AskUserQuestionsDetails,
152
+ details: { questions: params.questions, response: null, cancelled: true } satisfies LocalResultDetails,
135
153
  };
136
154
  }
137
155
 
138
156
  return {
139
157
  content: [{ type: "text", text: formatForLLM(result) }],
140
- details: { questions: params.questions, response: result, cancelled: false } as AskUserQuestionsDetails,
158
+ details: { questions: params.questions, response: result, cancelled: false } satisfies LocalResultDetails,
141
159
  };
142
160
  },
143
161
 
@@ -171,13 +189,44 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
171
189
  return new Text(text?.type === "text" ? text.text : "", 0, 0);
172
190
  }
173
191
 
192
+ // Remote channel result (discriminated on details.remote === true)
193
+ if (details.remote) {
194
+ if (details.timed_out) {
195
+ return new Text(
196
+ `${theme.fg("warning", `${details.channel} — timed out`)}${details.threadUrl ? theme.fg("dim", ` ${details.threadUrl}`) : ""}`,
197
+ 0,
198
+ 0,
199
+ );
200
+ }
201
+
202
+ const questions = (details.questions ?? []) as Question[];
203
+ const lines: string[] = [];
204
+ lines.push(theme.fg("dim", details.channel));
205
+ if (details.response) {
206
+ for (const q of questions) {
207
+ const answer = details.response.answers[q.id];
208
+ if (!answer) {
209
+ lines.push(`${theme.fg("accent", q.header)}: ${theme.fg("dim", "(no answer)")}`);
210
+ continue;
211
+ }
212
+ const answerText = answer.answers.length > 0 ? answer.answers.join(", ") : "(custom)";
213
+ let line = `${theme.fg("success", "✓ ")}${theme.fg("accent", q.header)}: ${answerText}`;
214
+ if (answer.user_note) {
215
+ line += ` ${theme.fg("muted", `[note: ${answer.user_note}]`)}`;
216
+ }
217
+ lines.push(line);
218
+ }
219
+ }
220
+ return new Text(lines.join("\n"), 0, 0);
221
+ }
222
+
174
223
  if (details.cancelled || !details.response) {
175
224
  return new Text(theme.fg("warning", "Cancelled"), 0, 0);
176
225
  }
177
226
 
178
227
  const lines: string[] = [];
179
228
  for (const q of details.questions) {
180
- const answer = details.response.answers[q.id];
229
+ const answer = (details.response as RoundResult).answers[q.id];
181
230
  if (!answer) {
182
231
  lines.push(`${theme.fg("accent", q.header)}: ${theme.fg("dim", "(no answer)")}`);
183
232
  continue;
@@ -247,6 +247,14 @@ export async function startAuto(
247
247
  if (!getLedger()) initMetrics(base);
248
248
  ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
249
249
  ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
250
+ // Rebuild disk state before resuming — user interaction during pause may have changed files
251
+ try { await rebuildState(base); } catch { /* non-fatal */ }
252
+ try {
253
+ const report = await runGSDDoctor(base, { fix: true });
254
+ if (report.fixesApplied.length > 0) {
255
+ ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
256
+ }
257
+ } catch { /* non-fatal */ }
250
258
  await dispatchNextUnit(ctx, pi);
251
259
  return;
252
260
  }
@@ -758,7 +766,12 @@ async function dispatchNextUnit(
758
766
  ctx: ExtensionContext,
759
767
  pi: ExtensionAPI,
760
768
  ): Promise<void> {
761
- if (!active || !cmdCtx) return;
769
+ if (!active || !cmdCtx) {
770
+ if (active && !cmdCtx) {
771
+ ctx.ui.notify("Auto-mode dispatch failed: no command context. Run /gsd auto to restart.", "error");
772
+ }
773
+ return;
774
+ }
762
775
 
763
776
  let state = await deriveState(basePath);
764
777
  let mid = state.activeMilestone?.id;
@@ -1086,7 +1099,7 @@ async function dispatchNextUnit(
1086
1099
  const allModels = ctx.modelRegistry.getAll();
1087
1100
  const model = allModels.find(m => m.id === preferredModelId);
1088
1101
  if (model) {
1089
- const ok = await pi.setModel(model);
1102
+ const ok = await pi.setModel(model, { persist: false });
1090
1103
  if (ok) {
1091
1104
  ctx.ui.notify(`Model: ${preferredModelId}`, "info");
1092
1105
  }
@@ -1186,7 +1199,8 @@ async function dispatchNextUnit(
1186
1199
  await pauseAuto(ctx, pi);
1187
1200
  }, hardTimeoutMs);
1188
1201
 
1189
- // Inject prompt
1202
+ // Inject prompt — verify auto-mode still active (guards against race with timeout/pause)
1203
+ if (!active) return;
1190
1204
  pi.sendMessage(
1191
1205
  { customType: "gsd-auto", content: finalPrompt, display: verbose },
1192
1206
  { triggerTurn: true },
@@ -31,6 +31,7 @@ import {
31
31
  } from "./doctor.js";
32
32
  import { loadPrompt } from "./prompt-loader.js";
33
33
  import { handleMigrate } from "./migrate/command.js";
34
+ import { handleRemote } from "../remote-questions/remote-command.js";
34
35
 
35
36
  function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
36
37
  const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
@@ -52,10 +53,10 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
52
53
 
53
54
  export function registerGSDCommand(pi: ExtensionAPI): void {
54
55
  pi.registerCommand("gsd", {
55
- description: "GSD — Get Shit Done: /gsd next|auto|stop|status|queue|prefs|doctor|migrate",
56
+ description: "GSD — Get Shit Done: /gsd next|auto|stop|status|queue|prefs|doctor|migrate|remote",
56
57
 
57
58
  getArgumentCompletions: (prefix: string) => {
58
- const subcommands = ["next", "auto", "stop", "status", "queue", "discuss", "prefs", "doctor", "migrate"];
59
+ const subcommands = ["next", "auto", "stop", "status", "queue", "discuss", "prefs", "doctor", "migrate", "remote"];
59
60
  const parts = prefix.trim().split(/\s+/);
60
61
 
61
62
  if (parts.length <= 1) {
@@ -78,6 +79,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
78
79
  .map((cmd) => ({ value: `prefs ${cmd}`, label: cmd }));
79
80
  }
80
81
 
82
+ if (parts[0] === "remote" && parts.length <= 2) {
83
+ const subPrefix = parts[1] ?? "";
84
+ return ["slack", "discord", "status", "disconnect"]
85
+ .filter((cmd) => cmd.startsWith(subPrefix))
86
+ .map((cmd) => ({ value: `remote ${cmd}`, label: cmd }));
87
+ }
88
+
81
89
  if (parts[0] === "doctor") {
82
90
  const modePrefix = parts[1] ?? "";
83
91
  const modes = ["fix", "heal", "audit"];
@@ -148,6 +156,11 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
148
156
  return;
149
157
  }
150
158
 
159
+ if (trimmed === "remote" || trimmed.startsWith("remote ")) {
160
+ await handleRemote(trimmed.replace(/^remote\s*/, "").trim(), ctx, pi);
161
+ return;
162
+ }
163
+
151
164
  if (trimmed === "") {
152
165
  // Bare /gsd defaults to step mode
153
166
  await startAuto(ctx, pi, process.cwd(), false, { step: true });
@@ -155,7 +168,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
155
168
  }
156
169
 
157
170
  ctx.ui.notify(
158
- `Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status], /gsd doctor [audit|fix|heal] [M###/S##], or /gsd migrate <path>.`,
171
+ `Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status], /gsd doctor [audit|fix|heal] [M###/S##], /gsd migrate <path>, or /gsd remote [slack|discord|status|disconnect].`,
159
172
  "warning",
160
173
  );
161
174
  },
@@ -102,7 +102,7 @@ export default function (pi: ExtensionAPI) {
102
102
  };
103
103
  pi.registerTool(dynamicBash as any);
104
104
 
105
- // ── session_start: render branded GSD header ───────────────────────────
105
+ // ── session_start: render branded GSD header + remote channel status ──
106
106
  pi.on("session_start", async (_event, ctx) => {
107
107
  const theme = ctx.ui.theme;
108
108
  const version = process.env.GSD_VERSION || "0.0.0";
@@ -112,6 +112,22 @@ export default function (pi: ExtensionAPI) {
112
112
 
113
113
  const headerContent = `${logoText}\n${titleLine}`;
114
114
  ctx.ui.setHeader((_ui, _theme) => new Text(headerContent, 1, 0));
115
+
116
+ // Notify remote questions status if configured
117
+ try {
118
+ const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([
119
+ import("../remote-questions/config.js"),
120
+ import("../remote-questions/status.js"),
121
+ ]);
122
+ const status = getRemoteConfigStatus();
123
+ const latest = getLatestPromptSummary();
124
+ if (!status.includes("not configured")) {
125
+ const suffix = latest ? `\nLast remote prompt: ${latest.id} (${latest.status})` : "";
126
+ ctx.ui.notify(`${status}${suffix}`, status.includes("disabled") ? "warning" : "info");
127
+ }
128
+ } catch {
129
+ // Remote questions module not available — ignore
130
+ }
115
131
  });
116
132
 
117
133
  // ── Ctrl+Alt+G shortcut — GSD dashboard overlay ────────────────────────
@@ -31,6 +31,13 @@ export interface AutoSupervisorConfig {
31
31
  hard_timeout_minutes?: number;
32
32
  }
33
33
 
34
+ export interface RemoteQuestionsConfig {
35
+ channel: "slack" | "discord";
36
+ channel_id: string | number;
37
+ timeout_minutes?: number; // clamped to 1-30
38
+ poll_interval_seconds?: number; // clamped to 2-30
39
+ }
40
+
34
41
  export interface GSDPreferences {
35
42
  version?: number;
36
43
  always_use_skills?: string[];
@@ -43,6 +50,7 @@ export interface GSDPreferences {
43
50
  auto_supervisor?: AutoSupervisorConfig;
44
51
  uat_dispatch?: boolean;
45
52
  budget_ceiling?: number;
53
+ remote_questions?: RemoteQuestionsConfig;
46
54
  }
47
55
 
48
56
  export interface LoadedGSDPreferences {
@@ -430,7 +438,12 @@ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
430
438
  function parseScalar(value: string): string | number | boolean {
431
439
  if (value === "true") return true;
432
440
  if (value === "false") return false;
433
- if (/^-?\d+$/.test(value)) return Number(value);
441
+ if (/^-?\d+$/.test(value)) {
442
+ const n = Number(value);
443
+ // Keep large integers (e.g. Discord channel IDs) as strings to avoid precision loss
444
+ if (Number.isSafeInteger(n)) return n;
445
+ return value;
446
+ }
434
447
  return value.replace(/^['\"]|['\"]$/g, "");
435
448
  }
436
449
 
@@ -495,6 +508,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
495
508
  auto_supervisor: { ...(base.auto_supervisor ?? {}), ...(override.auto_supervisor ?? {}) },
496
509
  uat_dispatch: override.uat_dispatch ?? base.uat_dispatch,
497
510
  budget_ceiling: override.budget_ceiling ?? base.budget_ceiling,
511
+ remote_questions: override.remote_questions
512
+ ? { ...(base.remote_questions ?? {}), ...override.remote_questions }
513
+ : base.remote_questions,
498
514
  };
499
515
  }
500
516