gsd-pi 2.47.0 → 2.48.0-dev.ced2eca

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 (200) hide show
  1. package/dist/resources/extensions/gsd/auto-dispatch.js +17 -2
  2. package/dist/resources/extensions/gsd/auto-post-unit.js +17 -3
  3. package/dist/resources/extensions/gsd/auto-start.js +8 -1
  4. package/dist/resources/extensions/gsd/auto-worktree.js +5 -2
  5. package/dist/resources/extensions/gsd/commands/handlers/auto.js +43 -3
  6. package/dist/resources/extensions/gsd/forensics.js +292 -1
  7. package/dist/resources/extensions/gsd/git-service.js +11 -10
  8. package/dist/resources/extensions/gsd/guided-flow.js +85 -3
  9. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +223 -56
  10. package/dist/resources/extensions/gsd/prompts/forensics.md +37 -5
  11. package/dist/resources/extensions/gsd/prompts/run-uat.md +4 -4
  12. package/dist/resources/extensions/gsd/session-forensics.js +10 -1
  13. package/dist/resources/extensions/gsd/worktree-command.js +1 -1
  14. package/dist/web/standalone/.next/BUILD_ID +1 -1
  15. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  16. package/dist/web/standalone/.next/build-manifest.json +3 -3
  17. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  18. package/dist/web/standalone/.next/required-server-files.json +3 -3
  19. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  20. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  22. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  30. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  40. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  42. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  44. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  46. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
  84. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  90. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  104. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  106. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  108. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  110. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/index.html +1 -1
  120. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  121. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  122. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  123. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  125. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  126. package/dist/web/standalone/.next/server/app/page.js +2 -2
  127. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  129. package/dist/web/standalone/.next/server/chunks/229.js +1 -1
  130. package/dist/web/standalone/.next/server/chunks/471.js +3 -3
  131. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/middleware.js +2 -2
  133. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  134. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  135. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  136. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  137. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  138. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  139. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  140. package/dist/web/standalone/.next/static/chunks/app/page-6654a8cca61a3d1c.js +1 -0
  141. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  142. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  143. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  144. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  145. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  146. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  147. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  148. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  149. package/dist/web/standalone/server.js +1 -1
  150. package/dist/worktree-cli.js +1 -1
  151. package/package.json +1 -1
  152. package/packages/pi-agent-core/dist/agent-loop.js +3 -2
  153. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  154. package/packages/pi-agent-core/src/agent-loop.ts +3 -2
  155. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +43 -0
  156. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  158. package/packages/pi-coding-agent/dist/core/model-registry.js +26 -3
  159. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  160. package/packages/pi-coding-agent/package.json +1 -1
  161. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +70 -0
  162. package/packages/pi-coding-agent/src/core/model-registry.ts +29 -2
  163. package/packages/pi-tui/dist/components/box.d.ts +1 -0
  164. package/packages/pi-tui/dist/components/box.d.ts.map +1 -1
  165. package/packages/pi-tui/dist/components/box.js +10 -0
  166. package/packages/pi-tui/dist/components/box.js.map +1 -1
  167. package/packages/pi-tui/src/components/box.ts +10 -0
  168. package/pkg/package.json +1 -1
  169. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +8 -4
  170. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -1
  171. package/src/resources/extensions/gsd/auto-post-unit.ts +18 -3
  172. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  173. package/src/resources/extensions/gsd/auto-worktree.ts +4 -2
  174. package/src/resources/extensions/gsd/commands/handlers/auto.ts +46 -3
  175. package/src/resources/extensions/gsd/forensics.ts +329 -2
  176. package/src/resources/extensions/gsd/git-service.ts +12 -11
  177. package/src/resources/extensions/gsd/guided-flow.ts +105 -3
  178. package/src/resources/extensions/gsd/prompts/discuss-headless.md +223 -56
  179. package/src/resources/extensions/gsd/prompts/forensics.md +37 -5
  180. package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
  181. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  182. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +2 -2
  183. package/src/resources/extensions/gsd/tests/auto-stash-merge.test.ts +1 -1
  184. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +14 -12
  185. package/src/resources/extensions/gsd/tests/discuss-queued-milestones.test.ts +241 -0
  186. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +1 -1
  187. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +121 -0
  188. package/src/resources/extensions/gsd/tests/forensics-journal.test.ts +162 -0
  189. package/src/resources/extensions/gsd/tests/git-service.test.ts +19 -9
  190. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +2 -2
  191. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +6 -6
  192. package/src/resources/extensions/gsd/tests/preflight-context-draft-filter.test.ts +115 -0
  193. package/src/resources/extensions/gsd/tests/run-uat.test.ts +68 -0
  194. package/src/resources/extensions/gsd/tests/stale-milestone-id-reservation.test.ts +79 -0
  195. package/src/resources/extensions/gsd/worktree-command.ts +1 -1
  196. package/dist/web/standalone/.next/static/chunks/app/page-12dd5ece0df4badc.js +0 -1
  197. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  198. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  199. /package/dist/web/standalone/.next/static/{VPcLnRF4BL8VoJEilBwlB → PTL5V00OW8q4-092tUQKx}/_buildManifest.js +0 -0
  200. /package/dist/web/standalone/.next/static/{VPcLnRF4BL8VoJEilBwlB → PTL5V00OW8q4-092tUQKx}/_ssgManifest.js +0 -0
@@ -160,15 +160,17 @@ describe("auto-worktree-milestone-merge", () => {
160
160
 
161
161
  const result = mergeMilestoneToMain(repo, "M020", roadmap);
162
162
 
163
- assert.match(result.commitMessage, /^feat\(M020\):/, "subject has conventional commit prefix");
163
+ assert.match(result.commitMessage, /^feat:/, "subject has conventional commit prefix without milestone ID");
164
164
  assert.ok(result.commitMessage.includes("Backend foundation"), "subject includes milestone title");
165
165
  assert.ok(result.commitMessage.includes("- S01: Core API"), "body lists S01");
166
166
  assert.ok(result.commitMessage.includes("- S02: Error handling"), "body lists S02");
167
167
  assert.ok(result.commitMessage.includes("- S03: Logging infra"), "body lists S03");
168
+ assert.ok(result.commitMessage.includes("GSD-Milestone: M020"), "body has GSD-Milestone trailer");
168
169
  assert.ok(result.commitMessage.includes("Branch: milestone/M020"), "body has branch metadata");
169
170
 
170
171
  const gitMsg = run("git log -1 --format=%B main", repo).trim();
171
- assert.match(gitMsg, /^feat\(M020\):/, "git commit message starts with feat(M020):");
172
+ assert.match(gitMsg, /^feat:/, "git commit message starts with feat:");
173
+ assert.ok(gitMsg.includes("GSD-Milestone: M020"), "git commit has GSD-Milestone trailer");
172
174
  assert.ok(gitMsg.includes("- S01: Core API"), "git commit body has S01");
173
175
  });
174
176
 
@@ -213,11 +215,11 @@ describe("auto-worktree-milestone-merge", () => {
213
215
  const result = mergeMilestoneToMain(repo, "M040", roadmap);
214
216
 
215
217
  const mainLog = run("git log --oneline main", repo);
216
- assert.ok(mainLog.includes("feat(M040)"), "milestone commit on main");
218
+ assert.ok(mainLog.includes("feat:"), "milestone commit on main");
217
219
 
218
220
  run("git push origin main", repo);
219
221
  const remoteLog = run("git log --oneline main", bareDir);
220
- assert.ok(remoteLog.includes("feat(M040)"), "milestone commit reachable on remote after manual push");
222
+ assert.ok(remoteLog.includes("feat:"), "milestone commit reachable on remote after manual push");
221
223
 
222
224
  assert.strictEqual(typeof result.pushed, "boolean", "pushed flag remains boolean");
223
225
  });
@@ -248,7 +250,7 @@ describe("auto-worktree-milestone-merge", () => {
248
250
  let threw = false;
249
251
  try {
250
252
  const result = mergeMilestoneToMain(repo, "M050", roadmap);
251
- assert.ok(result.commitMessage.includes("feat(M050)"), "merge commit created despite .gsd conflict");
253
+ assert.ok(result.commitMessage.includes("feat:") && result.commitMessage.includes("GSD-Milestone: M050"), "merge commit created despite .gsd conflict");
252
254
  } catch (err) {
253
255
  threw = true;
254
256
  }
@@ -274,7 +276,7 @@ describe("auto-worktree-milestone-merge", () => {
274
276
  let threw = false;
275
277
  try {
276
278
  const result = mergeMilestoneToMain(repo, "M060", roadmap);
277
- assert.ok(result.commitMessage.includes("feat(M060)"), "merge commit created");
279
+ assert.ok(result.commitMessage.includes("feat:") && result.commitMessage.includes("GSD-Milestone: M060"), "merge commit created");
278
280
  } catch (err) {
279
281
  threw = true;
280
282
  }
@@ -312,7 +314,7 @@ describe("auto-worktree-milestone-merge", () => {
312
314
  let errMsg = "";
313
315
  try {
314
316
  const result = mergeMilestoneToMain(dir, "M070", roadmap);
315
- assert.ok(result.commitMessage.includes("feat(M070)"), "merge commit created on master");
317
+ assert.ok(result.commitMessage.includes("feat:") && result.commitMessage.includes("GSD-Milestone: M070"), "merge commit created on master");
316
318
  } catch (err) {
317
319
  threw = true;
318
320
  errMsg = err instanceof Error ? err.message : String(err);
@@ -392,7 +394,7 @@ describe("auto-worktree-milestone-merge", () => {
392
394
  let threw = false;
393
395
  try {
394
396
  const result = mergeMilestoneToMain(repo, "M090", roadmap);
395
- assert.ok(result.commitMessage.includes("feat(M090)"), "#1738 merge succeeds after cleaning synced dirs");
397
+ assert.ok(result.commitMessage.includes("feat:") && result.commitMessage.includes("GSD-Milestone: M090"), "#1738 merge succeeds after cleaning synced dirs");
396
398
  } catch (err: unknown) {
397
399
  threw = true;
398
400
  }
@@ -419,7 +421,7 @@ describe("auto-worktree-milestone-merge", () => {
419
421
  let threw = false;
420
422
  try {
421
423
  const result = mergeMilestoneToMain(repo, "M100", roadmap);
422
- assert.ok(result.commitMessage.includes("feat(M100)"), "#2151: merge succeeds after stashing dirty files");
424
+ assert.ok(result.commitMessage.includes("feat:") && result.commitMessage.includes("GSD-Milestone: M100"), "#2151: merge succeeds after stashing dirty files");
423
425
  } catch {
424
426
  threw = true;
425
427
  }
@@ -519,7 +521,7 @@ describe("auto-worktree-milestone-merge", () => {
519
521
  let errMsg = "";
520
522
  try {
521
523
  const result = mergeMilestoneToMain(repo, "M140", roadmap);
522
- assert.ok(result.commitMessage.includes("feat(M140)"), "merge commit created");
524
+ assert.ok(result.commitMessage.includes("feat:") && result.commitMessage.includes("GSD-Milestone: M140"), "merge commit created");
523
525
  } catch (err) {
524
526
  threw = true;
525
527
  errMsg = err instanceof Error ? err.message : String(err);
@@ -589,7 +591,7 @@ describe("auto-worktree-milestone-merge", () => {
589
591
  assert.ok(existsSync(squashMsgPath), "SQUASH_MSG planted before merge");
590
592
 
591
593
  const result = mergeMilestoneToMain(repo, "M160", roadmap);
592
- assert.ok(result.commitMessage.includes("feat(M160)"), "merge commit created");
594
+ assert.ok(result.commitMessage.includes("feat:") && result.commitMessage.includes("GSD-Milestone: M160"), "merge commit created");
593
595
 
594
596
  assert.ok(!existsSync(squashMsgPath), "#1853: SQUASH_MSG must not persist after successful squash-merge");
595
597
  });
@@ -609,7 +611,7 @@ describe("auto-worktree-milestone-merge", () => {
609
611
  ]);
610
612
 
611
613
  const result = mergeMilestoneToMain(repo, "M170", roadmap);
612
- assert.ok(result.commitMessage.includes("feat(M170)"), "merge commit created");
614
+ assert.ok(result.commitMessage.includes("feat:") && result.commitMessage.includes("GSD-Milestone: M170"), "merge commit created");
613
615
 
614
616
  assert.ok(
615
617
  existsSync(join(repo, "uncommitted-agent-code.ts")),
@@ -0,0 +1,241 @@
1
+ /**
2
+ * discuss-queued-milestones.test.ts — Tests for #2307.
3
+ *
4
+ * /gsd discuss was previously gated on state.activeMilestone, which prevented
5
+ * users from discussing queued (pending) milestones during roadmap grooming.
6
+ *
7
+ * These tests verify:
8
+ * 1. deriveState correctly identifies pending milestones (the set the picker
9
+ * will show when no active milestone is present)
10
+ * 2. resolveMilestoneFile correctly resolves context artifacts for pending
11
+ * milestones so the picker can report their discussion state
12
+ * 3. The guided-flow.ts source code no longer hard-exits when no active
13
+ * milestone exists but pending milestones are present
14
+ * 4. The helper functions for queued discuss exist in the source
15
+ */
16
+
17
+ import { describe, test, afterEach } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { tmpdir } from "node:os";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname } from "node:path";
24
+
25
+ import { deriveState } from "../state.ts";
26
+ import { invalidateAllCaches } from "../cache.ts";
27
+ import { resolveMilestoneFile } from "../paths.ts";
28
+
29
+ // ─── Fixture Helpers ──────────────────────────────────────────────────────────
30
+
31
+ function createBase(): string {
32
+ const base = mkdtempSync(join(tmpdir(), "gsd-discuss-queued-"));
33
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
34
+ return base;
35
+ }
36
+
37
+ function cleanup(base: string): void {
38
+ rmSync(base, { recursive: true, force: true });
39
+ }
40
+
41
+ function writeMilestoneDir(base: string, mid: string): void {
42
+ mkdirSync(join(base, ".gsd", "milestones", mid), { recursive: true });
43
+ }
44
+
45
+ function writeContext(base: string, mid: string, content: string): void {
46
+ writeMilestoneDir(base, mid);
47
+ writeFileSync(join(base, ".gsd", "milestones", mid, `${mid}-CONTEXT.md`), content);
48
+ }
49
+
50
+ function writeContextDraft(base: string, mid: string, content: string): void {
51
+ writeMilestoneDir(base, mid);
52
+ writeFileSync(join(base, ".gsd", "milestones", mid, `${mid}-CONTEXT-DRAFT.md`), content);
53
+ }
54
+
55
+ function writeRoadmap(base: string, mid: string, content: string): void {
56
+ writeMilestoneDir(base, mid);
57
+ writeFileSync(join(base, ".gsd", "milestones", mid, `${mid}-ROADMAP.md`), content);
58
+ }
59
+
60
+ function readGuidedFlowSource(): string {
61
+ const thisFile = fileURLToPath(import.meta.url);
62
+ const thisDir = dirname(thisFile);
63
+ return readFileSync(join(thisDir, "..", "guided-flow.ts"), "utf-8");
64
+ }
65
+
66
+ // ─── Tests ────────────────────────────────────────────────────────────────────
67
+
68
+ describe("discuss-queued-milestones (#2307)", () => {
69
+
70
+ test("1. pending milestones appear in registry when active milestone exists", async () => {
71
+ const base = createBase();
72
+ try {
73
+ // M001: active — has context + roadmap with a slice
74
+ writeContext(base, "M001", "# M001: Active\nContext here.");
75
+ writeRoadmap(base, "M001",
76
+ "# M001: Active\n\n## Slices\n- [ ] **S01: Do work** `risk:low` `depends:[]`\n > After this: works\n");
77
+
78
+ // M002: pending — context only, no roadmap
79
+ writeContext(base, "M002", "# M002: Queued\nFuture work.");
80
+
81
+ // M003: pending — draft context only
82
+ writeContextDraft(base, "M003", "# M003: Draft\nSeed material.");
83
+
84
+ invalidateAllCaches();
85
+ const state = await deriveState(base);
86
+
87
+ assert.ok(!!state.activeMilestone, "M001 should be the active milestone");
88
+ assert.strictEqual(state.activeMilestone?.id, "M001");
89
+
90
+ const pendingIds = state.registry
91
+ .filter(m => m.status === "pending")
92
+ .map(m => m.id);
93
+
94
+ assert.ok(pendingIds.includes("M002"), "M002 should be pending");
95
+ assert.ok(pendingIds.includes("M003"), "M003 should be pending");
96
+ } finally {
97
+ cleanup(base);
98
+ }
99
+ });
100
+
101
+ test("2. first context-only milestone is active, subsequent ones are pending", async () => {
102
+ const base = createBase();
103
+ try {
104
+ // M001: first milestone with context but no roadmap — deriveState marks it active
105
+ writeContext(base, "M001", "# M001: First\nContext here.");
106
+ // M002: will be pending since M001 is active
107
+ writeContext(base, "M002", "# M002: Second\nMore future work.");
108
+
109
+ invalidateAllCaches();
110
+ const state = await deriveState(base);
111
+
112
+ // deriveState makes the first unfinished milestone "active" even without a roadmap
113
+ assert.ok(!!state.activeMilestone, "first milestone should be active");
114
+ assert.strictEqual(state.activeMilestone?.id, "M001", "M001 is the active milestone");
115
+
116
+ const pendingIds = state.registry
117
+ .filter(m => m.status === "pending")
118
+ .map(m => m.id);
119
+
120
+ assert.ok(pendingIds.includes("M002"),
121
+ "M002 should be pending — it comes after the active M001");
122
+ } finally {
123
+ cleanup(base);
124
+ }
125
+ });
126
+
127
+ test("3. resolveMilestoneFile finds CONTEXT.md for pending milestone", (t) => {
128
+ const base = createBase();
129
+ try {
130
+ writeContext(base, "M002", "# M002: Queued\nContent.");
131
+
132
+ const contextFile = resolveMilestoneFile(base, "M002", "CONTEXT");
133
+ assert.ok(contextFile !== null, "resolveMilestoneFile should find CONTEXT.md for M002");
134
+ assert.ok(contextFile!.endsWith("M002-CONTEXT.md"),
135
+ "resolved path should point to M002-CONTEXT.md");
136
+ } finally {
137
+ cleanup(base);
138
+ }
139
+ });
140
+
141
+ test("4. resolveMilestoneFile finds CONTEXT-DRAFT.md for pending milestone", (t) => {
142
+ const base = createBase();
143
+ try {
144
+ writeContextDraft(base, "M003", "# M003: Draft\nSeed content.");
145
+
146
+ const draftFile = resolveMilestoneFile(base, "M003", "CONTEXT-DRAFT");
147
+ assert.ok(draftFile !== null, "resolveMilestoneFile should find CONTEXT-DRAFT.md for M003");
148
+ assert.ok(draftFile!.endsWith("M003-CONTEXT-DRAFT.md"),
149
+ "resolved path should point to M003-CONTEXT-DRAFT.md");
150
+ } finally {
151
+ cleanup(base);
152
+ }
153
+ });
154
+
155
+ test("5. resolveMilestoneFile returns null when pending milestone has no context", (t) => {
156
+ const base = createBase();
157
+ try {
158
+ writeMilestoneDir(base, "M004");
159
+
160
+ const contextFile = resolveMilestoneFile(base, "M004", "CONTEXT");
161
+ assert.strictEqual(contextFile, null,
162
+ "resolveMilestoneFile should return null when no CONTEXT.md exists");
163
+
164
+ const draftFile = resolveMilestoneFile(base, "M004", "CONTEXT-DRAFT");
165
+ assert.strictEqual(draftFile, null,
166
+ "resolveMilestoneFile should return null when no CONTEXT-DRAFT.md exists");
167
+ } finally {
168
+ cleanup(base);
169
+ }
170
+ });
171
+
172
+ test("6. guided-flow no longer hard-exits when no active milestone but pending exist", () => {
173
+ const source = readGuidedFlowSource();
174
+
175
+ // The old guard was a simple early-exit:
176
+ // if (!state.activeMilestone) {
177
+ // ctx.ui.notify("No active milestone. Run /gsd to create one first.", "warning");
178
+ // return;
179
+ // }
180
+ //
181
+ // The new guard should check for pending milestones and route instead.
182
+ const oldGuardPattern = /if\s*\(!state\.activeMilestone\)\s*\{\s*ctx\.ui\.notify\("No active milestone/;
183
+ assert.ok(
184
+ !oldGuardPattern.test(source),
185
+ "guided-flow must not unconditionally exit when activeMilestone is null",
186
+ );
187
+ });
188
+
189
+ test("7. showDiscussQueuedMilestone helper exists in guided-flow", () => {
190
+ const source = readGuidedFlowSource();
191
+ assert.ok(
192
+ source.includes("showDiscussQueuedMilestone"),
193
+ "guided-flow must export showDiscussQueuedMilestone helper",
194
+ );
195
+ });
196
+
197
+ test("8. dispatchDiscussForMilestone helper exists in guided-flow", () => {
198
+ const source = readGuidedFlowSource();
199
+ assert.ok(
200
+ source.includes("dispatchDiscussForMilestone"),
201
+ "guided-flow must export dispatchDiscussForMilestone helper",
202
+ );
203
+ });
204
+
205
+ test("9. dispatchDiscussForMilestone does not set pendingAutoStart", () => {
206
+ const source = readGuidedFlowSource();
207
+
208
+ // Extract the dispatchDiscussForMilestone function body
209
+ const fnMatch = source.match(
210
+ /async function dispatchDiscussForMilestone\s*\([^)]*\)[^{]*\{([\s\S]*?)\n\}/,
211
+ );
212
+ assert.ok(!!fnMatch, "dispatchDiscussForMilestone function body must be present");
213
+
214
+ if (fnMatch) {
215
+ assert.ok(
216
+ !fnMatch[1].includes("pendingAutoStart"),
217
+ "dispatchDiscussForMilestone must NOT set pendingAutoStart — discussing a queued milestone must not activate it",
218
+ );
219
+ }
220
+ });
221
+
222
+ test("10. slice picker includes queued milestone option when pending milestones exist", () => {
223
+ const source = readGuidedFlowSource();
224
+ assert.ok(
225
+ source.includes("discuss_queued_milestone"),
226
+ "slice picker must include a 'discuss_queued_milestone' action id for queued milestones",
227
+ );
228
+ assert.ok(
229
+ source.includes("Discuss a queued milestone"),
230
+ "slice picker must label the queued milestone action clearly",
231
+ );
232
+ });
233
+
234
+ test("11. queued milestone picker labels entries with [queued]", () => {
235
+ const source = readGuidedFlowSource();
236
+ assert.ok(
237
+ source.includes("[queued]"),
238
+ "queued milestone picker must label entries with [queued] to distinguish from active",
239
+ );
240
+ });
241
+ });
@@ -252,7 +252,7 @@ describe('feature-branch-lifecycle-integration', async () => {
252
252
  // Exactly one new commit on feature branch (the squash merge)
253
253
  const featureLog = run(`git log --oneline ${featureBranch}`, repo);
254
254
  assert.ok(
255
- featureLog.includes(`feat(${milestoneId})`),
255
+ featureLog.includes("feat:"),
256
256
  "feature branch has milestone merge commit",
257
257
  );
258
258
 
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Regression test for #2539: extractTrace should not count benign bash
3
+ * exit-code-1 (grep no-match) or user skips as errors.
4
+ */
5
+ import { describe, test } from "node:test";
6
+ import assert from "node:assert/strict";
7
+
8
+ import { extractTrace } from "../session-forensics.ts";
9
+
10
+ /**
11
+ * Build a minimal JSONL entry pair: assistant tool_use → toolResult.
12
+ * This is the shape extractTrace() expects from session activity files.
13
+ */
14
+ function makeToolPair(
15
+ toolName: string,
16
+ input: Record<string, unknown>,
17
+ resultText: string,
18
+ isError: boolean,
19
+ ): unknown[] {
20
+ const toolCallId = `toolu_${Math.random().toString(36).slice(2, 10)}`;
21
+ return [
22
+ {
23
+ type: "message",
24
+ message: {
25
+ role: "assistant",
26
+ content: [
27
+ {
28
+ type: "toolCall",
29
+ id: toolCallId,
30
+ name: toolName,
31
+ arguments: input,
32
+ },
33
+ ],
34
+ },
35
+ },
36
+ {
37
+ type: "message",
38
+ message: {
39
+ role: "toolResult",
40
+ toolCallId,
41
+ toolName,
42
+ isError,
43
+ content: [{ type: "text", text: resultText }],
44
+ },
45
+ },
46
+ ];
47
+ }
48
+
49
+ describe("extractTrace error filtering (#2539)", () => {
50
+ test("grep exit-code-1 (no matches) is not counted as an error", () => {
51
+ const entries = makeToolPair(
52
+ "bash",
53
+ { command: "grep -rn 'nonexistent' src/" },
54
+ "(no output)\nCommand exited with code 1",
55
+ true,
56
+ );
57
+ const trace = extractTrace(entries);
58
+ assert.equal(trace.errors.length, 0, "grep no-match should not be an error");
59
+ });
60
+
61
+ test("user skip is not counted as an error", () => {
62
+ const entries = makeToolPair(
63
+ "bash",
64
+ { command: "npm run test" },
65
+ "Skipped due to queued user message",
66
+ true,
67
+ );
68
+ const trace = extractTrace(entries);
69
+ assert.equal(trace.errors.length, 0, "user skip should not be an error");
70
+ });
71
+
72
+ test("real bash error is still counted", () => {
73
+ const entries = makeToolPair(
74
+ "bash",
75
+ { command: "cat /nonexistent" },
76
+ "cat: /nonexistent: No such file or directory\nCommand exited with code 1",
77
+ true,
78
+ );
79
+ const trace = extractTrace(entries);
80
+ assert.equal(trace.errors.length, 1, "real error should still be counted");
81
+ assert.match(trace.errors[0], /No such file or directory/);
82
+ });
83
+
84
+ test("non-bash tool error is still counted", () => {
85
+ const entries = makeToolPair(
86
+ "edit",
87
+ { path: "foo.ts", oldText: "x", newText: "y" },
88
+ "oldText not found in file",
89
+ true,
90
+ );
91
+ const trace = extractTrace(entries);
92
+ assert.equal(trace.errors.length, 1, "non-bash tool errors should still be counted");
93
+ });
94
+
95
+ test("mixed entries: only real errors are counted", () => {
96
+ const entries = [
97
+ // benign grep no-match
98
+ ...makeToolPair("bash", { command: "grep -rn 'pattern' src/" }, "(no output)\nCommand exited with code 1", true),
99
+ // user skip
100
+ ...makeToolPair("bash", { command: "npm test" }, "Skipped due to queued user message", true),
101
+ // real error
102
+ ...makeToolPair("bash", { command: "node broken.js" }, "SyntaxError: Unexpected token\nCommand exited with code 1", true),
103
+ // successful command (not an error)
104
+ ...makeToolPair("bash", { command: "echo hello" }, "hello", false),
105
+ ];
106
+ const trace = extractTrace(entries);
107
+ assert.equal(trace.errors.length, 1, "only the real error should be counted");
108
+ assert.match(trace.errors[0], /SyntaxError/);
109
+ });
110
+
111
+ test("exit code 1 with actual output is still an error", () => {
112
+ const entries = makeToolPair(
113
+ "bash",
114
+ { command: "npm run lint" },
115
+ "src/foo.ts:10:5 - error TS2304: Cannot find name 'x'\nCommand exited with code 1",
116
+ true,
117
+ );
118
+ const trace = extractTrace(entries);
119
+ assert.equal(trace.errors.length, 1, "lint error with output should be counted");
120
+ });
121
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const gsdDir = join(__dirname, "..");
9
+
10
+ describe("forensics journal & activity log awareness", () => {
11
+ const forensicsSrc = readFileSync(join(gsdDir, "forensics.ts"), "utf-8");
12
+ const promptSrc = readFileSync(join(gsdDir, "prompts", "forensics.md"), "utf-8");
13
+
14
+ it("scanJournalForForensics reads journal files directly (no full queryJournal load)", () => {
15
+ // Must NOT use queryJournal which loads ALL entries into memory
16
+ assert.ok(
17
+ !forensicsSrc.includes('queryJournal('),
18
+ "forensics.ts must NOT call queryJournal() which loads all entries at once",
19
+ );
20
+ // Must have its own journal scanning with file-level limits
21
+ assert.ok(
22
+ forensicsSrc.includes("scanJournalForForensics"),
23
+ "forensics.ts must have scanJournalForForensics function",
24
+ );
25
+ });
26
+
27
+ it("journal scanning limits files parsed to avoid memory bloat", () => {
28
+ assert.ok(
29
+ forensicsSrc.includes("MAX_JOURNAL_RECENT_FILES"),
30
+ "must have MAX_JOURNAL_RECENT_FILES constant to limit parsed files",
31
+ );
32
+ assert.ok(
33
+ forensicsSrc.includes("MAX_JOURNAL_RECENT_EVENTS"),
34
+ "must have MAX_JOURNAL_RECENT_EVENTS constant to limit events extracted",
35
+ );
36
+ });
37
+
38
+ it("older journal files are line-counted without full JSON parse", () => {
39
+ assert.ok(
40
+ forensicsSrc.includes("olderEntryCount") || forensicsSrc.includes("olderFiles"),
41
+ "must handle older files separately from recent files",
42
+ );
43
+ });
44
+
45
+ it("ForensicReport includes journalSummary field", () => {
46
+ assert.ok(
47
+ forensicsSrc.includes("journalSummary"),
48
+ "ForensicReport must include journalSummary field",
49
+ );
50
+ });
51
+
52
+ it("ForensicReport includes activityLogMeta field", () => {
53
+ assert.ok(
54
+ forensicsSrc.includes("activityLogMeta"),
55
+ "ForensicReport must include activityLogMeta field",
56
+ );
57
+ });
58
+
59
+ it("buildForensicReport calls scanJournalForForensics", () => {
60
+ assert.ok(
61
+ forensicsSrc.includes("scanJournalForForensics"),
62
+ "buildForensicReport must call scanJournalForForensics",
63
+ );
64
+ });
65
+
66
+ it("buildForensicReport calls gatherActivityLogMeta", () => {
67
+ assert.ok(
68
+ forensicsSrc.includes("gatherActivityLogMeta"),
69
+ "buildForensicReport must call gatherActivityLogMeta",
70
+ );
71
+ });
72
+
73
+ it("forensics detects journal-based anomalies", () => {
74
+ assert.ok(
75
+ forensicsSrc.includes("detectJournalAnomalies"),
76
+ "forensics.ts must have detectJournalAnomalies function",
77
+ );
78
+ // Check for specific journal anomaly types
79
+ assert.ok(forensicsSrc.includes('"journal-stuck"'), "must detect journal-stuck anomalies");
80
+ assert.ok(forensicsSrc.includes('"journal-guard-block"'), "must detect journal-guard-block anomalies");
81
+ assert.ok(forensicsSrc.includes('"journal-rapid-iterations"'), "must detect journal-rapid-iterations anomalies");
82
+ assert.ok(forensicsSrc.includes('"journal-worktree-failure"'), "must detect journal-worktree-failure anomalies");
83
+ });
84
+
85
+ it("formatReportForPrompt includes journal summary section", () => {
86
+ assert.ok(
87
+ forensicsSrc.includes("Journal Summary"),
88
+ "prompt formatter must include a Journal Summary section",
89
+ );
90
+ });
91
+
92
+ it("formatReportForPrompt includes activity log overview section", () => {
93
+ assert.ok(
94
+ forensicsSrc.includes("Activity Log Overview"),
95
+ "prompt formatter must include an Activity Log Overview section",
96
+ );
97
+ });
98
+
99
+ it("activity log scanning uses tail-read with byte cap (not full file load)", () => {
100
+ // scanActivityLogs uses nativeParseJsonlTail + MAX_JSONL_BYTES for efficient reading
101
+ assert.ok(
102
+ forensicsSrc.includes("nativeParseJsonlTail"),
103
+ "activity log scanning must use nativeParseJsonlTail for tail-reading",
104
+ );
105
+ assert.ok(
106
+ forensicsSrc.includes("MAX_JSONL_BYTES"),
107
+ "activity log scanning must respect MAX_JSONL_BYTES cap",
108
+ );
109
+ // Only reads last 5 files
110
+ assert.ok(
111
+ forensicsSrc.includes("slice(-5)"),
112
+ "activity log scanning must limit to last 5 files",
113
+ );
114
+ });
115
+
116
+ it("activity log entries are distilled through extractTrace, not sent raw", () => {
117
+ assert.ok(
118
+ forensicsSrc.includes("extractTrace("),
119
+ "activity log entries must be distilled through extractTrace before reporting",
120
+ );
121
+ });
122
+
123
+ it("prompt output is hard-capped at 30KB", () => {
124
+ assert.ok(
125
+ forensicsSrc.includes("MAX_BYTES") && forensicsSrc.includes("30 * 1024"),
126
+ "formatReportForPrompt must have a 30KB hard cap",
127
+ );
128
+ assert.ok(
129
+ forensicsSrc.includes("truncated at 30KB"),
130
+ "prompt must show truncation message when capped",
131
+ );
132
+ });
133
+
134
+ it("forensics prompt documents journal format", () => {
135
+ assert.ok(
136
+ promptSrc.includes("### Journal Format"),
137
+ "forensics.md must document the journal format",
138
+ );
139
+ assert.ok(
140
+ promptSrc.includes("flowId"),
141
+ "forensics.md must reference flowId concept",
142
+ );
143
+ assert.ok(
144
+ promptSrc.includes("causedBy"),
145
+ "forensics.md must reference causedBy for causal chains",
146
+ );
147
+ });
148
+
149
+ it("forensics prompt includes journal directory in runtime path reference", () => {
150
+ assert.ok(
151
+ promptSrc.includes("journal/"),
152
+ "forensics.md runtime path reference must include journal/",
153
+ );
154
+ });
155
+
156
+ it("investigation protocol references journal data", () => {
157
+ assert.ok(
158
+ promptSrc.includes("journal timeline") || promptSrc.includes("journal events"),
159
+ "investigation protocol must reference journal data for tracing",
160
+ );
161
+ });
162
+ });