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.
Files changed (128) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +62 -4
  3. package/dist/headless.d.ts +21 -0
  4. package/dist/headless.js +346 -0
  5. package/dist/help-text.js +32 -0
  6. package/dist/mcp-server.d.ts +20 -3
  7. package/dist/mcp-server.js +21 -1
  8. package/dist/models-resolver.d.ts +32 -0
  9. package/dist/models-resolver.js +50 -0
  10. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  11. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  12. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  13. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  14. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  15. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  16. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  17. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  18. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  19. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  20. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  21. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  22. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  23. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  24. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  25. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  26. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  27. package/dist/resources/extensions/gsd/auto-recovery.ts +10 -0
  28. package/dist/resources/extensions/gsd/auto.ts +437 -11
  29. package/dist/resources/extensions/gsd/captures.ts +49 -0
  30. package/dist/resources/extensions/gsd/commands.ts +20 -3
  31. package/dist/resources/extensions/gsd/dashboard-overlay.ts +16 -2
  32. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  33. package/dist/resources/extensions/gsd/doctor.ts +20 -1
  34. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  35. package/dist/resources/extensions/gsd/guided-flow.ts +10 -5
  36. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  37. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  40. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  41. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  42. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
  43. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  44. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  45. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  46. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  47. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
  48. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  49. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  50. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  51. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  52. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  55. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  56. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  57. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  58. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  59. package/package.json +1 -1
  60. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  61. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  62. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  63. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  65. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  67. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  69. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  71. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  73. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/index.js +1 -1
  75. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  76. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  77. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  78. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  79. package/packages/pi-coding-agent/src/index.ts +1 -0
  80. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  81. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  82. package/src/resources/extensions/bg-shell/types.ts +33 -1
  83. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  84. package/src/resources/extensions/browser-tools/index.ts +20 -0
  85. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  86. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  87. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  88. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  89. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  90. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  91. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  92. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  93. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  94. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  95. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  96. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  97. package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
  98. package/src/resources/extensions/gsd/auto.ts +437 -11
  99. package/src/resources/extensions/gsd/captures.ts +49 -0
  100. package/src/resources/extensions/gsd/commands.ts +20 -3
  101. package/src/resources/extensions/gsd/dashboard-overlay.ts +16 -2
  102. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  103. package/src/resources/extensions/gsd/doctor.ts +20 -1
  104. package/src/resources/extensions/gsd/forensics.ts +95 -52
  105. package/src/resources/extensions/gsd/guided-flow.ts +10 -5
  106. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  107. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  108. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  109. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  110. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  111. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  112. package/src/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
  113. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  114. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  115. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  116. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  117. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
  118. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  119. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  120. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  121. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  122. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  123. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  124. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  125. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  126. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  127. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  128. 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
- : th.fg("dim", "idle");
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 = th.fg("dim", formatDuration(this.dashData.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
- function git(args: string[], cwd: string): string {
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
- // 1. Committed changes in the last N commits (or since sinceDays)
63
- let committedFiles: string[] = [];
64
- try {
65
- const days = Math.max(1, Math.floor(Number(sinceDays)));
66
- if (!Number.isFinite(days)) throw new Error("invalid sinceDays");
67
- const raw = git(["log", "--diff-filter=ACMR", "--name-only", "--pretty=format:", `--since=${days} days ago`], dir);
68
- committedFiles = splitLines(raw);
69
- } catch {
70
- // Fallback: use HEAD~10
71
- try {
72
- const raw = git(["diff", "--name-only", "HEAD~10"], dir);
73
- committedFiles = splitLines(raw);
74
- } catch {
75
- // Shallow clone or <10 commits — ignore
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
- try {
135
- const numstat = git(["diff", "--cached", "--numstat"], dir);
136
- for (const line of splitLines(numstat)) {
137
- const [added, deleted, filePath] = line.split("\t");
138
- if (!filePath) continue;
139
- const lines =
140
- added === "-" || deleted === "-"
141
- ? undefined
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
- try {
151
- const numstat = git(["diff", "--numstat"], dir);
152
- for (const line of splitLines(numstat)) {
153
- const [added, deleted, filePath] = line.split("\t");
154
- if (!filePath) continue;
155
- const lines =
156
- added === "-" || deleted === "-"
157
- ? undefined
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
- try {
167
- const raw = git(["status", "--porcelain"], dir);
168
- for (const line of splitLines(raw)) {
169
- const code = line.slice(0, 2);
170
- const filePath = line.slice(3);
171
- if (seen.has(filePath)) continue;
172
-
173
- if (code.includes("?")) {
174
- add({ path: filePath, changeType: "added" });
175
- } else if (code.includes("D")) {
176
- add({ path: filePath, changeType: "deleted" });
177
- } else if (code.includes("A")) {
178
- add({ path: filePath, changeType: "added" });
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
- // 2. Scan activity logs (last 5)
147
- const unitTraces = scanActivityLogs(basePath);
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
- let gsdVersion = "unknown";
183
- try {
184
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../../../package.json");
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 activityDir = join(gsdRoot(basePath), "activity");
221
- if (!existsSync(activityDir)) return [];
222
-
223
- const files = readdirSync(activityDir).filter(f => f.endsWith(".jsonl")).sort();
224
- const lastFiles = files.slice(-5);
225
- const traces: UnitTrace[] = [];
226
-
227
- for (const file of lastFiles) {
228
- const match = ACTIVITY_FILENAME_RE.exec(file);
229
- if (!match) continue;
230
-
231
- const seq = parseInt(match[1]!, 10);
232
- const unitType = match[2]!;
233
- const unitId = match[3]!;
234
- const filePath = join(activityDir, file);
235
-
236
- let entries: unknown[] = [];
237
- const nativeResult = nativeParseJsonlTail(filePath, MAX_JSONL_BYTES);
238
- if (nativeResult) {
239
- entries = nativeResult.entries;
240
- } else {
241
- try {
242
- const raw = readFileSync(filePath, "utf-8");
243
- entries = parseJSONL(raw);
244
- } catch { continue; }
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
- const trace = extractTrace(entries);
248
- const stat = statSync(filePath, { throwIfNoEntry: false });
249
-
250
- traces.push({
251
- file,
252
- unitType,
253
- unitId,
254
- seq,
255
- trace,
256
- mtime: stat?.mtimeMs ?? 0,
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
- return traces.sort((a, b) => b.seq - a.seq);
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
- if (!verifyExpectedArtifact(unitType, unitId, basePath)) {
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);