@opengsd/gsd-pi 1.6.0 → 1.7.0-dev.30c4ec2f

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 (229) hide show
  1. package/README.md +6 -10
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-post-unit.js +19 -3
  4. package/dist/resources/extensions/gsd/auto-prompts.js +20 -0
  5. package/dist/resources/extensions/gsd/auto.js +12 -0
  6. package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +68 -2
  7. package/dist/resources/extensions/gsd/doctor-history.js +65 -0
  8. package/dist/resources/extensions/gsd/doctor-state-checks.js +509 -0
  9. package/dist/resources/extensions/gsd/doctor-workspace-checks.js +91 -0
  10. package/dist/resources/extensions/gsd/doctor.js +14 -653
  11. package/dist/resources/extensions/gsd/md-importer.js +7 -2
  12. package/dist/resources/extensions/gsd/migrate/planning-writer.js +8 -2
  13. package/dist/resources/extensions/gsd/migration-auto-check.js +4 -0
  14. package/dist/resources/extensions/gsd/reopen-reason.js +98 -0
  15. package/dist/resources/extensions/gsd/session-status-io.js +31 -0
  16. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +54 -14
  17. package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +48 -5
  18. package/dist/resources/extensions/gsd/tools/reopen-task.js +15 -0
  19. package/dist/resources/extensions/shared/gsd-browser-cli.js +22 -5
  20. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  21. package/dist/web/standalone/.next/BUILD_ID +1 -1
  22. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  23. package/dist/web/standalone/.next/build-manifest.json +3 -3
  24. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  25. package/dist/web/standalone/.next/required-server-files.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  37. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  65. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  87. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  97. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  103. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  119. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  123. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/app/index.html +1 -1
  133. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  134. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  135. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  136. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  137. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  138. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  139. package/dist/web/standalone/.next/server/app/page.js +2 -2
  140. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  142. package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
  143. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  144. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/middleware.js +3 -3
  146. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  147. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  148. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  149. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +1 -0
  150. package/dist/web/standalone/.next/static/chunks/app/{layout-b35cbfff38aaf4cf.js → layout-4ae2d68984392bbf.js} +1 -1
  151. package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +1 -0
  152. package/dist/web/standalone/.next/static/chunks/{main-app-590a74400e35f685.js → main-app-90d1d8d5e5d2dc6b.js} +1 -1
  153. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +1 -0
  154. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  155. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  156. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  157. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  158. package/dist/web/standalone/server.js +1 -1
  159. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  160. package/integrations/hermes/pyproject.toml +1 -1
  161. package/package.json +6 -6
  162. package/packages/cloud-mcp-gateway/package.json +2 -2
  163. package/packages/contracts/package.json +1 -1
  164. package/packages/daemon/dist/cloud-cli.d.ts +1 -1
  165. package/packages/daemon/dist/cloud-cli.d.ts.map +1 -1
  166. package/packages/daemon/dist/cloud-cli.js +42 -2
  167. package/packages/daemon/dist/cloud-cli.js.map +1 -1
  168. package/packages/daemon/dist/cloud-config.d.ts +1 -0
  169. package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
  170. package/packages/daemon/dist/cloud-config.js +26 -8
  171. package/packages/daemon/dist/cloud-config.js.map +1 -1
  172. package/packages/daemon/dist/cloud-config.test.js +19 -1
  173. package/packages/daemon/dist/cloud-config.test.js.map +1 -1
  174. package/packages/daemon/dist/device-flow.d.ts +24 -0
  175. package/packages/daemon/dist/device-flow.d.ts.map +1 -0
  176. package/packages/daemon/dist/device-flow.js +128 -0
  177. package/packages/daemon/dist/device-flow.js.map +1 -0
  178. package/packages/daemon/dist/device-flow.test.d.ts +2 -0
  179. package/packages/daemon/dist/device-flow.test.d.ts.map +1 -0
  180. package/packages/daemon/dist/device-flow.test.js +172 -0
  181. package/packages/daemon/dist/device-flow.test.js.map +1 -0
  182. package/packages/daemon/package.json +5 -5
  183. package/packages/gsd-agent-core/package.json +5 -5
  184. package/packages/gsd-agent-modes/package.json +7 -7
  185. package/packages/gsd-cloud/README.md +48 -0
  186. package/packages/gsd-cloud/bin/gsd-cloud.js +28 -0
  187. package/packages/gsd-cloud/package.json +40 -0
  188. package/packages/mcp-server/package.json +4 -4
  189. package/packages/native/package.json +1 -1
  190. package/packages/pi-agent-core/package.json +1 -1
  191. package/packages/pi-ai/package.json +1 -1
  192. package/packages/pi-coding-agent/package.json +7 -7
  193. package/packages/pi-tui/package.json +2 -2
  194. package/packages/rpc-client/package.json +2 -2
  195. package/pkg/package.json +1 -1
  196. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +19 -0
  197. package/src/resources/extensions/gsd/auto-post-unit.ts +22 -3
  198. package/src/resources/extensions/gsd/auto-prompts.ts +20 -0
  199. package/src/resources/extensions/gsd/auto.ts +12 -0
  200. package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +77 -1
  201. package/src/resources/extensions/gsd/doctor-history.ts +81 -0
  202. package/src/resources/extensions/gsd/doctor-state-checks.ts +527 -0
  203. package/src/resources/extensions/gsd/doctor-workspace-checks.ts +97 -0
  204. package/src/resources/extensions/gsd/doctor.ts +14 -673
  205. package/src/resources/extensions/gsd/md-importer.ts +7 -2
  206. package/src/resources/extensions/gsd/migrate/planning-writer.ts +9 -3
  207. package/src/resources/extensions/gsd/migration-auto-check.ts +3 -0
  208. package/src/resources/extensions/gsd/reopen-reason.ts +109 -0
  209. package/src/resources/extensions/gsd/session-status-io.ts +34 -0
  210. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +67 -20
  211. package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +47 -5
  212. package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +26 -0
  213. package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +76 -1
  214. package/src/resources/extensions/gsd/tests/doctor-history-public-api.test.ts +25 -0
  215. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +53 -0
  216. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +53 -0
  217. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +4 -0
  218. package/src/resources/extensions/gsd/tests/planning-writer.test.ts +18 -1
  219. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +18 -3
  220. package/src/resources/extensions/gsd/tests/reopen-reason.test.ts +117 -0
  221. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +177 -25
  222. package/src/resources/extensions/gsd/tests/worker-resume-wait.test.ts +73 -0
  223. package/src/resources/extensions/gsd/tools/reopen-task.ts +15 -0
  224. package/src/resources/extensions/shared/gsd-browser-cli.ts +35 -7
  225. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +0 -1
  226. package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +0 -1
  227. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +0 -1
  228. /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → ue2JDvA1wboxPBPeblD5N}/_buildManifest.js +0 -0
  229. /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → ue2JDvA1wboxPBPeblD5N}/_ssgManifest.js +0 -0
@@ -1,25 +1,21 @@
1
- import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { execFileSync } from "node:child_process";
4
- import { loadFile, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
5
- import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
6
- import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
1
+ import { existsSync } from "node:fs";
2
+ import { loadFile, saveFile } from "./files.js";
3
+ import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
4
+ import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
7
5
  import { openExistingWorkflowDatabase } from "./db-workspace.js";
8
- import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, legacyMilestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
9
- import { findMilestoneIds } from "./milestone-ids.js";
6
+ import { resolveMilestoneFile, milestonesDir, legacyMilestonesDir, resolveGsdRootFile } from "./paths.js";
10
7
  import { deriveState, isMilestoneComplete } from "./state.js";
11
8
  import { invalidateAllCaches } from "./cache.js";
12
9
  import { loadEffectiveGSDPreferences } from "./preferences.js";
10
+ import { appendDoctorHistory } from "./doctor-history.js";
11
+ import { checkWorkspaceRepositoryHealth } from "./doctor-workspace-checks.js";
13
12
  import { collectPreferenceDiagnostics, formatPreferenceDiagnosticDetail } from "./preferences-diagnostics.js";
14
- import { isClosedStatus } from "./status-guards.js";
15
13
  import { GLOBAL_STATE_CODES } from "./doctor-types.js";
16
- import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
17
- import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
18
14
  import { checkGitHealth, checkRuntimeHealth, checkGlobalHealth, checkEngineHealth } from "./doctor-checks.js";
19
15
  import { checkEnvironmentHealth } from "./doctor-environment.js";
20
- import { runProviderChecks } from "./doctor-providers.js";
21
- import { validateTitle } from "./validation.js";
16
+ import { checkGsdStateHealth } from "./doctor-state-checks.js";
22
17
  export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt, formatDoctorReportJson } from "./doctor-format.js";
18
+ export { readDoctorHistory } from "./doctor-history.js";
23
19
  export { runEnvironmentChecks, runFullEnvironmentChecks, formatEnvironmentReport } from "./doctor-environment.js";
24
20
  export { computeProgressScore, computeProgressScoreWithContext, formatProgressLine, formatProgressReport } from "./progress-score.js";
25
21
  export { validateTitle } from "./validation.js";
@@ -115,55 +111,6 @@ export async function rebuildState(basePath) {
115
111
  const path = resolveGsdRootFile(basePath, "STATE");
116
112
  await saveFile(path, buildStateMarkdown(state));
117
113
  }
118
- function matchesScope(unitId, scope) {
119
- if (!scope)
120
- return true;
121
- return unitId === scope || unitId.startsWith(`${scope}/`);
122
- }
123
- function auditRequirements(content) {
124
- if (!content)
125
- return [];
126
- const issues = [];
127
- const blocks = content.split(/^###\s+/m).slice(1);
128
- for (const block of blocks) {
129
- const idMatch = block.match(/^(R\d+)/);
130
- if (!idMatch)
131
- continue;
132
- const requirementId = idMatch[1];
133
- const status = block.match(/^-\s+Status:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
134
- const owner = block.match(/^-\s+Primary owning slice:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
135
- const notes = block.match(/^-\s+Notes:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
136
- if (status === "active" && (!owner || owner === "none" || owner === "none yet")) {
137
- // #4414: Downgrade to warning. A newly-created requirement has
138
- // primary_owner='' by default until the planning agent wires it to
139
- // a slice via gsd_requirement_update. Flagging this as an error
140
- // during normal planning is noisy — the real failure mode is when
141
- // it persists past milestone completion, which is covered by other
142
- // audits. Keep the signal but don't treat it as a blocker.
143
- issues.push({
144
- severity: "warning",
145
- code: "active_requirement_missing_owner",
146
- scope: "project",
147
- unitId: requirementId,
148
- message: `${requirementId} is Active but has no primary owning slice`,
149
- file: relGsdRootFile("REQUIREMENTS"),
150
- fixable: false,
151
- });
152
- }
153
- if (status === "blocked" && !notes) {
154
- issues.push({
155
- severity: "warning",
156
- code: "blocked_requirement_missing_reason",
157
- scope: "project",
158
- unitId: requirementId,
159
- message: `${requirementId} is Blocked but has no reason in Notes`,
160
- file: relGsdRootFile("REQUIREMENTS"),
161
- fixable: false,
162
- });
163
- }
164
- }
165
- return issues;
166
- }
167
114
  export async function selectDoctorScope(basePath, requestedScope) {
168
115
  if (requestedScope)
169
116
  return requestedScope;
@@ -197,179 +144,6 @@ export async function selectDoctorScope(basePath, requestedScope) {
197
144
  }
198
145
  return state.registry[0]?.id;
199
146
  }
200
- // ── Helper: circular dependency detection ──────────────────────────────────
201
- function detectCircularDependencies(slices) {
202
- const known = new Set(slices.map(s => s.id));
203
- const adj = new Map();
204
- for (const s of slices)
205
- adj.set(s.id, s.depends.filter(d => known.has(d)));
206
- const state = new Map();
207
- for (const s of slices)
208
- state.set(s.id, "unvisited");
209
- const cycles = [];
210
- function dfs(id, path) {
211
- const st = state.get(id);
212
- if (st === "done")
213
- return;
214
- if (st === "visiting") {
215
- cycles.push([...path.slice(path.indexOf(id)), id]);
216
- return;
217
- }
218
- state.set(id, "visiting");
219
- for (const dep of adj.get(id) ?? [])
220
- dfs(dep, [...path, id]);
221
- state.set(id, "done");
222
- }
223
- for (const s of slices)
224
- if (state.get(s.id) === "unvisited")
225
- dfs(s.id, []);
226
- return cycles;
227
- }
228
- async function appendDoctorHistory(basePath, report) {
229
- try {
230
- const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
231
- const errorCount = report.issues.filter(i => i.severity === "error").length;
232
- const warningCount = report.issues.filter(i => i.severity === "warning").length;
233
- const issueDetails = report.issues
234
- .filter(i => i.severity === "error" || i.severity === "warning")
235
- .slice(0, 10) // cap to keep JSONL lines bounded
236
- .map(i => ({ severity: i.severity, code: i.code, message: i.message, unitId: i.unitId }));
237
- // Human-readable one-line summary
238
- const summaryParts = [];
239
- if (report.ok) {
240
- summaryParts.push("Clean");
241
- }
242
- else {
243
- const counts = [];
244
- if (errorCount > 0)
245
- counts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
246
- if (warningCount > 0)
247
- counts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
248
- summaryParts.push(counts.join(", "));
249
- }
250
- if (report.fixesApplied.length > 0) {
251
- summaryParts.push(`${report.fixesApplied.length} fixed`);
252
- }
253
- if (issueDetails.length > 0) {
254
- const topIssue = issueDetails.find(i => i.severity === "error") ?? issueDetails[0];
255
- summaryParts.push(topIssue.message);
256
- }
257
- const entry = JSON.stringify({
258
- ts: new Date().toISOString(),
259
- ok: report.ok,
260
- errors: errorCount,
261
- warnings: warningCount,
262
- fixes: report.fixesApplied.length,
263
- codes: [...new Set(report.issues.map(i => i.code))],
264
- issues: issueDetails.length > 0 ? issueDetails : undefined,
265
- fixDescriptions: report.fixesApplied.length > 0 ? report.fixesApplied : undefined,
266
- scope: report.scope,
267
- summary: summaryParts.join(" · "),
268
- });
269
- const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
270
- await saveFile(historyPath, existing + entry + "\n");
271
- }
272
- catch { /* non-fatal */ }
273
- }
274
- /** Read the last N doctor history entries. Returns most-recent-first. */
275
- export async function readDoctorHistory(basePath, lastN = 50) {
276
- try {
277
- const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
278
- if (!existsSync(historyPath))
279
- return [];
280
- const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
281
- return lines.slice(-lastN).reverse().map(l => JSON.parse(l));
282
- }
283
- catch {
284
- return [];
285
- }
286
- }
287
- /**
288
- * Resolve the git working-tree root for a path, or null if it is not a repo.
289
- * Used by the workspace-repository probe to check that a declared child path is
290
- * itself a git repository (not merely nested inside the parent's repo).
291
- */
292
- function resolveGitToplevel(cwd) {
293
- try {
294
- const out = execFileSync("git", ["rev-parse", "--show-toplevel"], {
295
- cwd,
296
- stdio: ["ignore", "pipe", "pipe"],
297
- encoding: "utf-8",
298
- env: GIT_NO_PROMPT_ENV,
299
- }).trim();
300
- return out ? resolve(out) : null;
301
- }
302
- catch {
303
- return null;
304
- }
305
- }
306
- /** realpath that falls back to the resolved path if the link cannot be read. */
307
- function realpathSafe(p) {
308
- try {
309
- return realpathSync(p);
310
- }
311
- catch {
312
- return resolve(p);
313
- }
314
- }
315
- /**
316
- * Parent-workspace probe (#818): validate that every declared child repository
317
- * exists on disk and is a git repository at its own root. The registry only
318
- * checks paths stay inside the project root; it never checks existence or
319
- * git-ness, so a typo'd path (e.g. `frontned`) would otherwise build cleanly
320
- * and silently produce "no problems found." No-op for single-repo projects.
321
- */
322
- function checkWorkspaceRepositoryHealth(basePath, prefs, issues) {
323
- if (prefs?.workspace?.mode !== "parent")
324
- return;
325
- let registry;
326
- try {
327
- registry = createRepositoryRegistryFromPreferences(basePath, prefs);
328
- }
329
- catch (err) {
330
- issues.push({
331
- severity: "error",
332
- code: "invalid_preferences",
333
- scope: "project",
334
- unitId: "workspace",
335
- message: `workspace registry failed to build: ${err instanceof Error ? err.message : String(err)}`,
336
- fixable: false,
337
- });
338
- return;
339
- }
340
- for (const repo of registry.repositories) {
341
- if (repo.id === "project")
342
- continue;
343
- if (!existsSync(repo.root)) {
344
- issues.push({
345
- severity: "error",
346
- code: "workspace_repo_path_missing",
347
- scope: "project",
348
- unitId: `workspace.repositories.${repo.id}`,
349
- message: `declared repository "${repo.id}" path does not exist on disk: ${repo.root}`,
350
- fixable: false,
351
- });
352
- continue;
353
- }
354
- // Must be a repo at its OWN root — a plain dir nested in the parent repo
355
- // would otherwise pass a bare `nativeIsRepo` check via the enclosing
356
- // parent's .git. Compare on realpath so symlinked temp roots (macOS
357
- // /var vs /private/var) don't cause a false mismatch.
358
- const toplevel = resolveGitToplevel(repo.root);
359
- const declaredReal = realpathSafe(repo.root);
360
- const toplevelReal = toplevel ? realpathSafe(toplevel) : null;
361
- if (toplevelReal !== declaredReal) {
362
- issues.push({
363
- severity: "warning",
364
- code: "workspace_repo_not_a_repo",
365
- scope: "project",
366
- unitId: `workspace.repositories.${repo.id}`,
367
- message: `declared repository "${repo.id}" path is not a git repository at its own root: ${repo.root}`,
368
- fixable: false,
369
- });
370
- }
371
- }
372
- }
373
147
  export async function runGSDDoctor(basePath, options) {
374
148
  const issues = [];
375
149
  const fixesApplied = [];
@@ -450,424 +224,11 @@ export async function runGSDDoctor(basePath, options) {
450
224
  await appendDoctorHistory(basePath, report);
451
225
  return report;
452
226
  }
453
- const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
454
- const requirementsContent = await loadFile(requirementsPath);
455
- issues.push(...auditRequirements(requirementsContent));
456
- const state = await deriveState(basePath);
457
- // Provider / auth health checks — only relevant when there is active work to dispatch.
458
- // Skipped for idle projects (no active milestone) to avoid noise in environments
459
- // where CI/test runners have no API key configured.
460
- if (state.activeMilestone) {
461
- try {
462
- const providerResults = runProviderChecks();
463
- for (const result of providerResults) {
464
- if (!result.required)
465
- continue;
466
- if (result.status === "error") {
467
- issues.push({
468
- severity: "warning",
469
- code: "provider_key_missing",
470
- scope: "project",
471
- unitId: "project",
472
- message: result.message + (result.detail ? ` — ${result.detail}` : ""),
473
- fixable: false,
474
- });
475
- }
476
- else if (result.status === "warning") {
477
- issues.push({
478
- severity: "warning",
479
- code: "provider_key_backedoff",
480
- scope: "project",
481
- unitId: "project",
482
- message: result.message + (result.detail ? ` — ${result.detail}` : ""),
483
- fixable: false,
484
- });
485
- }
486
- }
487
- }
488
- catch {
489
- // Non-fatal — provider check failure should not block other checks
490
- }
491
- }
492
- // When DB is unavailable, state.registry is empty. Fall back to a direct
493
- // filesystem scan so the doctor can still report issues (e.g. missing ROADMAP)
494
- // for milestone dirs that exist on disk.
495
- const milestoneEntries = state.registry.length > 0
496
- ? state.registry
497
- : findMilestoneIds(basePath).map(id => ({ id, title: id }));
498
- for (const milestone of milestoneEntries) {
499
- const milestoneId = milestone.id;
500
- const milestonePath = resolveMilestonePath(basePath, milestoneId);
501
- if (!milestonePath)
502
- continue;
503
- // Validate milestone title for delimiter characters that break state documents.
504
- const milestoneTitleIssue = validateTitle(milestone.title);
505
- if (milestoneTitleIssue) {
506
- const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
507
- let wasFixed = false;
508
- if (shouldFix("delimiter_in_title") && roadmapFile) {
509
- try {
510
- const raw = readFileSync(roadmapFile, "utf-8");
511
- // Replace em/en dashes with " - " in the H1 title line only
512
- const sanitized = raw.replace(/^(# .*)$/m, (line) => line.replace(/[\u2014\u2013]/g, "-"));
513
- if (sanitized !== raw) {
514
- await saveFile(roadmapFile, sanitized);
515
- fixesApplied.push(`sanitized delimiter characters in ${milestoneId} title`);
516
- wasFixed = true;
517
- }
518
- }
519
- catch { /* non-fatal — report the warning below */ }
520
- }
521
- if (!wasFixed) {
522
- issues.push({
523
- severity: "warning",
524
- code: "delimiter_in_title",
525
- scope: "milestone",
526
- unitId: milestoneId,
527
- message: `Milestone ${milestoneId} ${milestoneTitleIssue}. Rename the milestone to remove these characters to prevent state corruption.`,
528
- file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
529
- fixable: true,
530
- });
531
- }
532
- }
533
- const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
534
- const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
535
- if (!roadmapContent) {
536
- issues.push({
537
- severity: "error",
538
- code: "missing_roadmap",
539
- scope: "milestone",
540
- unitId: milestoneId,
541
- message: `Milestone ${milestoneId} is missing its ROADMAP.md file.`,
542
- fixable: false,
543
- });
544
- continue;
545
- }
546
- let slices;
547
- if (isDbAvailable()) {
548
- const dbSlices = getMilestoneSlices(milestoneId);
549
- slices = dbSlices.map(s => ({
550
- id: s.id,
551
- title: s.title,
552
- done: isClosedStatus(s.status),
553
- pending: s.status === "pending",
554
- skipped: s.status === "skipped",
555
- risk: (s.risk || "medium"),
556
- depends: s.depends,
557
- demo: s.demo,
558
- }));
559
- }
560
- else {
561
- const activeMilestoneId = state.activeMilestone?.id;
562
- const activeSliceId = state.activeSlice?.id;
563
- slices = parseLegacyRoadmap(roadmapContent).slices.map(s => ({
564
- ...s,
565
- // Legacy roadmaps only encode done vs not-done. For doctor's
566
- // missing-directory checks, treat every undone slice except the
567
- // current active slice as effectively pending/unstarted.
568
- pending: !s.done && (milestoneId !== activeMilestoneId || s.id !== activeSliceId),
569
- }));
570
- }
571
- // Wrap in Roadmap-compatible shape for detectCircularDependencies
572
- const roadmap = { slices };
573
- // ── Circular dependency detection ──────────────────────────────────────
574
- for (const cycle of detectCircularDependencies(roadmap.slices)) {
575
- issues.push({
576
- severity: "error",
577
- code: "circular_slice_dependency",
578
- scope: "milestone",
579
- unitId: milestoneId,
580
- message: `Circular dependency detected: ${cycle.join(" → ")}`,
581
- file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
582
- fixable: false,
583
- });
584
- }
585
- // ── Orphaned slice directories ─────────────────────────────────────────
586
- try {
587
- const slicesDir = join(milestonePath, "slices");
588
- if (existsSync(slicesDir)) {
589
- const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
590
- for (const entry of readdirSync(slicesDir)) {
591
- try {
592
- if (!lstatSync(join(slicesDir, entry)).isDirectory())
593
- continue;
594
- }
595
- catch {
596
- continue;
597
- }
598
- if (entry === "parallel-research")
599
- continue;
600
- if (!knownSliceIds.has(entry)) {
601
- const quarantineExample = `.gsd/quarantine/milestones/${milestoneId}/slices/${entry}-manual-review`;
602
- issues.push({
603
- severity: "warning",
604
- code: "orphaned_slice_directory",
605
- scope: "milestone",
606
- unitId: milestoneId,
607
- message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap or DB. ` +
608
- `Review it; if stale, move or delete it. To preserve it, move it under ${quarantineExample}. ` +
609
- "If it contains work to keep, copy or merge that content into a DB-backed slice before resuming.",
610
- file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
611
- fixable: false,
612
- });
613
- }
614
- }
615
- }
616
- }
617
- catch { /* non-fatal */ }
618
- for (const slice of roadmap.slices) {
619
- const unitId = `${milestoneId}/${slice.id}`;
620
- if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId)
621
- continue;
622
- // Validate slice title for delimiter characters.
623
- const sliceTitleIssue = validateTitle(slice.title);
624
- if (sliceTitleIssue) {
625
- // Slice titles live inside the roadmap H1/checkbox lines — the milestone-level
626
- // fix above already sanitizes the roadmap file. For slices we only report, because
627
- // the title comes from the checkbox text and requires careful regex to fix safely.
628
- issues.push({
629
- severity: "warning",
630
- code: "delimiter_in_title",
631
- scope: "slice",
632
- unitId,
633
- message: `Slice ${unitId} ${sliceTitleIssue}. Rename the slice to remove these characters to prevent state corruption.`,
634
- file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
635
- fixable: false,
636
- });
637
- }
638
- // Check for unresolvable dependency IDs
639
- const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
640
- for (const dep of slice.depends) {
641
- if (!knownSliceIds.has(dep)) {
642
- issues.push({
643
- severity: "warning",
644
- code: "unresolvable_dependency",
645
- scope: "slice",
646
- unitId,
647
- 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]\``,
648
- file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
649
- fixable: false,
650
- });
651
- }
652
- }
653
- const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
654
- if (!slicePath) {
655
- // Pending slices haven't been planned yet — directories are created
656
- // lazily by ensurePreconditions() at dispatch time. Skipped slices are
657
- // intentionally allowed to remain summary-less and directory-less.
658
- if (slice.pending || slice.skipped)
659
- continue;
660
- const expectedPath = relSlicePath(basePath, milestoneId, slice.id);
661
- issues.push({
662
- severity: slice.done ? "warning" : "error",
663
- code: "missing_slice_dir",
664
- scope: "slice",
665
- unitId,
666
- message: slice.done
667
- ? `Missing slice directory for ${unitId} (slice is complete — cosmetic only)`
668
- : `Missing slice directory for ${unitId}`,
669
- file: expectedPath,
670
- fixable: true,
671
- });
672
- if (fix) {
673
- const absoluteSliceDir = join(milestonePath, "slices", slice.id);
674
- mkdirSync(absoluteSliceDir, { recursive: true });
675
- fixesApplied.push(`created ${absoluteSliceDir}`);
676
- }
677
- continue;
678
- }
679
- const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
680
- if (!tasksDir) {
681
- // Pending slices haven't been planned yet — tasks/ is created on demand.
682
- // Skipped slices may legitimately never create tasks/.
683
- if (slice.pending || slice.skipped)
684
- continue;
685
- // Flat-phase: tasks are embedded in plan files; no tasks/ subdir expected.
686
- if (!existsSync(legacyMilestonesDir(basePath)))
687
- continue;
688
- issues.push({
689
- severity: slice.done ? "warning" : "error",
690
- code: "missing_tasks_dir",
691
- scope: "slice",
692
- unitId,
693
- message: slice.done
694
- ? `Missing tasks directory for ${unitId} (slice is complete \u2014 cosmetic only)`
695
- : `Missing tasks directory for ${unitId}`,
696
- file: relSlicePath(basePath, milestoneId, slice.id),
697
- fixable: true,
698
- });
699
- if (fix) {
700
- mkdirSync(join(slicePath, "tasks"), { recursive: true });
701
- fixesApplied.push(`created ${join(slicePath, "tasks")}`);
702
- }
703
- }
704
- const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
705
- const planContent = planPath ? await loadFile(planPath) : null;
706
- // Normalize plan tasks: prefer DB, fall back to parsers-legacy
707
- let plan = null;
708
- if (isDbAvailable()) {
709
- const dbTasks = getSliceTasks(milestoneId, slice.id);
710
- if (dbTasks.length > 0) {
711
- plan = { tasks: dbTasks.map(t => ({ id: t.id, done: t.status === "complete" || t.status === "done", title: t.title, estimate: t.estimate || undefined })) };
712
- }
713
- }
714
- if (!plan && planContent) {
715
- plan = parseLegacyPlan(planContent);
716
- }
717
- if (!plan) {
718
- if (!slice.done) {
719
- issues.push({
720
- severity: "warning",
721
- code: "missing_slice_plan",
722
- scope: "slice",
723
- unitId,
724
- message: `Slice ${unitId} has no plan file`,
725
- file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
726
- fixable: false,
727
- });
728
- }
729
- continue;
730
- }
731
- // ── Duplicate task IDs ───────────────────────────────────────────────
732
- const taskIdCounts = new Map();
733
- for (const task of plan.tasks)
734
- taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
735
- for (const [taskId, count] of taskIdCounts) {
736
- if (count > 1) {
737
- issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
738
- message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
739
- file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
740
- }
741
- }
742
- // ── Task files on disk not in plan ────────────────────────────────────
743
- try {
744
- if (tasksDir) {
745
- const planTaskIds = new Set(plan.tasks.map(t => t.id));
746
- for (const f of readdirSync(tasksDir)) {
747
- if (!f.endsWith("-SUMMARY.md"))
748
- continue;
749
- const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
750
- if (!planTaskIds.has(diskTaskId)) {
751
- issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
752
- message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
753
- file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
754
- }
755
- }
756
- }
757
- }
758
- catch { /* non-fatal */ }
759
- let allTasksDone = plan.tasks.length > 0;
760
- for (const task of plan.tasks) {
761
- const taskUnitId = `${unitId}/${task.id}`;
762
- const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
763
- const hasSummary = !!(summaryPath && await loadFile(summaryPath));
764
- // Must-have verification
765
- if (task.done && hasSummary) {
766
- const taskPlanPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "PLAN");
767
- if (taskPlanPath) {
768
- const taskPlanContent = await loadFile(taskPlanPath);
769
- if (taskPlanContent) {
770
- const mustHaves = parseTaskPlanMustHaves(taskPlanContent);
771
- if (mustHaves.length > 0) {
772
- const summaryContent = await loadFile(summaryPath);
773
- const mentionedCount = summaryContent
774
- ? countMustHavesMentionedInSummary(mustHaves, summaryContent)
775
- : 0;
776
- if (mentionedCount < mustHaves.length) {
777
- issues.push({
778
- severity: "warning",
779
- code: "task_done_must_haves_not_verified",
780
- scope: "task",
781
- unitId: taskUnitId,
782
- message: `Task ${task.id} has ${mustHaves.length} must-haves but summary addresses only ${mentionedCount}`,
783
- file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
784
- fixable: false,
785
- });
786
- }
787
- }
788
- }
789
- }
790
- }
791
- // ── Future timestamp check ─────────────────────────────────────
792
- if (task.done && hasSummary && summaryPath) {
793
- try {
794
- const rawSummary = await loadFile(summaryPath);
795
- const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
796
- if (m) {
797
- const ts = new Date(m[1].trim());
798
- if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
799
- issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
800
- message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
801
- file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
802
- }
803
- }
804
- }
805
- catch { /* non-fatal */ }
806
- }
807
- allTasksDone = allTasksDone && task.done;
808
- }
809
- // Blocker-without-replan detection
810
- // Skip when all tasks are done — the blocker was implicitly resolved
811
- // within the task and the slice is not stuck (#3105 Bug 2).
812
- const replanPath = resolveSliceFile(basePath, milestoneId, slice.id, "REPLAN");
813
- if (!replanPath && !allTasksDone) {
814
- for (const task of plan.tasks) {
815
- if (!task.done)
816
- continue;
817
- const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
818
- if (!summaryPath)
819
- continue;
820
- const summaryContent = await loadFile(summaryPath);
821
- if (!summaryContent)
822
- continue;
823
- const summary = parseSummary(summaryContent);
824
- if (summary.frontmatter.blocker_discovered) {
825
- issues.push({
826
- severity: "warning",
827
- code: "blocker_discovered_no_replan",
828
- scope: "slice",
829
- unitId,
830
- message: `Task ${task.id} reported blocker_discovered but no REPLAN.md exists for ${slice.id} \u2014 slice may be stuck`,
831
- file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"),
832
- fixable: false,
833
- });
834
- break;
835
- }
836
- }
837
- }
838
- // ── Stale REPLAN: exists but all tasks done ────────────────────────
839
- if (replanPath && allTasksDone) {
840
- issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
841
- message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
842
- file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
843
- }
844
- }
845
- // Milestone-level check: all slices done but no validation file
846
- const milestoneComplete = roadmap.slices.length > 0 && roadmap.slices.every(s => s.done);
847
- if (milestoneComplete && !resolveMilestoneFile(basePath, milestoneId, "VALIDATION") && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
848
- issues.push({
849
- severity: "info",
850
- code: "all_slices_done_missing_milestone_validation",
851
- scope: "milestone",
852
- unitId: milestoneId,
853
- message: `All slices are done but the milestone VALIDATION.md is missing \u2014 milestone is in validating-milestone phase`,
854
- file: relMilestoneFile(basePath, milestoneId, "VALIDATION"),
855
- fixable: false,
856
- });
857
- }
858
- // Milestone-level check: all slices done but no milestone summary
859
- if (milestoneComplete && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
860
- issues.push({
861
- severity: "warning",
862
- code: "all_slices_done_missing_milestone_summary",
863
- scope: "milestone",
864
- unitId: milestoneId,
865
- message: `All slices are done but ${milestoneId}-SUMMARY.md is missing \u2014 milestone is stuck in completing-milestone phase`,
866
- file: relMilestoneFile(basePath, milestoneId, "SUMMARY"),
867
- fixable: false,
868
- });
869
- }
870
- }
227
+ await checkGsdStateHealth(basePath, issues, fixesApplied, {
228
+ fix,
229
+ shouldFix,
230
+ scope: options?.scope,
231
+ });
871
232
  if (fix && !dryRun && fixesApplied.length > 0) {
872
233
  await updateStateFile(basePath, fixesApplied);
873
234
  }