gsd-pi 0.3.1 → 0.3.3
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/loader.js +0 -0
- package/dist/resource-loader.d.ts +3 -3
- package/dist/resource-loader.js +10 -4
- package/package.json +3 -1
- package/src/resources/extensions/browser-tools/index.ts +76 -6
- package/src/resources/extensions/gsd/auto.ts +188 -10
- package/src/resources/extensions/gsd/commands.ts +13 -6
- package/src/resources/extensions/gsd/doctor.ts +7 -0
- package/src/resources/extensions/gsd/guided-flow.ts +8 -5
- package/src/resources/extensions/gsd/index.ts +8 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +70 -26
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +51 -17
- package/src/resources/extensions/gsd/worktree-command.ts +219 -49
- package/src/resources/extensions/gsd/worktree-manager.ts +106 -16
- package/src/resources/extensions/mcporter/index.ts +410 -0
- package/src/resources/extensions/slash-commands/clear.ts +10 -0
- package/src/resources/extensions/slash-commands/index.ts +2 -2
- package/src/resources/extensions/voice/index.ts +176 -0
- package/src/resources/extensions/voice/speech-recognizer +0 -0
- package/src/resources/extensions/voice/speech-recognizer.swift +76 -0
- package/dist/modes/interactive/theme/dark.json +0 -85
- package/dist/modes/interactive/theme/light.json +0 -84
- package/dist/modes/interactive/theme/theme-schema.json +0 -335
- package/dist/modes/interactive/theme/theme.d.ts +0 -78
- package/dist/modes/interactive/theme/theme.d.ts.map +0 -1
- package/dist/modes/interactive/theme/theme.js +0 -949
- package/dist/modes/interactive/theme/theme.js.map +0 -1
- package/src/resources/extensions/slash-commands/gsd-run.ts +0 -34
package/dist/loader.js
CHANGED
|
File without changes
|
|
@@ -15,8 +15,8 @@ import { DefaultResourceLoader } from '@mariozechner/pi-coding-agent';
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function initResources(agentDir: string): void;
|
|
17
17
|
/**
|
|
18
|
-
* Constructs a DefaultResourceLoader
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Constructs a DefaultResourceLoader that loads extensions from both
|
|
19
|
+
* ~/.gsd/agent/extensions/ (GSD's default) and ~/.pi/agent/extensions/ (pi's default).
|
|
20
|
+
* This allows users to use extensions from either location.
|
|
21
21
|
*/
|
|
22
22
|
export declare function buildResourceLoader(agentDir: string): DefaultResourceLoader;
|
package/dist/resource-loader.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DefaultResourceLoader } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, join, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -45,10 +46,15 @@ export function initResources(agentDir) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
48
|
-
* Constructs a DefaultResourceLoader
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* Constructs a DefaultResourceLoader that loads extensions from both
|
|
50
|
+
* ~/.gsd/agent/extensions/ (GSD's default) and ~/.pi/agent/extensions/ (pi's default).
|
|
51
|
+
* This allows users to use extensions from either location.
|
|
51
52
|
*/
|
|
52
53
|
export function buildResourceLoader(agentDir) {
|
|
53
|
-
|
|
54
|
+
const piAgentDir = join(homedir(), '.pi', 'agent');
|
|
55
|
+
const piExtensionsDir = join(piAgentDir, 'extensions');
|
|
56
|
+
return new DefaultResourceLoader({
|
|
57
|
+
agentDir,
|
|
58
|
+
additionalExtensionPaths: [piExtensionsDir],
|
|
59
|
+
});
|
|
54
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-pi",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"test": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test 'src/resources/extensions/gsd/tests/*.test.ts' 'src/resources/extensions/gsd/tests/*.test.mjs' 'src/tests/*.test.ts'",
|
|
39
39
|
"dev": "tsc --watch",
|
|
40
40
|
"postinstall": "node scripts/postinstall.js",
|
|
41
|
+
"pi:install-global": "node scripts/install-pi-global.js",
|
|
42
|
+
"pi:uninstall-global": "node scripts/uninstall-pi-global.js",
|
|
41
43
|
"sync-pkg-version": "node scripts/sync-pkg-version.cjs",
|
|
42
44
|
"prepublishOnly": "npm run sync-pkg-version && npm run build"
|
|
43
45
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Every action returns feedback (accessibility snapshot, screenshots on navigate)
|
|
9
9
|
* - Errors include visual debugging (screenshots on failure, surfaced JS errors)
|
|
10
10
|
* - Smart waits (domcontentloaded + best-effort settle, not blocking networkidle)
|
|
11
|
-
* -
|
|
11
|
+
* - Screenshots capped at 1568px max dimension (Anthropic API limit safety)
|
|
12
12
|
* - JPEG for viewport screenshots (smaller), PNG for element crops (transparency)
|
|
13
13
|
* - Auto-handles JS dialogs (alert/confirm/prompt) to prevent page freezes
|
|
14
14
|
* - Auto-switches to new tabs (popups, target="_blank")
|
|
@@ -731,11 +731,75 @@ async function postActionSummary(p: Page, target?: Page | Frame): Promise<string
|
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
+
// Anthropic API rejects images > 2000px in multi-image requests.
|
|
735
|
+
// Cap at 1568px (recommended optimal size) to stay well within limits.
|
|
736
|
+
const MAX_SCREENSHOT_DIM = 1568;
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* If either dimension of the image buffer exceeds MAX_SCREENSHOT_DIM,
|
|
740
|
+
* downscale proportionally using the browser's canvas (zero dependencies).
|
|
741
|
+
* Returns the original buffer unchanged if already within limits.
|
|
742
|
+
*/
|
|
743
|
+
async function constrainScreenshot(
|
|
744
|
+
page: Page,
|
|
745
|
+
buffer: Buffer,
|
|
746
|
+
mimeType: string,
|
|
747
|
+
quality: number,
|
|
748
|
+
): Promise<Buffer> {
|
|
749
|
+
let width: number;
|
|
750
|
+
let height: number;
|
|
751
|
+
|
|
752
|
+
if (mimeType === "image/png") {
|
|
753
|
+
width = buffer.readUInt32BE(16);
|
|
754
|
+
height = buffer.readUInt32BE(20);
|
|
755
|
+
} else {
|
|
756
|
+
width = 0;
|
|
757
|
+
height = 0;
|
|
758
|
+
for (let i = 0; i < buffer.length - 8; i++) {
|
|
759
|
+
if (buffer[i] === 0xff && (buffer[i + 1] === 0xc0 || buffer[i + 1] === 0xc2)) {
|
|
760
|
+
height = buffer.readUInt16BE(i + 5);
|
|
761
|
+
width = buffer.readUInt16BE(i + 7);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (width <= MAX_SCREENSHOT_DIM && height <= MAX_SCREENSHOT_DIM) {
|
|
768
|
+
return buffer;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const b64 = buffer.toString("base64");
|
|
772
|
+
const result = await page.evaluate(
|
|
773
|
+
async ({ b64, mime, maxDim, q }) => {
|
|
774
|
+
const img = new Image();
|
|
775
|
+
await new Promise<void>((resolve, reject) => {
|
|
776
|
+
img.onload = () => resolve();
|
|
777
|
+
img.onerror = reject;
|
|
778
|
+
img.src = `data:${mime};base64,${b64}`;
|
|
779
|
+
});
|
|
780
|
+
const scale = Math.min(maxDim / img.width, maxDim / img.height);
|
|
781
|
+
const w = Math.round(img.width * scale);
|
|
782
|
+
const h = Math.round(img.height * scale);
|
|
783
|
+
const canvas = document.createElement("canvas");
|
|
784
|
+
canvas.width = w;
|
|
785
|
+
canvas.height = h;
|
|
786
|
+
const ctx = canvas.getContext("2d")!;
|
|
787
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
788
|
+
return canvas.toDataURL(mime, q / 100);
|
|
789
|
+
},
|
|
790
|
+
{ b64, mime: mimeType, maxDim: MAX_SCREENSHOT_DIM, q: quality },
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
const resizedB64 = result.split(",")[1];
|
|
794
|
+
return Buffer.from(resizedB64, "base64");
|
|
795
|
+
}
|
|
796
|
+
|
|
734
797
|
/** Capture a JPEG screenshot for error debugging. Returns base64 or null. */
|
|
735
798
|
async function captureErrorScreenshot(p: Page | null): Promise<{ data: string; mimeType: string } | null> {
|
|
736
799
|
if (!p) return null;
|
|
737
800
|
try {
|
|
738
|
-
|
|
801
|
+
let buf = await p.screenshot({ type: "jpeg", quality: 60, scale: "css" });
|
|
802
|
+
buf = await constrainScreenshot(p, buf, "image/jpeg", 60);
|
|
739
803
|
return { data: buf.toString("base64"), mimeType: "image/jpeg" };
|
|
740
804
|
} catch {
|
|
741
805
|
return null;
|
|
@@ -1602,7 +1666,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1602
1666
|
|
|
1603
1667
|
let screenshotContent: any[] = [];
|
|
1604
1668
|
try {
|
|
1605
|
-
|
|
1669
|
+
let buf = await p.screenshot({ type: "jpeg", quality: 80, scale: "css" });
|
|
1670
|
+
buf = await constrainScreenshot(p, buf, "image/jpeg", 80);
|
|
1606
1671
|
screenshotContent = [{ type: "image", data: buf.toString("base64"), mimeType: "image/jpeg" }];
|
|
1607
1672
|
} catch {}
|
|
1608
1673
|
|
|
@@ -1744,7 +1809,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1744
1809
|
// Include screenshot like navigate does
|
|
1745
1810
|
let screenshotContent: any[] = [];
|
|
1746
1811
|
try {
|
|
1747
|
-
|
|
1812
|
+
let buf = await p.screenshot({ type: "jpeg", quality: 80, scale: "css" });
|
|
1813
|
+
buf = await constrainScreenshot(p, buf, "image/jpeg", 80);
|
|
1748
1814
|
screenshotContent = [{
|
|
1749
1815
|
type: "image",
|
|
1750
1816
|
data: buf.toString("base64"),
|
|
@@ -1805,23 +1871,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
1805
1871
|
|
|
1806
1872
|
let screenshotBuffer: Buffer;
|
|
1807
1873
|
let mimeType: string;
|
|
1874
|
+
const quality = params.quality ?? 80;
|
|
1808
1875
|
|
|
1809
1876
|
if (params.selector) {
|
|
1810
1877
|
// Element screenshots: keep PNG (may have transparency)
|
|
1811
1878
|
const locator = p.locator(params.selector).first();
|
|
1812
|
-
screenshotBuffer = await locator.screenshot({ type: "png" });
|
|
1879
|
+
screenshotBuffer = await locator.screenshot({ type: "png", scale: "css" });
|
|
1813
1880
|
mimeType = "image/png";
|
|
1814
1881
|
} else {
|
|
1815
1882
|
// Viewport/fullpage: use JPEG (3-5x smaller, fine for AI analysis)
|
|
1816
|
-
const quality = params.quality ?? 80;
|
|
1817
1883
|
screenshotBuffer = await p.screenshot({
|
|
1818
1884
|
fullPage: params.fullPage ?? false,
|
|
1819
1885
|
type: "jpeg",
|
|
1820
1886
|
quality,
|
|
1887
|
+
scale: "css",
|
|
1821
1888
|
});
|
|
1822
1889
|
mimeType = "image/jpeg";
|
|
1823
1890
|
}
|
|
1824
1891
|
|
|
1892
|
+
// Downscale if dimensions exceed API limit (1568px max)
|
|
1893
|
+
screenshotBuffer = await constrainScreenshot(p, screenshotBuffer, mimeType, quality);
|
|
1894
|
+
|
|
1825
1895
|
const base64Data = screenshotBuffer.toString("base64");
|
|
1826
1896
|
const title = await p.title();
|
|
1827
1897
|
const url = p.url();
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
|
|
19
19
|
import { deriveState } from "./state.js";
|
|
20
20
|
import type { GSDState } from "./types.js";
|
|
21
|
-
import { loadFile, parseContinue, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary } from "./files.js";
|
|
21
|
+
import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary } from "./files.js";
|
|
22
22
|
export { inlinePriorMilestoneSummary };
|
|
23
23
|
import type { UatType } from "./files.js";
|
|
24
24
|
import { loadPrompt } from "./prompt-loader.js";
|
|
@@ -36,7 +36,6 @@ import {
|
|
|
36
36
|
clearUnitRuntimeRecord,
|
|
37
37
|
formatExecuteTaskRecoveryStatus,
|
|
38
38
|
inspectExecuteTaskDurability,
|
|
39
|
-
recordUnitProgress,
|
|
40
39
|
readUnitRuntimeRecord,
|
|
41
40
|
writeUnitRuntimeRecord,
|
|
42
41
|
} from "./unit-runtime.js";
|
|
@@ -49,6 +48,7 @@ import {
|
|
|
49
48
|
formatValidationIssues,
|
|
50
49
|
} from "./observability-validator.js";
|
|
51
50
|
import { ensureGitignore } from "./gitignore.js";
|
|
51
|
+
import { runGSDDoctor, rebuildState } from "./doctor.js";
|
|
52
52
|
import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
|
|
53
53
|
import {
|
|
54
54
|
initMetrics, resetMetrics, snapshotUnitMetrics, getLedger,
|
|
@@ -65,11 +65,13 @@ import {
|
|
|
65
65
|
} from "./worktree.ts";
|
|
66
66
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
67
67
|
import { makeUI, GLYPH, INDENT } from "../shared/ui.js";
|
|
68
|
+
import { showNextAction } from "../shared/next-action-ui.js";
|
|
68
69
|
|
|
69
70
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
70
71
|
|
|
71
72
|
let active = false;
|
|
72
73
|
let paused = false;
|
|
74
|
+
let stepMode = false;
|
|
73
75
|
let verbose = false;
|
|
74
76
|
let cmdCtx: ExtensionCommandContext | null = null;
|
|
75
77
|
let basePath = "";
|
|
@@ -102,6 +104,7 @@ let idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
|
|
|
102
104
|
export interface AutoDashboardData {
|
|
103
105
|
active: boolean;
|
|
104
106
|
paused: boolean;
|
|
107
|
+
stepMode: boolean;
|
|
105
108
|
startTime: number;
|
|
106
109
|
elapsed: number;
|
|
107
110
|
currentUnit: { type: string; id: string; startedAt: number } | null;
|
|
@@ -118,6 +121,7 @@ export function getAutoDashboardData(): AutoDashboardData {
|
|
|
118
121
|
return {
|
|
119
122
|
active,
|
|
120
123
|
paused,
|
|
124
|
+
stepMode,
|
|
121
125
|
startTime: autoStartTime,
|
|
122
126
|
elapsed: (active || paused) ? Date.now() - autoStartTime : 0,
|
|
123
127
|
currentUnit: currentUnit ? { ...currentUnit } : null,
|
|
@@ -138,6 +142,10 @@ export function isAutoPaused(): boolean {
|
|
|
138
142
|
return paused;
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
export function isStepMode(): boolean {
|
|
146
|
+
return stepMode;
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
function clearUnitTimeout(): void {
|
|
142
150
|
if (unitTimeoutHandle) {
|
|
143
151
|
clearTimeout(unitTimeoutHandle);
|
|
@@ -174,6 +182,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
174
182
|
resetMetrics();
|
|
175
183
|
active = false;
|
|
176
184
|
paused = false;
|
|
185
|
+
stepMode = false;
|
|
177
186
|
lastUnit = null;
|
|
178
187
|
currentUnit = null;
|
|
179
188
|
currentMilestoneId = null;
|
|
@@ -208,8 +217,9 @@ export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Pro
|
|
|
208
217
|
// — all needed for resume and dashboard display
|
|
209
218
|
ctx?.ui.setStatus("gsd-auto", "paused");
|
|
210
219
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
220
|
+
const resumeCmd = stepMode ? "/gsd next" : "/gsd auto";
|
|
211
221
|
ctx?.ui.notify(
|
|
212
|
-
"Auto-mode paused (Escape). Type to interact, or
|
|
222
|
+
`${stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`,
|
|
213
223
|
"info",
|
|
214
224
|
);
|
|
215
225
|
}
|
|
@@ -219,19 +229,24 @@ export async function startAuto(
|
|
|
219
229
|
pi: ExtensionAPI,
|
|
220
230
|
base: string,
|
|
221
231
|
verboseMode: boolean,
|
|
232
|
+
options?: { step?: boolean },
|
|
222
233
|
): Promise<void> {
|
|
234
|
+
const requestedStepMode = options?.step ?? false;
|
|
235
|
+
|
|
223
236
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
224
237
|
// The conversation is still intact — no need to reinitialize everything.
|
|
225
238
|
if (paused) {
|
|
226
239
|
paused = false;
|
|
227
240
|
active = true;
|
|
228
241
|
verbose = verboseMode;
|
|
242
|
+
// Allow switching between step/auto on resume
|
|
243
|
+
stepMode = requestedStepMode;
|
|
229
244
|
cmdCtx = ctx;
|
|
230
245
|
basePath = base;
|
|
231
246
|
// Re-initialize metrics in case ledger was lost during pause
|
|
232
247
|
if (!getLedger()) initMetrics(base);
|
|
233
|
-
ctx.ui.setStatus("gsd-auto", "auto");
|
|
234
|
-
ctx.ui.notify("Auto-mode resumed.", "info");
|
|
248
|
+
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
249
|
+
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
235
250
|
await dispatchNextUnit(ctx, pi);
|
|
236
251
|
return;
|
|
237
252
|
}
|
|
@@ -287,7 +302,7 @@ export async function startAuto(
|
|
|
287
302
|
// No active work at all — start a new milestone via the discuss flow.
|
|
288
303
|
if (!state.activeMilestone || state.phase === "complete") {
|
|
289
304
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
290
|
-
await showSmartEntry(ctx, pi, base);
|
|
305
|
+
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
291
306
|
return;
|
|
292
307
|
}
|
|
293
308
|
|
|
@@ -299,13 +314,14 @@ export async function startAuto(
|
|
|
299
314
|
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
300
315
|
if (!hasContext) {
|
|
301
316
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
302
|
-
await showSmartEntry(ctx, pi, base);
|
|
317
|
+
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
303
318
|
return;
|
|
304
319
|
}
|
|
305
320
|
// Has context, no roadmap — auto-mode will research + plan it
|
|
306
321
|
}
|
|
307
322
|
|
|
308
323
|
active = true;
|
|
324
|
+
stepMode = requestedStepMode;
|
|
309
325
|
verbose = verboseMode;
|
|
310
326
|
cmdCtx = ctx;
|
|
311
327
|
basePath = base;
|
|
@@ -325,12 +341,13 @@ export async function startAuto(
|
|
|
325
341
|
snapshotSkills();
|
|
326
342
|
}
|
|
327
343
|
|
|
328
|
-
ctx.ui.setStatus("gsd-auto", "auto");
|
|
344
|
+
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
345
|
+
const modeLabel = stepMode ? "Step-mode" : "Auto-mode";
|
|
329
346
|
const pendingCount = state.registry.filter(m => m.status !== 'complete').length;
|
|
330
347
|
const scopeMsg = pendingCount > 1
|
|
331
348
|
? `Will loop through ${pendingCount} milestones.`
|
|
332
349
|
: "Will loop until milestone complete.";
|
|
333
|
-
ctx.ui.notify(
|
|
350
|
+
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
334
351
|
|
|
335
352
|
// Dispatch the first unit
|
|
336
353
|
await dispatchNextUnit(ctx, pi);
|
|
@@ -360,11 +377,141 @@ export async function handleAgentEnd(
|
|
|
360
377
|
} catch {
|
|
361
378
|
// Non-fatal
|
|
362
379
|
}
|
|
380
|
+
|
|
381
|
+
// Post-hook: fix mechanical bookkeeping the LLM may have skipped.
|
|
382
|
+
// 1. Doctor handles: checkbox marking, stub summaries/UATs.
|
|
383
|
+
// 2. STATE.md is always rebuilt from disk state (purely derived, no LLM needed).
|
|
384
|
+
// This is more reliable than prompt instructions for mechanical tasks.
|
|
385
|
+
// Scope to slice level (M001/S01) so doctor checks all tasks within the slice.
|
|
386
|
+
try {
|
|
387
|
+
const scopeParts = currentUnit.id.split("/").slice(0, 2);
|
|
388
|
+
const doctorScope = scopeParts.join("/");
|
|
389
|
+
const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope });
|
|
390
|
+
if (report.fixesApplied.length > 0) {
|
|
391
|
+
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
// Non-fatal — doctor failure should never block dispatch
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
await rebuildState(basePath);
|
|
398
|
+
autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id);
|
|
399
|
+
} catch {
|
|
400
|
+
// Non-fatal
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// In step mode, pause and show a wizard instead of immediately dispatching
|
|
405
|
+
if (stepMode) {
|
|
406
|
+
await showStepWizard(ctx, pi);
|
|
407
|
+
return;
|
|
363
408
|
}
|
|
364
409
|
|
|
365
410
|
await dispatchNextUnit(ctx, pi);
|
|
366
411
|
}
|
|
367
412
|
|
|
413
|
+
// ─── Step Mode Wizard ─────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Show the step-mode wizard after a unit completes.
|
|
417
|
+
* Derives the next unit from disk state and presents it to the user.
|
|
418
|
+
* If the user confirms, dispatches the next unit. If not, pauses.
|
|
419
|
+
*/
|
|
420
|
+
async function showStepWizard(
|
|
421
|
+
ctx: ExtensionContext,
|
|
422
|
+
pi: ExtensionAPI,
|
|
423
|
+
): Promise<void> {
|
|
424
|
+
if (!cmdCtx) return;
|
|
425
|
+
|
|
426
|
+
const state = await deriveState(basePath);
|
|
427
|
+
const mid = state.activeMilestone?.id;
|
|
428
|
+
|
|
429
|
+
// Build summary of what just completed
|
|
430
|
+
const justFinished = currentUnit
|
|
431
|
+
? `${unitVerb(currentUnit.type)} ${currentUnit.id}`
|
|
432
|
+
: "previous unit";
|
|
433
|
+
|
|
434
|
+
// If no active milestone or everything is complete, stop
|
|
435
|
+
if (!mid || state.phase === "complete") {
|
|
436
|
+
await stopAuto(ctx, pi);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Peek at what's next by examining state
|
|
441
|
+
const nextDesc = describeNextUnit(state);
|
|
442
|
+
|
|
443
|
+
const choice = await showNextAction(cmdCtx, {
|
|
444
|
+
title: `GSD — ${justFinished} complete`,
|
|
445
|
+
summary: [
|
|
446
|
+
`${mid}: ${state.activeMilestone?.title ?? mid}`,
|
|
447
|
+
...(state.activeSlice ? [`${state.activeSlice.id}: ${state.activeSlice.title}`] : []),
|
|
448
|
+
],
|
|
449
|
+
actions: [
|
|
450
|
+
{
|
|
451
|
+
id: "continue",
|
|
452
|
+
label: nextDesc.label,
|
|
453
|
+
description: nextDesc.description,
|
|
454
|
+
recommended: true,
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
id: "auto",
|
|
458
|
+
label: "Switch to auto",
|
|
459
|
+
description: "Continue without pausing between steps.",
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "status",
|
|
463
|
+
label: "View status",
|
|
464
|
+
description: "Open the dashboard.",
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
notYetMessage: "Run /gsd next when ready to continue.",
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
if (choice === "continue") {
|
|
471
|
+
await dispatchNextUnit(ctx, pi);
|
|
472
|
+
} else if (choice === "auto") {
|
|
473
|
+
stepMode = false;
|
|
474
|
+
ctx.ui.setStatus("gsd-auto", "auto");
|
|
475
|
+
ctx.ui.notify("Switched to auto-mode.", "info");
|
|
476
|
+
await dispatchNextUnit(ctx, pi);
|
|
477
|
+
} else if (choice === "status") {
|
|
478
|
+
// Show status then re-show the wizard
|
|
479
|
+
const { fireStatusViaCommand } = await import("./commands.js");
|
|
480
|
+
await fireStatusViaCommand(ctx as ExtensionCommandContext);
|
|
481
|
+
await showStepWizard(ctx, pi);
|
|
482
|
+
} else {
|
|
483
|
+
// "not_yet" — pause
|
|
484
|
+
await pauseAuto(ctx, pi);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Describe what the next unit will be, based on current state.
|
|
490
|
+
*/
|
|
491
|
+
function describeNextUnit(state: GSDState): { label: string; description: string } {
|
|
492
|
+
const sid = state.activeSlice?.id;
|
|
493
|
+
const sTitle = state.activeSlice?.title;
|
|
494
|
+
const tid = state.activeTask?.id;
|
|
495
|
+
const tTitle = state.activeTask?.title;
|
|
496
|
+
|
|
497
|
+
switch (state.phase) {
|
|
498
|
+
case "pre-planning":
|
|
499
|
+
return { label: "Research & plan milestone", description: "Scout the landscape and create the roadmap." };
|
|
500
|
+
case "planning":
|
|
501
|
+
return { label: `Plan ${sid}: ${sTitle}`, description: "Research and decompose into tasks." };
|
|
502
|
+
case "executing":
|
|
503
|
+
return { label: `Execute ${tid}: ${tTitle}`, description: "Run the next task in a fresh session." };
|
|
504
|
+
case "summarizing":
|
|
505
|
+
return { label: `Complete ${sid}: ${sTitle}`, description: "Write summary, UAT, and merge to main." };
|
|
506
|
+
case "replanning-slice":
|
|
507
|
+
return { label: `Replan ${sid}: ${sTitle}`, description: "Blocker found — replan the slice." };
|
|
508
|
+
case "completing-milestone":
|
|
509
|
+
return { label: "Complete milestone", description: "Write milestone summary." };
|
|
510
|
+
default:
|
|
511
|
+
return { label: "Continue", description: "Execute the next step." };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
368
515
|
// ─── Progress Widget ──────────────────────────────────────────────────────
|
|
369
516
|
|
|
370
517
|
function unitVerb(unitType: string): string {
|
|
@@ -465,7 +612,8 @@ function updateProgressWidget(
|
|
|
465
612
|
? theme.fg("accent", GLYPH.statusActive)
|
|
466
613
|
: theme.fg("dim", GLYPH.statusPending);
|
|
467
614
|
const elapsed = formatAutoElapsed();
|
|
468
|
-
const
|
|
615
|
+
const modeTag = stepMode ? "NEXT" : "AUTO";
|
|
616
|
+
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
|
|
469
617
|
const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
|
|
470
618
|
lines.push(rightAlign(headerLeft, headerRight, width));
|
|
471
619
|
|
|
@@ -985,6 +1133,17 @@ async function dispatchNextUnit(
|
|
|
985
1133
|
if (!runtime) return;
|
|
986
1134
|
if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
|
|
987
1135
|
|
|
1136
|
+
// Before triggering recovery, check if the agent is actually producing
|
|
1137
|
+
// work on disk. `git status --porcelain` is cheap and catches any
|
|
1138
|
+
// staged/unstaged/untracked changes the agent made since lastProgressAt.
|
|
1139
|
+
if (detectWorkingTreeActivity(basePath)) {
|
|
1140
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
1141
|
+
lastProgressAt: Date.now(),
|
|
1142
|
+
lastProgressKind: "filesystem-activity",
|
|
1143
|
+
});
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
988
1147
|
if (currentUnit) {
|
|
989
1148
|
const modelId = ctx.model?.id ?? "unknown";
|
|
990
1149
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
@@ -2136,6 +2295,25 @@ export function skipExecuteTask(
|
|
|
2136
2295
|
return true;
|
|
2137
2296
|
}
|
|
2138
2297
|
|
|
2298
|
+
/**
|
|
2299
|
+
* Detect whether the agent is producing work on disk by checking git for
|
|
2300
|
+
* any working-tree changes (staged, unstaged, or untracked). Returns true
|
|
2301
|
+
* if there are uncommitted changes — meaning the agent is actively working,
|
|
2302
|
+
* even though it hasn't signaled progress through runtime records.
|
|
2303
|
+
*/
|
|
2304
|
+
function detectWorkingTreeActivity(cwd: string): boolean {
|
|
2305
|
+
try {
|
|
2306
|
+
const out = execSync("git status --porcelain", {
|
|
2307
|
+
cwd,
|
|
2308
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2309
|
+
timeout: 5000,
|
|
2310
|
+
});
|
|
2311
|
+
return out.toString().trim().length > 0;
|
|
2312
|
+
} catch {
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2139
2317
|
/**
|
|
2140
2318
|
* Resolve the expected artifact for a non-execute-task unit to an absolute path.
|
|
2141
2319
|
* Returns null for unit types that don't produce a single file (execute-task,
|
|
@@ -10,8 +10,8 @@ import { join, dirname } from "node:path";
|
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { deriveState } from "./state.js";
|
|
12
12
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
13
|
-
import {
|
|
14
|
-
import { startAuto, stopAuto, isAutoActive, isAutoPaused } from "./auto.js";
|
|
13
|
+
import { showQueue, showDiscuss } from "./guided-flow.js";
|
|
14
|
+
import { startAuto, stopAuto, isAutoActive, isAutoPaused, isStepMode } from "./auto.js";
|
|
15
15
|
import {
|
|
16
16
|
getGlobalGSDPreferencesPath,
|
|
17
17
|
getLegacyGlobalGSDPreferencesPath,
|
|
@@ -52,10 +52,10 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
|
|
|
52
52
|
|
|
53
53
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
54
54
|
pi.registerCommand("gsd", {
|
|
55
|
-
description: "GSD — Get Shit Done: /gsd auto|stop|status|queue|prefs|doctor|migrate",
|
|
55
|
+
description: "GSD — Get Shit Done: /gsd next|auto|stop|status|queue|prefs|doctor|migrate",
|
|
56
56
|
|
|
57
57
|
getArgumentCompletions: (prefix: string) => {
|
|
58
|
-
const subcommands = ["auto", "stop", "status", "queue", "discuss", "prefs", "doctor", "migrate"];
|
|
58
|
+
const subcommands = ["next", "auto", "stop", "status", "queue", "discuss", "prefs", "doctor", "migrate"];
|
|
59
59
|
const parts = prefix.trim().split(/\s+/);
|
|
60
60
|
|
|
61
61
|
if (parts.length <= 1) {
|
|
@@ -112,6 +112,12 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
if (trimmed === "next" || trimmed.startsWith("next ")) {
|
|
116
|
+
const verboseMode = trimmed.includes("--verbose");
|
|
117
|
+
await startAuto(ctx, pi, process.cwd(), verboseMode, { step: true });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
if (trimmed === "auto" || trimmed.startsWith("auto ")) {
|
|
116
122
|
const verboseMode = trimmed.includes("--verbose");
|
|
117
123
|
await startAuto(ctx, pi, process.cwd(), verboseMode);
|
|
@@ -143,12 +149,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
if (trimmed === "") {
|
|
146
|
-
|
|
152
|
+
// Bare /gsd defaults to step mode
|
|
153
|
+
await startAuto(ctx, pi, process.cwd(), false, { step: true });
|
|
147
154
|
return;
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
ctx.ui.notify(
|
|
151
|
-
`Unknown: /gsd ${trimmed}. Use /gsd, /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>.`,
|
|
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>.`,
|
|
152
159
|
"warning",
|
|
153
160
|
);
|
|
154
161
|
},
|
|
@@ -147,6 +147,13 @@ async function updateStateFile(basePath: string, fixesApplied: string[]): Promis
|
|
|
147
147
|
fixesApplied.push(`updated ${path}`);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/** Rebuild STATE.md from current disk state. Exported for auto-mode post-hooks. */
|
|
151
|
+
export async function rebuildState(basePath: string): Promise<void> {
|
|
152
|
+
const state = await deriveState(basePath);
|
|
153
|
+
const path = resolveGsdRootFile(basePath, "STATE");
|
|
154
|
+
await saveFile(path, buildStateMarkdown(state));
|
|
155
|
+
}
|
|
156
|
+
|
|
150
157
|
async function ensureSliceSummaryStub(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise<void> {
|
|
151
158
|
const path = join(resolveSlicePath(basePath, milestoneId, sliceId) ?? relSlicePath(basePath, milestoneId, sliceId), `${sliceId}-SUMMARY.md`);
|
|
152
159
|
const absolute = resolveSliceFile(basePath, milestoneId, sliceId, "SUMMARY") ?? join(resolveSlicePath(basePath, milestoneId, sliceId)!, `${sliceId}-SUMMARY.md`);
|
|
@@ -31,13 +31,14 @@ let pendingAutoStart: {
|
|
|
31
31
|
pi: ExtensionAPI;
|
|
32
32
|
basePath: string;
|
|
33
33
|
milestoneId: string; // the milestone being discussed
|
|
34
|
+
step?: boolean; // preserve step mode through discuss → auto transition
|
|
34
35
|
} | null = null;
|
|
35
36
|
|
|
36
37
|
/** Called from agent_end to check if auto-mode should start after discuss */
|
|
37
38
|
export function checkAutoStartAfterDiscuss(): boolean {
|
|
38
39
|
if (!pendingAutoStart) return false;
|
|
39
40
|
|
|
40
|
-
const { ctx, pi, basePath, milestoneId } = pendingAutoStart;
|
|
41
|
+
const { ctx, pi, basePath, milestoneId, step } = pendingAutoStart;
|
|
41
42
|
|
|
42
43
|
// Don't fire until the discuss phase has actually produced a context file
|
|
43
44
|
// for the milestone being discussed. agent_end fires after every LLM turn,
|
|
@@ -47,7 +48,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
47
48
|
if (!contextFile) return false; // no context yet — keep waiting
|
|
48
49
|
|
|
49
50
|
pendingAutoStart = null;
|
|
50
|
-
startAuto(ctx, pi, basePath, false).catch(() => {});
|
|
51
|
+
startAuto(ctx, pi, basePath, false, { step }).catch(() => {});
|
|
51
52
|
return true;
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -435,7 +436,9 @@ export async function showSmartEntry(
|
|
|
435
436
|
ctx: ExtensionCommandContext,
|
|
436
437
|
pi: ExtensionAPI,
|
|
437
438
|
basePath: string,
|
|
439
|
+
options?: { step?: boolean },
|
|
438
440
|
): Promise<void> {
|
|
441
|
+
const stepMode = options?.step;
|
|
439
442
|
|
|
440
443
|
// ── Ensure git repo exists — GSD needs it for branch-per-slice ──────
|
|
441
444
|
try {
|
|
@@ -501,7 +504,7 @@ export async function showSmartEntry(
|
|
|
501
504
|
|
|
502
505
|
if (isFirst) {
|
|
503
506
|
// First ever — skip wizard, just ask directly
|
|
504
|
-
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
|
|
507
|
+
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
505
508
|
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
506
509
|
`New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`,
|
|
507
510
|
basePath
|
|
@@ -522,7 +525,7 @@ export async function showSmartEntry(
|
|
|
522
525
|
});
|
|
523
526
|
|
|
524
527
|
if (choice === "new_milestone") {
|
|
525
|
-
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
|
|
528
|
+
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
526
529
|
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
527
530
|
`New milestone ${nextId}.`,
|
|
528
531
|
basePath
|
|
@@ -560,7 +563,7 @@ export async function showSmartEntry(
|
|
|
560
563
|
const milestoneIds = findMilestoneIds(basePath);
|
|
561
564
|
const nextId = `M${String(milestoneIds.length + 1).padStart(3, "0")}`;
|
|
562
565
|
|
|
563
|
-
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
|
|
566
|
+
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
564
567
|
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
565
568
|
`New milestone ${nextId}.`,
|
|
566
569
|
basePath
|