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.
- package/README.md +26 -12
- package/dist/cli.js +24 -1
- package/dist/wizard.js +16 -0
- package/package.json +1 -1
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +60 -0
- package/scripts/postinstall.js +5 -3
- package/src/resources/extensions/ask-user-questions.ts +54 -5
- package/src/resources/extensions/gsd/auto.ts +17 -3
- package/src/resources/extensions/gsd/commands.ts +16 -3
- package/src/resources/extensions/gsd/index.ts +17 -1
- package/src/resources/extensions/gsd/preferences.ts +17 -1
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
- package/src/resources/extensions/gsd/worktree.ts +11 -0
- package/src/resources/extensions/remote-questions/config.ts +81 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
- package/src/resources/extensions/remote-questions/format.ts +163 -0
- package/src/resources/extensions/remote-questions/manager.ts +192 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
- package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
- package/src/resources/extensions/remote-questions/status.ts +31 -0
- package/src/resources/extensions/remote-questions/store.ts +77 -0
- package/src/resources/extensions/remote-questions/types.ts +75 -0
- package/src/resources/extensions/github/formatters.ts +0 -207
- package/src/resources/extensions/github/gh-api.ts +0 -553
- 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/
|
|
5
|
+
**The evolution of [Get Shit Done](https://github.com/gsd-build/get-shit-done) — now a real coding agent.**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/gsd-pi)
|
|
8
8
|
[](https://www.npmjs.com/package/gsd-pi)
|
|
9
|
-
[](https://github.com/gsd-build/GSD-2)
|
|
10
10
|
[](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
|
-
###
|
|
125
|
+
### `/gsd` and `/gsd next` — Step Mode
|
|
126
126
|
|
|
127
|
-
|
|
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
|
|
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
|
-
|
|
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` —
|
|
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` |
|
|
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
|
|
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/...
|
|
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,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
|
package/scripts/postinstall.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 }
|
|
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 }
|
|
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)
|
|
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##],
|
|
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))
|
|
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
|
|