gsd-pi 2.3.6 → 2.3.8
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/dist/wizard.js +16 -0
- package/package.json +1 -1
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +60 -0
- package/src/resources/extensions/ask-user-questions.ts +54 -5
- package/src/resources/extensions/gsd/auto.ts +62 -32
- package/src/resources/extensions/gsd/commands.ts +16 -3
- package/src/resources/extensions/gsd/index.ts +71 -2
- 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/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/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
|
|
@@ -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;
|
|
@@ -60,6 +60,8 @@ import { execSync } from "node:child_process";
|
|
|
60
60
|
import {
|
|
61
61
|
autoCommitCurrentBranch,
|
|
62
62
|
ensureSliceBranch,
|
|
63
|
+
getCurrentBranch,
|
|
64
|
+
getSliceBranchName,
|
|
63
65
|
switchToMain,
|
|
64
66
|
mergeSliceToMain,
|
|
65
67
|
} from "./worktree.ts";
|
|
@@ -247,6 +249,14 @@ export async function startAuto(
|
|
|
247
249
|
if (!getLedger()) initMetrics(base);
|
|
248
250
|
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
249
251
|
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
252
|
+
// Rebuild disk state before resuming — user interaction during pause may have changed files
|
|
253
|
+
try { await rebuildState(base); } catch { /* non-fatal */ }
|
|
254
|
+
try {
|
|
255
|
+
const report = await runGSDDoctor(base, { fix: true });
|
|
256
|
+
if (report.fixesApplied.length > 0) {
|
|
257
|
+
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
|
|
258
|
+
}
|
|
259
|
+
} catch { /* non-fatal */ }
|
|
250
260
|
await dispatchNextUnit(ctx, pi);
|
|
251
261
|
return;
|
|
252
262
|
}
|
|
@@ -758,7 +768,12 @@ async function dispatchNextUnit(
|
|
|
758
768
|
ctx: ExtensionContext,
|
|
759
769
|
pi: ExtensionAPI,
|
|
760
770
|
): Promise<void> {
|
|
761
|
-
if (!active || !cmdCtx)
|
|
771
|
+
if (!active || !cmdCtx) {
|
|
772
|
+
if (active && !cmdCtx) {
|
|
773
|
+
ctx.ui.notify("Auto-mode dispatch failed: no command context. Run /gsd auto to restart.", "error");
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
762
777
|
|
|
763
778
|
let state = await deriveState(basePath);
|
|
764
779
|
let mid = state.activeMilestone?.id;
|
|
@@ -787,39 +802,53 @@ async function dispatchNextUnit(
|
|
|
787
802
|
return;
|
|
788
803
|
}
|
|
789
804
|
|
|
790
|
-
// ──
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
805
|
+
// ── General merge guard: merge completed slice branches before advancing ──
|
|
806
|
+
// If we're on a gsd/MID/SID branch and that slice is done (roadmap [x]),
|
|
807
|
+
// merge to main before dispatching the next unit. This handles:
|
|
808
|
+
// - Normal complete-slice → merge → reassess flow
|
|
809
|
+
// - LLM writes summary during task execution, skipping complete-slice
|
|
810
|
+
// - Doctor post-hook marks everything done, skipping complete-slice
|
|
811
|
+
// - complete-milestone runs on a slice branch (last slice bypass)
|
|
812
|
+
{
|
|
813
|
+
const currentBranch = getCurrentBranch(basePath);
|
|
814
|
+
const branchMatch = currentBranch.match(/^gsd\/(M\d+)\/(S\d+)$/);
|
|
815
|
+
if (branchMatch) {
|
|
816
|
+
const branchMid = branchMatch[1]!;
|
|
817
|
+
const branchSid = branchMatch[2]!;
|
|
818
|
+
// Check if this slice is marked done in the roadmap
|
|
819
|
+
const roadmapFile = resolveMilestoneFile(basePath, branchMid, "ROADMAP");
|
|
798
820
|
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
799
|
-
let sliceTitleForMerge = completedSid!;
|
|
800
821
|
if (roadmapContent) {
|
|
801
822
|
const roadmap = parseRoadmap(roadmapContent);
|
|
802
|
-
const sliceEntry = roadmap.slices.find(s => s.id ===
|
|
803
|
-
if (sliceEntry)
|
|
823
|
+
const sliceEntry = roadmap.slices.find(s => s.id === branchSid);
|
|
824
|
+
if (sliceEntry?.done) {
|
|
825
|
+
try {
|
|
826
|
+
const sliceTitleForMerge = sliceEntry.title || branchSid;
|
|
827
|
+
switchToMain(basePath);
|
|
828
|
+
const mergeResult = mergeSliceToMain(
|
|
829
|
+
basePath, branchMid, branchSid, sliceTitleForMerge,
|
|
830
|
+
);
|
|
831
|
+
ctx.ui.notify(
|
|
832
|
+
`Merged ${mergeResult.branch} → main.`,
|
|
833
|
+
"info",
|
|
834
|
+
);
|
|
835
|
+
// Re-derive state from main so downstream logic sees merged state
|
|
836
|
+
state = await deriveState(basePath);
|
|
837
|
+
mid = state.activeMilestone?.id;
|
|
838
|
+
midTitle = state.activeMilestone?.title;
|
|
839
|
+
} catch (error) {
|
|
840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
841
|
+
ctx.ui.notify(
|
|
842
|
+
`Slice merge failed: ${message}`,
|
|
843
|
+
"error",
|
|
844
|
+
);
|
|
845
|
+
// Re-derive state so dispatch can figure out what to do
|
|
846
|
+
state = await deriveState(basePath);
|
|
847
|
+
mid = state.activeMilestone?.id;
|
|
848
|
+
midTitle = state.activeMilestone?.title;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
804
851
|
}
|
|
805
|
-
switchToMain(basePath);
|
|
806
|
-
const mergeResult = mergeSliceToMain(
|
|
807
|
-
basePath, completedMid!, completedSid!, sliceTitleForMerge,
|
|
808
|
-
);
|
|
809
|
-
ctx.ui.notify(
|
|
810
|
-
`Merged ${mergeResult.branch} → main.`,
|
|
811
|
-
"info",
|
|
812
|
-
);
|
|
813
|
-
} catch (error) {
|
|
814
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
815
|
-
ctx.ui.notify(
|
|
816
|
-
`Slice merge failed: ${message}`,
|
|
817
|
-
"error",
|
|
818
|
-
);
|
|
819
|
-
// Re-derive state so dispatch can figure out what to do
|
|
820
|
-
state = await deriveState(basePath);
|
|
821
|
-
mid = state.activeMilestone?.id;
|
|
822
|
-
midTitle = state.activeMilestone?.title;
|
|
823
852
|
}
|
|
824
853
|
}
|
|
825
854
|
|
|
@@ -1086,7 +1115,7 @@ async function dispatchNextUnit(
|
|
|
1086
1115
|
const allModels = ctx.modelRegistry.getAll();
|
|
1087
1116
|
const model = allModels.find(m => m.id === preferredModelId);
|
|
1088
1117
|
if (model) {
|
|
1089
|
-
const ok = await pi.setModel(model);
|
|
1118
|
+
const ok = await pi.setModel(model, { persist: false });
|
|
1090
1119
|
if (ok) {
|
|
1091
1120
|
ctx.ui.notify(`Model: ${preferredModelId}`, "info");
|
|
1092
1121
|
}
|
|
@@ -1186,7 +1215,8 @@ async function dispatchNextUnit(
|
|
|
1186
1215
|
await pauseAuto(ctx, pi);
|
|
1187
1216
|
}, hardTimeoutMs);
|
|
1188
1217
|
|
|
1189
|
-
// Inject prompt
|
|
1218
|
+
// Inject prompt — verify auto-mode still active (guards against race with timeout/pause)
|
|
1219
|
+
if (!active) return;
|
|
1190
1220
|
pi.sendMessage(
|
|
1191
1221
|
{ customType: "gsd-auto", content: finalPrompt, display: verbose },
|
|
1192
1222
|
{ 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
|
},
|
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
ExtensionAPI,
|
|
23
23
|
ExtensionContext,
|
|
24
24
|
} from "@mariozechner/pi-coding-agent";
|
|
25
|
-
import { createBashTool } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import { createBashTool, createWriteTool, createReadTool, createEditTool } from "@mariozechner/pi-coding-agent";
|
|
26
26
|
|
|
27
27
|
import { registerGSDCommand } from "./commands.js";
|
|
28
28
|
import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
|
|
@@ -102,7 +102,60 @@ export default function (pi: ExtensionAPI) {
|
|
|
102
102
|
};
|
|
103
103
|
pi.registerTool(dynamicBash as any);
|
|
104
104
|
|
|
105
|
-
// ──
|
|
105
|
+
// ── Dynamic-cwd file tools (write, read, edit) ────────────────────────
|
|
106
|
+
// The built-in file tools capture cwd at startup. When process.chdir()
|
|
107
|
+
// moves us into a worktree, relative paths still resolve against the
|
|
108
|
+
// original launch directory. These replacements delegate to freshly-
|
|
109
|
+
// created tools on each call so that process.cwd() is read dynamically.
|
|
110
|
+
const baseWrite = createWriteTool(process.cwd());
|
|
111
|
+
const dynamicWrite = {
|
|
112
|
+
...baseWrite,
|
|
113
|
+
execute: async (
|
|
114
|
+
toolCallId: string,
|
|
115
|
+
params: { path: string; content: string },
|
|
116
|
+
signal?: AbortSignal,
|
|
117
|
+
onUpdate?: any,
|
|
118
|
+
ctx?: any,
|
|
119
|
+
) => {
|
|
120
|
+
const fresh = createWriteTool(process.cwd());
|
|
121
|
+
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
pi.registerTool(dynamicWrite as any);
|
|
125
|
+
|
|
126
|
+
const baseRead = createReadTool(process.cwd());
|
|
127
|
+
const dynamicRead = {
|
|
128
|
+
...baseRead,
|
|
129
|
+
execute: async (
|
|
130
|
+
toolCallId: string,
|
|
131
|
+
params: { path: string; offset?: number; limit?: number },
|
|
132
|
+
signal?: AbortSignal,
|
|
133
|
+
onUpdate?: any,
|
|
134
|
+
ctx?: any,
|
|
135
|
+
) => {
|
|
136
|
+
const fresh = createReadTool(process.cwd());
|
|
137
|
+
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
pi.registerTool(dynamicRead as any);
|
|
141
|
+
|
|
142
|
+
const baseEdit = createEditTool(process.cwd());
|
|
143
|
+
const dynamicEdit = {
|
|
144
|
+
...baseEdit,
|
|
145
|
+
execute: async (
|
|
146
|
+
toolCallId: string,
|
|
147
|
+
params: { path: string; oldText: string; newText: string },
|
|
148
|
+
signal?: AbortSignal,
|
|
149
|
+
onUpdate?: any,
|
|
150
|
+
ctx?: any,
|
|
151
|
+
) => {
|
|
152
|
+
const fresh = createEditTool(process.cwd());
|
|
153
|
+
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
pi.registerTool(dynamicEdit as any);
|
|
157
|
+
|
|
158
|
+
// ── session_start: render branded GSD header + remote channel status ──
|
|
106
159
|
pi.on("session_start", async (_event, ctx) => {
|
|
107
160
|
const theme = ctx.ui.theme;
|
|
108
161
|
const version = process.env.GSD_VERSION || "0.0.0";
|
|
@@ -112,6 +165,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
112
165
|
|
|
113
166
|
const headerContent = `${logoText}\n${titleLine}`;
|
|
114
167
|
ctx.ui.setHeader((_ui, _theme) => new Text(headerContent, 1, 0));
|
|
168
|
+
|
|
169
|
+
// Notify remote questions status if configured
|
|
170
|
+
try {
|
|
171
|
+
const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([
|
|
172
|
+
import("../remote-questions/config.js"),
|
|
173
|
+
import("../remote-questions/status.js"),
|
|
174
|
+
]);
|
|
175
|
+
const status = getRemoteConfigStatus();
|
|
176
|
+
const latest = getLatestPromptSummary();
|
|
177
|
+
if (!status.includes("not configured")) {
|
|
178
|
+
const suffix = latest ? `\nLast remote prompt: ${latest.id} (${latest.status})` : "";
|
|
179
|
+
ctx.ui.notify(`${status}${suffix}`, status.includes("disabled") ? "warning" : "info");
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Remote questions module not available — ignore
|
|
183
|
+
}
|
|
115
184
|
});
|
|
116
185
|
|
|
117
186
|
// ── 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
|
|