gsd-pi 2.22.0 → 2.24.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 (228) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +74 -7
  3. package/dist/headless.d.ts +25 -0
  4. package/dist/headless.js +454 -0
  5. package/dist/help-text.js +47 -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/resource-loader.js +64 -9
  11. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  12. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  13. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  14. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  15. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  16. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  17. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  18. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  19. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  20. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  21. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  22. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  23. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  24. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  25. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  26. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  27. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  28. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  29. package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
  30. package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
  31. package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
  32. package/dist/resources/extensions/gsd/auto.ts +560 -52
  33. package/dist/resources/extensions/gsd/captures.ts +49 -0
  34. package/dist/resources/extensions/gsd/commands.ts +194 -11
  35. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  36. package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  37. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  38. package/dist/resources/extensions/gsd/doctor.ts +76 -12
  39. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  40. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  41. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  42. package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
  43. package/dist/resources/extensions/gsd/index.ts +34 -1
  44. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  45. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  46. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  47. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  48. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  49. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  50. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  51. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  52. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  54. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  55. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  56. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  57. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  58. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  59. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  60. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  61. package/dist/resources/extensions/gsd/state.ts +72 -30
  62. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  63. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  64. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  65. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  66. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  67. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  68. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  69. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  70. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  71. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  72. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  73. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  74. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  75. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  76. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  77. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  78. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  79. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  80. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  81. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  82. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  83. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  84. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  85. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  86. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  87. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  88. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  89. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  90. package/dist/resources/extensions/gsd/types.ts +15 -1
  91. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  92. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  93. package/dist/resources/extensions/subagent/index.ts +5 -0
  94. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  95. package/dist/update-check.d.ts +9 -0
  96. package/dist/update-check.js +97 -0
  97. package/package.json +6 -1
  98. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  99. package/packages/pi-ai/dist/providers/anthropic.js +16 -7
  100. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  101. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  103. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  106. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  112. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +21 -8
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  117. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  118. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  119. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  123. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  125. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  127. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  129. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  133. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  135. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/index.js +1 -1
  137. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  138. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  139. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  140. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  141. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  142. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  143. package/packages/pi-coding-agent/src/index.ts +1 -0
  144. package/scripts/postinstall.js +7 -109
  145. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  146. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  147. package/src/resources/extensions/bg-shell/types.ts +33 -1
  148. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  149. package/src/resources/extensions/browser-tools/index.ts +20 -0
  150. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  151. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  152. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  153. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  154. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  155. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  156. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  157. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  158. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  159. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  160. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  161. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  163. package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
  164. package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
  165. package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
  166. package/src/resources/extensions/gsd/auto.ts +560 -52
  167. package/src/resources/extensions/gsd/captures.ts +49 -0
  168. package/src/resources/extensions/gsd/commands.ts +194 -11
  169. package/src/resources/extensions/gsd/complexity.ts +1 -0
  170. package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  171. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  172. package/src/resources/extensions/gsd/doctor.ts +76 -12
  173. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  174. package/src/resources/extensions/gsd/forensics.ts +95 -52
  175. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  176. package/src/resources/extensions/gsd/guided-flow.ts +85 -5
  177. package/src/resources/extensions/gsd/index.ts +34 -1
  178. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  179. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  180. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  181. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  182. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  183. package/src/resources/extensions/gsd/preferences.ts +65 -1
  184. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  185. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  186. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  187. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  188. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  189. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  190. package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  191. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  192. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  193. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  194. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  195. package/src/resources/extensions/gsd/state.ts +72 -30
  196. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  197. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  198. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  199. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  200. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  201. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  202. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  203. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  204. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  205. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  206. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  207. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  208. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  209. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  210. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  211. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  212. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  213. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  214. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  215. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  216. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  217. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  218. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  219. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  220. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  221. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  222. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  223. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  224. package/src/resources/extensions/gsd/types.ts +15 -1
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  226. package/src/resources/extensions/gsd/workspace-index.ts +34 -6
  227. package/src/resources/extensions/subagent/index.ts +5 -0
  228. package/src/resources/extensions/subagent/worker-registry.ts +99 -0
@@ -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;
@@ -11,6 +11,7 @@ import { RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
11
11
  import { nativeIsRepo, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached } from "./native-git-bridge.js";
12
12
  import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
13
13
  import { ensureGitignore } from "./gitignore.js";
14
+ import { readAllSessionStatuses, isSessionStale, removeSessionStatus } from "./session-status-io.js";
14
15
 
15
16
  export type DoctorSeverity = "info" | "warning" | "error";
16
17
  export type DoctorIssueCode =
@@ -24,6 +25,7 @@ export type DoctorIssueCode =
24
25
  | "all_tasks_done_roadmap_not_checked"
25
26
  | "slice_checked_missing_summary"
26
27
  | "slice_checked_missing_uat"
28
+ | "all_slices_done_missing_milestone_validation"
27
29
  | "all_slices_done_missing_milestone_summary"
28
30
  | "task_done_must_haves_not_verified"
29
31
  | "active_requirement_missing_owner"
@@ -36,12 +38,14 @@ export type DoctorIssueCode =
36
38
  | "tracked_runtime_files"
37
39
  | "legacy_slice_branches"
38
40
  | "stale_crash_lock"
41
+ | "stale_parallel_session"
39
42
  | "orphaned_completed_units"
40
43
  | "stale_hook_state"
41
44
  | "activity_log_bloat"
42
45
  | "state_file_stale"
43
46
  | "state_file_missing"
44
- | "gitignore_missing_patterns";
47
+ | "gitignore_missing_patterns"
48
+ | "unresolvable_dependency";
45
49
 
46
50
  export interface DoctorIssue {
47
51
  severity: DoctorSeverity;
@@ -709,6 +713,31 @@ async function checkRuntimeHealth(
709
713
  // Non-fatal — crash lock check failed
710
714
  }
711
715
 
716
+ // ── Stale parallel sessions ────────────────────────────────────────────
717
+ try {
718
+ const parallelStatuses = readAllSessionStatuses(basePath);
719
+ for (const status of parallelStatuses) {
720
+ if (isSessionStale(status)) {
721
+ issues.push({
722
+ severity: "warning",
723
+ code: "stale_parallel_session",
724
+ scope: "project",
725
+ unitId: status.milestoneId,
726
+ message: `Stale parallel session for ${status.milestoneId} (PID ${status.pid}, started ${new Date(status.startedAt).toISOString()}, last heartbeat ${new Date(status.lastHeartbeat).toISOString()}) — process is no longer running`,
727
+ file: `.gsd/parallel/${status.milestoneId}.status.json`,
728
+ fixable: true,
729
+ });
730
+
731
+ if (shouldFix("stale_parallel_session")) {
732
+ removeSessionStatus(basePath, status.milestoneId);
733
+ fixesApplied.push(`cleaned up stale parallel session for ${status.milestoneId}`);
734
+ }
735
+ }
736
+ }
737
+ } catch {
738
+ // Non-fatal — parallel session check failed
739
+ }
740
+
712
741
  // ── Orphaned completed-units keys ─────────────────────────────────────
713
742
  try {
714
743
  const completedKeysFile = join(root, "completed-units.json");
@@ -1041,17 +1070,37 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
1041
1070
  });
1042
1071
  }
1043
1072
 
1073
+ // Check for unresolvable dependency IDs — catches range syntax like "S01-S04"
1074
+ // that the parser expanded but that don't match any actual slice in the roadmap.
1075
+ // Also catches plain typos or IDs referencing slices not yet defined.
1076
+ const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
1077
+ for (const dep of slice.depends) {
1078
+ if (!knownSliceIds.has(dep)) {
1079
+ issues.push({
1080
+ severity: "warning",
1081
+ code: "unresolvable_dependency",
1082
+ scope: "slice",
1083
+ unitId,
1084
+ 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]\``,
1085
+ file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
1086
+ fixable: false,
1087
+ });
1088
+ }
1089
+ }
1090
+
1044
1091
  const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
1045
1092
  if (!slicePath) continue;
1046
1093
 
1047
1094
  const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
1048
1095
  if (!tasksDir) {
1049
1096
  issues.push({
1050
- severity: "error",
1097
+ severity: slice.done ? "warning" : "error",
1051
1098
  code: "missing_tasks_dir",
1052
1099
  scope: "slice",
1053
1100
  unitId,
1054
- message: `Missing tasks directory for ${unitId}`,
1101
+ message: slice.done
1102
+ ? `Missing tasks directory for ${unitId} (slice is complete — cosmetic only)`
1103
+ : `Missing tasks directory for ${unitId}`,
1055
1104
  file: relSlicePath(basePath, milestoneId, slice.id),
1056
1105
  fixable: true,
1057
1106
  });
@@ -1065,15 +1114,17 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
1065
1114
  const planContent = planPath ? await loadFile(planPath) : null;
1066
1115
  const plan = planContent ? parsePlan(planContent) : null;
1067
1116
  if (!plan) {
1068
- issues.push({
1069
- severity: "warning",
1070
- code: "missing_slice_plan",
1071
- scope: "slice",
1072
- unitId,
1073
- message: `Slice ${unitId} has no plan file`,
1074
- file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
1075
- fixable: false,
1076
- });
1117
+ if (!slice.done) {
1118
+ issues.push({
1119
+ severity: "warning",
1120
+ code: "missing_slice_plan",
1121
+ scope: "slice",
1122
+ unitId,
1123
+ message: `Slice ${unitId} has no plan file`,
1124
+ file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
1125
+ fixable: false,
1126
+ });
1127
+ }
1077
1128
  continue;
1078
1129
  }
1079
1130
 
@@ -1236,6 +1287,19 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
1236
1287
  }
1237
1288
  }
1238
1289
 
1290
+ // Milestone-level check: all slices done but no validation file
1291
+ if (isMilestoneComplete(roadmap) && !resolveMilestoneFile(basePath, milestoneId, "VALIDATION") && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
1292
+ issues.push({
1293
+ severity: "info",
1294
+ code: "all_slices_done_missing_milestone_validation",
1295
+ scope: "milestone",
1296
+ unitId: milestoneId,
1297
+ message: `All slices are done but ${milestoneId}-VALIDATION.md is missing — milestone is in validating-milestone phase`,
1298
+ file: relMilestoneFile(basePath, milestoneId, "VALIDATION"),
1299
+ fixable: false,
1300
+ });
1301
+ }
1302
+
1239
1303
  // Milestone-level check: all slices done but no milestone summary
1240
1304
  if (isMilestoneComplete(roadmap) && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
1241
1305
  issues.push({
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
2
 
3
- type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI) => Promise<void>;
3
+ type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI, reason?: string) => Promise<void>;
4
4
 
5
5
  export function registerExitCommand(
6
6
  pi: ExtensionAPI,
@@ -11,7 +11,7 @@ export function registerExitCommand(
11
11
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
12
12
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
13
13
  const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
14
- await stopAuto(ctx, pi);
14
+ await stopAuto(ctx, pi, "Graceful exit");
15
15
  ctx.shutdown();
16
16
  },
17
17
  });
@@ -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) {
@@ -19,6 +19,7 @@ const GSD_RUNTIME_PATTERNS = [
19
19
  ".gsd/forensics/",
20
20
  ".gsd/runtime/",
21
21
  ".gsd/worktrees/",
22
+ ".gsd/parallel/",
22
23
  ".gsd/auto.lock",
23
24
  ".gsd/metrics.json",
24
25
  ".gsd/completed-units.json",