gsd-pi 2.22.0 → 2.23.0
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 +25 -1
- package/dist/cli.js +62 -4
- package/dist/headless.d.ts +21 -0
- package/dist/headless.js +346 -0
- package/dist/help-text.js +32 -0
- package/dist/mcp-server.d.ts +20 -3
- package/dist/mcp-server.js +21 -1
- package/dist/models-resolver.d.ts +32 -0
- package/dist/models-resolver.js +50 -0
- package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/dist/resources/extensions/bg-shell/types.ts +33 -1
- package/dist/resources/extensions/browser-tools/capture.ts +18 -16
- package/dist/resources/extensions/browser-tools/index.ts +20 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +10 -0
- package/dist/resources/extensions/gsd/auto.ts +437 -11
- package/dist/resources/extensions/gsd/captures.ts +49 -0
- package/dist/resources/extensions/gsd/commands.ts +20 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +16 -2
- package/dist/resources/extensions/gsd/diff-context.ts +73 -80
- package/dist/resources/extensions/gsd/doctor.ts +20 -1
- package/dist/resources/extensions/gsd/forensics.ts +95 -52
- package/dist/resources/extensions/gsd/guided-flow.ts +10 -5
- package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
- package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/src/resources/extensions/bg-shell/types.ts +33 -1
- package/src/resources/extensions/browser-tools/capture.ts +18 -16
- package/src/resources/extensions/browser-tools/index.ts +20 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
- package/src/resources/extensions/gsd/auto.ts +437 -11
- package/src/resources/extensions/gsd/captures.ts +49 -0
- package/src/resources/extensions/gsd/commands.ts +20 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +16 -2
- package/src/resources/extensions/gsd/diff-context.ts +73 -80
- package/src/resources/extensions/gsd/doctor.ts +20 -1
- package/src/resources/extensions/gsd/forensics.ts +95 -52
- package/src/resources/extensions/gsd/guided-flow.ts +10 -5
- package/src/resources/extensions/gsd/mcp-server.ts +33 -12
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/src/resources/extensions/gsd/session-forensics.ts +36 -2
- package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/src/resources/extensions/gsd/workspace-index.ts +34 -6
|
@@ -14,7 +14,7 @@ import { deriveState } from "./state.js";
|
|
|
14
14
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
15
15
|
import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
|
|
16
16
|
import { showQueue, showDiscuss } from "./guided-flow.js";
|
|
17
|
-
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
|
|
17
|
+
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, dispatchDirectPhase } from "./auto.js";
|
|
18
18
|
import { resolveProjectRoot } from "./worktree.js";
|
|
19
19
|
import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
|
20
20
|
import {
|
|
@@ -69,11 +69,11 @@ function projectRoot(): string {
|
|
|
69
69
|
|
|
70
70
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
71
71
|
pi.registerCommand("gsd", {
|
|
72
|
-
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge",
|
|
72
|
+
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge",
|
|
73
73
|
getArgumentCompletions: (prefix: string) => {
|
|
74
74
|
const subcommands = [
|
|
75
75
|
"help", "next", "auto", "stop", "pause", "status", "visualize", "queue", "quick", "discuss",
|
|
76
|
-
"capture", "triage",
|
|
76
|
+
"capture", "triage", "dispatch",
|
|
77
77
|
"history", "undo", "skip", "export", "cleanup", "mode", "prefs",
|
|
78
78
|
"config", "hooks", "run-hook", "skill-health", "doctor", "forensics", "migrate", "remote", "steer", "inspect", "knowledge",
|
|
79
79
|
];
|
|
@@ -165,6 +165,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
165
165
|
return [];
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
if (parts[0] === "dispatch" && parts.length <= 2) {
|
|
169
|
+
const phasePrefix = parts[1] ?? "";
|
|
170
|
+
return ["research", "plan", "execute", "complete", "reassess", "uat", "replan"]
|
|
171
|
+
.filter((cmd) => cmd.startsWith(phasePrefix))
|
|
172
|
+
.map((cmd) => ({ value: `dispatch ${cmd}`, label: cmd }));
|
|
173
|
+
}
|
|
174
|
+
|
|
168
175
|
return [];
|
|
169
176
|
},
|
|
170
177
|
|
|
@@ -388,6 +395,16 @@ Examples:
|
|
|
388
395
|
return;
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
if (trimmed === "dispatch" || trimmed.startsWith("dispatch ")) {
|
|
399
|
+
const phase = trimmed.replace(/^dispatch\s*/, "").trim();
|
|
400
|
+
if (!phase) {
|
|
401
|
+
ctx.ui.notify("Usage: /gsd dispatch <phase> (research|plan|execute|complete|reassess|uat|replan)", "warning");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
await dispatchDirectPhase(ctx, pi, phase, projectRoot());
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
391
408
|
if (trimmed === "inspect") {
|
|
392
409
|
await handleInspect(ctx);
|
|
393
410
|
return;
|
|
@@ -319,16 +319,23 @@ export class GSDDashboardOverlay {
|
|
|
319
319
|
const centered = (content: string) => row(centerLine(content, contentWidth));
|
|
320
320
|
|
|
321
321
|
const title = th.fg("accent", th.bold("GSD Dashboard"));
|
|
322
|
+
const isRemote = !!this.dashData.remoteSession;
|
|
322
323
|
const status = this.dashData.active
|
|
323
324
|
? `${Date.now() % 2000 < 1000 ? th.fg("success", "●") : th.fg("dim", "○")} ${th.fg("success", "AUTO")}`
|
|
324
325
|
: this.dashData.paused
|
|
325
326
|
? th.fg("warning", "⏸ PAUSED")
|
|
326
|
-
:
|
|
327
|
+
: isRemote
|
|
328
|
+
? `${Date.now() % 2000 < 1000 ? th.fg("success", "●") : th.fg("dim", "○")} ${th.fg("success", "AUTO")} ${th.fg("dim", `(PID ${this.dashData.remoteSession!.pid})`)}`
|
|
329
|
+
: th.fg("dim", "idle");
|
|
327
330
|
const worktreeName = getActiveWorktreeName();
|
|
328
331
|
const worktreeTag = worktreeName
|
|
329
332
|
? ` ${th.fg("warning", `⎇ ${worktreeName}`)}`
|
|
330
333
|
: "";
|
|
331
|
-
const elapsed =
|
|
334
|
+
const elapsed = this.dashData.active || this.dashData.paused
|
|
335
|
+
? th.fg("dim", formatDuration(this.dashData.elapsed))
|
|
336
|
+
: isRemote
|
|
337
|
+
? th.fg("dim", `since ${this.dashData.remoteSession!.startedAt.replace("T", " ").slice(0, 19)}`)
|
|
338
|
+
: "";
|
|
332
339
|
lines.push(row(joinColumns(`${title} ${status}${worktreeTag}`, elapsed, contentWidth)));
|
|
333
340
|
lines.push(blank());
|
|
334
341
|
|
|
@@ -344,6 +351,13 @@ export class GSDDashboardOverlay {
|
|
|
344
351
|
} else if (this.dashData.paused) {
|
|
345
352
|
lines.push(row(th.fg("dim", "/gsd auto to resume")));
|
|
346
353
|
lines.push(blank());
|
|
354
|
+
} else if (isRemote) {
|
|
355
|
+
const rs = this.dashData.remoteSession!;
|
|
356
|
+
const unitDisplay = rs.unitType === "starting" || rs.unitType === "resuming"
|
|
357
|
+
? rs.unitType
|
|
358
|
+
: `${unitLabel(rs.unitType)} ${rs.unitId}`;
|
|
359
|
+
lines.push(row(th.fg("text", `Remote session: ${unitDisplay}`)));
|
|
360
|
+
lines.push(blank());
|
|
347
361
|
} else {
|
|
348
362
|
lines.push(row(th.fg("dim", "No unit running · /gsd auto to start")));
|
|
349
363
|
lines.push(blank());
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Standalone module: only imports node:child_process and node:path.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { execFileSync, execFile } from "node:child_process";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
@@ -32,10 +32,23 @@ const EXEC_OPTS = {
|
|
|
32
32
|
stdio: ["pipe", "pipe", "pipe"] as ["pipe", "pipe", "pipe"],
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/** Synchronous git — used where sequential control flow is required (fallback paths). */
|
|
36
|
+
function gitSync(args: string[], cwd: string): string {
|
|
36
37
|
return execFileSync("git", args, { ...EXEC_OPTS, cwd }).trim();
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/** Async git — returns stdout on success, empty string on any error. */
|
|
41
|
+
function gitAsync(args: string[], cwd: string): Promise<string> {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
execFile(
|
|
44
|
+
"git",
|
|
45
|
+
args,
|
|
46
|
+
{ encoding: "utf-8", timeout: 5000, cwd },
|
|
47
|
+
(err, stdout) => resolve(err ? "" : stdout.trim()),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
function splitLines(output: string): string[] {
|
|
40
53
|
return output
|
|
41
54
|
.split("\n")
|
|
@@ -49,6 +62,8 @@ function splitLines(output: string): string[] {
|
|
|
49
62
|
* Returns recently-changed file paths, deduplicated and sorted by recency
|
|
50
63
|
* (most recent first). Combines committed diffs, staged changes, and
|
|
51
64
|
* unstaged/untracked files from `git status`.
|
|
65
|
+
*
|
|
66
|
+
* The three git queries (log, diff --cached, status) run concurrently.
|
|
52
67
|
*/
|
|
53
68
|
export async function getRecentlyChangedFiles(
|
|
54
69
|
cwd: string,
|
|
@@ -59,40 +74,23 @@ export async function getRecentlyChangedFiles(
|
|
|
59
74
|
const dir = resolve(cwd);
|
|
60
75
|
|
|
61
76
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// 2. Staged changes
|
|
80
|
-
let stagedFiles: string[] = [];
|
|
81
|
-
try {
|
|
82
|
-
const raw = git(["diff", "--cached", "--name-only"], dir);
|
|
83
|
-
stagedFiles = splitLines(raw);
|
|
84
|
-
} catch {
|
|
85
|
-
// ignore
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 3. Unstaged / untracked via porcelain status
|
|
89
|
-
let statusFiles: string[] = [];
|
|
90
|
-
try {
|
|
91
|
-
const raw = git(["status", "--porcelain"], dir);
|
|
92
|
-
statusFiles = splitLines(raw).map((line) => line.slice(3)); // strip XY + space
|
|
93
|
-
} catch {
|
|
94
|
-
// ignore
|
|
95
|
-
}
|
|
77
|
+
const days = Math.max(1, Math.floor(Number(sinceDays)));
|
|
78
|
+
if (!Number.isFinite(days)) throw new Error("invalid sinceDays");
|
|
79
|
+
|
|
80
|
+
// Run all three queries concurrently — they read independent git state
|
|
81
|
+
const [logRaw, stagedRaw, statusRaw] = await Promise.all([
|
|
82
|
+
// 1. Committed changes since N days ago (fallback to HEAD~10 on error)
|
|
83
|
+
gitAsync(["log", "--diff-filter=ACMR", "--name-only", "--pretty=format:", `--since=${days} days ago`], dir)
|
|
84
|
+
.then((out) => out || gitAsync(["diff", "--name-only", "HEAD~10"], dir)),
|
|
85
|
+
// 2. Staged changes
|
|
86
|
+
gitAsync(["diff", "--cached", "--name-only"], dir),
|
|
87
|
+
// 3. Unstaged / untracked
|
|
88
|
+
gitAsync(["status", "--porcelain"], dir),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const committedFiles = splitLines(logRaw);
|
|
92
|
+
const stagedFiles = splitLines(stagedRaw);
|
|
93
|
+
const statusFiles = splitLines(statusRaw).map((line) => line.slice(3)); // strip XY + space
|
|
96
94
|
|
|
97
95
|
// Deduplicate, preserving insertion order (most-recent-first: status → staged → committed)
|
|
98
96
|
const seen = new Set<string>();
|
|
@@ -113,6 +111,9 @@ export async function getRecentlyChangedFiles(
|
|
|
113
111
|
|
|
114
112
|
/**
|
|
115
113
|
* Returns richer change metadata: change type and approximate line counts.
|
|
114
|
+
*
|
|
115
|
+
* The three git queries (diff --cached --numstat, diff --numstat, status --porcelain)
|
|
116
|
+
* run concurrently — they read independent git state.
|
|
116
117
|
*/
|
|
117
118
|
export async function getChangedFilesWithContext(
|
|
118
119
|
cwd: string,
|
|
@@ -120,6 +121,13 @@ export async function getChangedFilesWithContext(
|
|
|
120
121
|
const dir = resolve(cwd);
|
|
121
122
|
|
|
122
123
|
try {
|
|
124
|
+
// Run all three queries concurrently
|
|
125
|
+
const [cachedNumstat, unstagedNumstat, statusRaw] = await Promise.all([
|
|
126
|
+
gitAsync(["diff", "--cached", "--numstat"], dir),
|
|
127
|
+
gitAsync(["diff", "--numstat"], dir),
|
|
128
|
+
gitAsync(["status", "--porcelain"], dir),
|
|
129
|
+
]);
|
|
130
|
+
|
|
123
131
|
const result: ChangedFileInfo[] = [];
|
|
124
132
|
const seen = new Set<string>();
|
|
125
133
|
|
|
@@ -131,57 +139,42 @@ export async function getChangedFilesWithContext(
|
|
|
131
139
|
};
|
|
132
140
|
|
|
133
141
|
// 1. Staged files with numstat
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
added
|
|
141
|
-
|
|
142
|
-
: Number(added) + Number(deleted);
|
|
143
|
-
add({ path: filePath, changeType: "staged", linesChanged: lines });
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
// ignore
|
|
142
|
+
for (const line of splitLines(cachedNumstat)) {
|
|
143
|
+
const [added, deleted, filePath] = line.split("\t");
|
|
144
|
+
if (!filePath) continue;
|
|
145
|
+
const lines =
|
|
146
|
+
added === "-" || deleted === "-"
|
|
147
|
+
? undefined
|
|
148
|
+
: Number(added) + Number(deleted);
|
|
149
|
+
add({ path: filePath, changeType: "staged", linesChanged: lines });
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
// 2. Unstaged modifications with numstat
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
added
|
|
157
|
-
|
|
158
|
-
: Number(added) + Number(deleted);
|
|
159
|
-
add({ path: filePath, changeType: "modified", linesChanged: lines });
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
// ignore
|
|
153
|
+
for (const line of splitLines(unstagedNumstat)) {
|
|
154
|
+
const [added, deleted, filePath] = line.split("\t");
|
|
155
|
+
if (!filePath) continue;
|
|
156
|
+
const lines =
|
|
157
|
+
added === "-" || deleted === "-"
|
|
158
|
+
? undefined
|
|
159
|
+
: Number(added) + Number(deleted);
|
|
160
|
+
add({ path: filePath, changeType: "modified", linesChanged: lines });
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
// 3. Untracked / deleted from porcelain status
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
} else {
|
|
180
|
-
add({ path: filePath, changeType: "modified" });
|
|
181
|
-
}
|
|
164
|
+
for (const line of splitLines(statusRaw)) {
|
|
165
|
+
const code = line.slice(0, 2);
|
|
166
|
+
const filePath = line.slice(3);
|
|
167
|
+
if (seen.has(filePath)) continue;
|
|
168
|
+
|
|
169
|
+
if (code.includes("?")) {
|
|
170
|
+
add({ path: filePath, changeType: "added" });
|
|
171
|
+
} else if (code.includes("D")) {
|
|
172
|
+
add({ path: filePath, changeType: "deleted" });
|
|
173
|
+
} else if (code.includes("A")) {
|
|
174
|
+
add({ path: filePath, changeType: "added" });
|
|
175
|
+
} else {
|
|
176
|
+
add({ path: filePath, changeType: "modified" });
|
|
182
177
|
}
|
|
183
|
-
} catch {
|
|
184
|
-
// ignore
|
|
185
178
|
}
|
|
186
179
|
|
|
187
180
|
return result;
|
|
@@ -41,7 +41,8 @@ export type DoctorIssueCode =
|
|
|
41
41
|
| "activity_log_bloat"
|
|
42
42
|
| "state_file_stale"
|
|
43
43
|
| "state_file_missing"
|
|
44
|
-
| "gitignore_missing_patterns"
|
|
44
|
+
| "gitignore_missing_patterns"
|
|
45
|
+
| "unresolvable_dependency";
|
|
45
46
|
|
|
46
47
|
export interface DoctorIssue {
|
|
47
48
|
severity: DoctorSeverity;
|
|
@@ -1041,6 +1042,24 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
1041
1042
|
});
|
|
1042
1043
|
}
|
|
1043
1044
|
|
|
1045
|
+
// Check for unresolvable dependency IDs — catches range syntax like "S01-S04"
|
|
1046
|
+
// that the parser expanded but that don't match any actual slice in the roadmap.
|
|
1047
|
+
// Also catches plain typos or IDs referencing slices not yet defined.
|
|
1048
|
+
const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
|
|
1049
|
+
for (const dep of slice.depends) {
|
|
1050
|
+
if (!knownSliceIds.has(dep)) {
|
|
1051
|
+
issues.push({
|
|
1052
|
+
severity: "warning",
|
|
1053
|
+
code: "unresolvable_dependency",
|
|
1054
|
+
scope: "slice",
|
|
1055
|
+
unitId,
|
|
1056
|
+
message: `Slice ${unitId} depends on "${dep}" which is not a slice ID in this roadmap. This permanently blocks the slice. Use comma-separated IDs: \`depends:[S01,S02]\``,
|
|
1057
|
+
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
1058
|
+
fixable: false,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1044
1063
|
const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
|
|
1045
1064
|
if (!slicePath) continue;
|
|
1046
1065
|
|
|
@@ -27,6 +27,7 @@ import { isAutoActive } from "./auto.js";
|
|
|
27
27
|
import { loadPrompt } from "./prompt-loader.js";
|
|
28
28
|
import { gsdRoot } from "./paths.js";
|
|
29
29
|
import { formatDuration } from "./history.js";
|
|
30
|
+
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
30
31
|
|
|
31
32
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
32
33
|
|
|
@@ -54,6 +55,7 @@ interface ForensicReport {
|
|
|
54
55
|
basePath: string;
|
|
55
56
|
activeMilestone: string | null;
|
|
56
57
|
activeSlice: string | null;
|
|
58
|
+
activeWorktree: string | null;
|
|
57
59
|
unitTraces: UnitTrace[];
|
|
58
60
|
metrics: MetricsLedger | null;
|
|
59
61
|
completedKeys: string[];
|
|
@@ -143,8 +145,11 @@ async function buildForensicReport(basePath: string): Promise<ForensicReport> {
|
|
|
143
145
|
activeSlice = state.activeSlice?.id ?? null;
|
|
144
146
|
} catch { /* state derivation failure is non-fatal */ }
|
|
145
147
|
|
|
146
|
-
//
|
|
147
|
-
const
|
|
148
|
+
// 1b. Check for active auto-worktree
|
|
149
|
+
const activeWorktree = activeMilestone ? getAutoWorktreePath(basePath, activeMilestone) : null;
|
|
150
|
+
|
|
151
|
+
// 2. Scan activity logs (last 5) — worktree-aware
|
|
152
|
+
const unitTraces = scanActivityLogs(basePath, activeMilestone);
|
|
148
153
|
|
|
149
154
|
// 3. Load metrics
|
|
150
155
|
const metrics = loadLedgerFromDisk(basePath);
|
|
@@ -178,20 +183,16 @@ async function buildForensicReport(basePath: string): Promise<ForensicReport> {
|
|
|
178
183
|
}
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
// 8. GSD version
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (existsSync(pkgPath)) {
|
|
186
|
-
gsdVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "unknown";
|
|
187
|
-
}
|
|
188
|
-
} catch { /* non-fatal */ }
|
|
186
|
+
// 8. GSD version — use GSD_VERSION env var set by the loader at startup.
|
|
187
|
+
// Extensions run from ~/.gsd/agent/extensions/gsd/ at runtime, so path-traversal
|
|
188
|
+
// from import.meta.url would resolve to ~/package.json (wrong on every system).
|
|
189
|
+
const gsdVersion = process.env.GSD_VERSION || "unknown";
|
|
189
190
|
|
|
190
191
|
// 9. Run anomaly detectors
|
|
191
192
|
if (metrics?.units) detectStuckLoops(metrics.units, anomalies);
|
|
192
193
|
if (metrics?.units) detectCostSpikes(metrics.units, anomalies);
|
|
193
194
|
detectTimeouts(unitTraces, anomalies);
|
|
194
|
-
detectMissingArtifacts(completedKeys, basePath, anomalies);
|
|
195
|
+
detectMissingArtifacts(completedKeys, basePath, activeMilestone, anomalies);
|
|
195
196
|
detectCrash(crashLock, anomalies);
|
|
196
197
|
detectDoctorIssues(doctorIssues, anomalies);
|
|
197
198
|
detectErrorTraces(unitTraces, anomalies);
|
|
@@ -202,6 +203,7 @@ async function buildForensicReport(basePath: string): Promise<ForensicReport> {
|
|
|
202
203
|
basePath,
|
|
203
204
|
activeMilestone,
|
|
204
205
|
activeSlice,
|
|
206
|
+
activeWorktree: activeWorktree ? relative(basePath, activeWorktree) : null,
|
|
205
207
|
unitTraces,
|
|
206
208
|
metrics,
|
|
207
209
|
completedKeys,
|
|
@@ -216,48 +218,78 @@ async function buildForensicReport(basePath: string): Promise<ForensicReport> {
|
|
|
216
218
|
|
|
217
219
|
const ACTIVITY_FILENAME_RE = /^(\d+)-(.+?)-(.+)\.jsonl$/;
|
|
218
220
|
|
|
219
|
-
function scanActivityLogs(basePath: string): UnitTrace[] {
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
221
|
+
function scanActivityLogs(basePath: string, activeMilestone?: string | null): UnitTrace[] {
|
|
222
|
+
const activityDirs = resolveActivityDirs(basePath, activeMilestone);
|
|
223
|
+
const allTraces: UnitTrace[] = [];
|
|
224
|
+
|
|
225
|
+
for (const activityDir of activityDirs) {
|
|
226
|
+
if (!existsSync(activityDir)) continue;
|
|
227
|
+
|
|
228
|
+
const files = readdirSync(activityDir).filter(f => f.endsWith(".jsonl")).sort();
|
|
229
|
+
const lastFiles = files.slice(-5);
|
|
230
|
+
|
|
231
|
+
for (const file of lastFiles) {
|
|
232
|
+
const match = ACTIVITY_FILENAME_RE.exec(file);
|
|
233
|
+
if (!match) continue;
|
|
234
|
+
|
|
235
|
+
const seq = parseInt(match[1]!, 10);
|
|
236
|
+
const unitType = match[2]!;
|
|
237
|
+
const unitId = match[3]!;
|
|
238
|
+
const filePath = join(activityDir, file);
|
|
239
|
+
|
|
240
|
+
let entries: unknown[] = [];
|
|
241
|
+
const nativeResult = nativeParseJsonlTail(filePath, MAX_JSONL_BYTES);
|
|
242
|
+
if (nativeResult) {
|
|
243
|
+
entries = nativeResult.entries;
|
|
244
|
+
} else {
|
|
245
|
+
try {
|
|
246
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
247
|
+
entries = parseJSONL(raw);
|
|
248
|
+
} catch { continue; }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const trace = extractTrace(entries);
|
|
252
|
+
const stat = statSync(filePath, { throwIfNoEntry: false });
|
|
253
|
+
|
|
254
|
+
allTraces.push({
|
|
255
|
+
file: activityDirs.length > 1 ? `[${relative(basePath, activityDir)}] ${file}` : file,
|
|
256
|
+
unitType,
|
|
257
|
+
unitId,
|
|
258
|
+
seq,
|
|
259
|
+
trace,
|
|
260
|
+
mtime: stat?.mtimeMs ?? 0,
|
|
261
|
+
});
|
|
245
262
|
}
|
|
263
|
+
}
|
|
246
264
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
265
|
+
// Sort by mtime descending so the most recent traces (regardless of source) come first
|
|
266
|
+
return allTraces.sort((a, b) => b.mtime - a.mtime).slice(0, 5);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolve activity directories to scan for forensics.
|
|
271
|
+
* If an active auto-worktree exists for the milestone, its activity dir
|
|
272
|
+
* is included first (preferred) so stale root logs don't mask worktree progress.
|
|
273
|
+
*/
|
|
274
|
+
function resolveActivityDirs(basePath: string, activeMilestone?: string | null): string[] {
|
|
275
|
+
const dirs: string[] = [];
|
|
276
|
+
|
|
277
|
+
// Check for active auto-worktree activity logs
|
|
278
|
+
if (activeMilestone) {
|
|
279
|
+
const wtPath = getAutoWorktreePath(basePath, activeMilestone);
|
|
280
|
+
if (wtPath) {
|
|
281
|
+
const wtActivityDir = join(wtPath, ".gsd", "activity");
|
|
282
|
+
if (existsSync(wtActivityDir)) {
|
|
283
|
+
dirs.push(wtActivityDir);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
258
286
|
}
|
|
259
287
|
|
|
260
|
-
|
|
288
|
+
// Always include root activity logs
|
|
289
|
+
const rootActivityDir = join(gsdRoot(basePath), "activity");
|
|
290
|
+
dirs.push(rootActivityDir);
|
|
291
|
+
|
|
292
|
+
return dirs;
|
|
261
293
|
}
|
|
262
294
|
|
|
263
295
|
// ─── Completed Keys Loader ────────────────────────────────────────────────────
|
|
@@ -336,21 +368,27 @@ function detectTimeouts(traces: UnitTrace[], anomalies: ForensicAnomaly[]): void
|
|
|
336
368
|
}
|
|
337
369
|
}
|
|
338
370
|
|
|
339
|
-
function detectMissingArtifacts(completedKeys: string[], basePath: string, anomalies: ForensicAnomaly[]): void {
|
|
371
|
+
function detectMissingArtifacts(completedKeys: string[], basePath: string, activeMilestone: string | null, anomalies: ForensicAnomaly[]): void {
|
|
372
|
+
// Also check the worktree path for artifacts — they may exist there but not at root
|
|
373
|
+
const wtBasePath = activeMilestone ? getAutoWorktreePath(basePath, activeMilestone) : null;
|
|
374
|
+
|
|
340
375
|
for (const key of completedKeys) {
|
|
341
376
|
const slashIdx = key.indexOf("/");
|
|
342
377
|
if (slashIdx === -1) continue;
|
|
343
378
|
const unitType = key.slice(0, slashIdx);
|
|
344
379
|
const unitId = key.slice(slashIdx + 1);
|
|
345
380
|
|
|
346
|
-
|
|
381
|
+
const rootHasArtifact = verifyExpectedArtifact(unitType, unitId, basePath);
|
|
382
|
+
const wtHasArtifact = wtBasePath ? verifyExpectedArtifact(unitType, unitId, wtBasePath) : false;
|
|
383
|
+
|
|
384
|
+
if (!rootHasArtifact && !wtHasArtifact) {
|
|
347
385
|
anomalies.push({
|
|
348
386
|
type: "missing-artifact",
|
|
349
387
|
severity: "error",
|
|
350
388
|
unitType,
|
|
351
389
|
unitId,
|
|
352
390
|
summary: `Completed key ${key} but artifact missing or invalid`,
|
|
353
|
-
details: `The unit is recorded as completed but verifyExpectedArtifact() returns false. The completion state is stale.`,
|
|
391
|
+
details: `The unit is recorded as completed but verifyExpectedArtifact() returns false at both project root and worktree. The completion state is stale.`,
|
|
354
392
|
});
|
|
355
393
|
}
|
|
356
394
|
}
|
|
@@ -416,6 +454,7 @@ function saveForensicReport(basePath: string, report: ForensicReport, problemDes
|
|
|
416
454
|
`**GSD Version:** ${report.gsdVersion}`,
|
|
417
455
|
`**Active Milestone:** ${report.activeMilestone ?? "none"}`,
|
|
418
456
|
`**Active Slice:** ${report.activeSlice ?? "none"}`,
|
|
457
|
+
`**Active Worktree:** ${report.activeWorktree ?? "none"}`,
|
|
419
458
|
``,
|
|
420
459
|
`## Problem Description`,
|
|
421
460
|
``,
|
|
@@ -559,6 +598,10 @@ function formatReportForPrompt(report: ForensicReport): string {
|
|
|
559
598
|
sections.push(`### GSD Version: ${report.gsdVersion}`);
|
|
560
599
|
sections.push(`### Active Milestone: ${report.activeMilestone ?? "none"}`);
|
|
561
600
|
sections.push(`### Active Slice: ${report.activeSlice ?? "none"}`);
|
|
601
|
+
if (report.activeWorktree) {
|
|
602
|
+
sections.push(`### Active Worktree: ${report.activeWorktree}`);
|
|
603
|
+
sections.push(`Note: Activity logs were scanned from both the worktree and the project root. Worktree logs take priority.`);
|
|
604
|
+
}
|
|
562
605
|
|
|
563
606
|
let result = sections.join("\n");
|
|
564
607
|
if (result.length > MAX_BYTES) {
|
|
@@ -821,8 +821,9 @@ export async function showDiscuss(
|
|
|
821
821
|
|
|
822
822
|
if (choice === "discuss_draft") {
|
|
823
823
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
824
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
824
825
|
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
825
|
-
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
826
|
+
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
826
827
|
});
|
|
827
828
|
const seed = draftContent
|
|
828
829
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
@@ -831,9 +832,10 @@ export async function showDiscuss(
|
|
|
831
832
|
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
832
833
|
} else if (choice === "discuss_fresh") {
|
|
833
834
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
835
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
834
836
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
835
837
|
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
836
|
-
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
838
|
+
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
837
839
|
}), "gsd-discuss");
|
|
838
840
|
} else if (choice === "skip_milestone") {
|
|
839
841
|
const milestoneIds = findMilestoneIds(basePath);
|
|
@@ -1136,8 +1138,9 @@ export async function showSmartEntry(
|
|
|
1136
1138
|
|
|
1137
1139
|
if (choice === "discuss_draft") {
|
|
1138
1140
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1141
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
1139
1142
|
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
1140
|
-
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
1143
|
+
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1141
1144
|
});
|
|
1142
1145
|
const seed = draftContent
|
|
1143
1146
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
@@ -1146,9 +1149,10 @@ export async function showSmartEntry(
|
|
|
1146
1149
|
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
1147
1150
|
} else if (choice === "discuss_fresh") {
|
|
1148
1151
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1152
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
1149
1153
|
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
|
1150
1154
|
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1151
|
-
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
1155
|
+
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1152
1156
|
}), "gsd-discuss");
|
|
1153
1157
|
} else if (choice === "skip_milestone") {
|
|
1154
1158
|
const milestoneIds = findMilestoneIds(basePath);
|
|
@@ -1220,8 +1224,9 @@ export async function showSmartEntry(
|
|
|
1220
1224
|
}));
|
|
1221
1225
|
} else if (choice === "discuss") {
|
|
1222
1226
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1227
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
1223
1228
|
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1224
|
-
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
1229
|
+
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1225
1230
|
}));
|
|
1226
1231
|
} else if (choice === "skip_milestone") {
|
|
1227
1232
|
const milestoneIds = findMilestoneIds(basePath);
|