@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
@@ -0,0 +1,509 @@
1
+ import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadFile, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
4
+ import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
5
+ import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
6
+ import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, legacyMilestonesDir, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
7
+ import { findMilestoneIds } from "./milestone-ids.js";
8
+ import { deriveState } from "./state.js";
9
+ import { isClosedStatus } from "./status-guards.js";
10
+ import { runProviderChecks } from "./doctor-providers.js";
11
+ import { validateTitle } from "./validation.js";
12
+ function matchesScope(unitId, scope) {
13
+ if (!scope)
14
+ return true;
15
+ return unitId === scope || unitId.startsWith(`${scope}/`);
16
+ }
17
+ function auditRequirements(content) {
18
+ if (!content)
19
+ return [];
20
+ const issues = [];
21
+ const blocks = content.split(/^###\s+/m).slice(1);
22
+ for (const block of blocks) {
23
+ const idMatch = block.match(/^(R\d+)/);
24
+ if (!idMatch)
25
+ continue;
26
+ const requirementId = idMatch[1];
27
+ const status = block.match(/^-\s+Status:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
28
+ const owner = block.match(/^-\s+Primary owning slice:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
29
+ const notes = block.match(/^-\s+Notes:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
30
+ if (status === "active" && (!owner || owner === "none" || owner === "none yet")) {
31
+ // #4414: Downgrade to warning. A newly-created requirement has
32
+ // primary_owner='' by default until the planning agent wires it to
33
+ // a slice via gsd_requirement_update. Flagging this as an error
34
+ // during normal planning is noisy — the real failure mode is when
35
+ // it persists past milestone completion, which is covered by other
36
+ // audits. Keep the signal but don't treat it as a blocker.
37
+ issues.push({
38
+ severity: "warning",
39
+ code: "active_requirement_missing_owner",
40
+ scope: "project",
41
+ unitId: requirementId,
42
+ message: `${requirementId} is Active but has no primary owning slice`,
43
+ file: relGsdRootFile("REQUIREMENTS"),
44
+ fixable: false,
45
+ });
46
+ }
47
+ if (status === "blocked" && !notes) {
48
+ issues.push({
49
+ severity: "warning",
50
+ code: "blocked_requirement_missing_reason",
51
+ scope: "project",
52
+ unitId: requirementId,
53
+ message: `${requirementId} is Blocked but has no reason in Notes`,
54
+ file: relGsdRootFile("REQUIREMENTS"),
55
+ fixable: false,
56
+ });
57
+ }
58
+ }
59
+ return issues;
60
+ }
61
+ // ── Helper: circular dependency detection ──────────────────────────────────
62
+ function detectCircularDependencies(slices) {
63
+ const known = new Set(slices.map(s => s.id));
64
+ const adj = new Map();
65
+ for (const s of slices)
66
+ adj.set(s.id, s.depends.filter(d => known.has(d)));
67
+ const state = new Map();
68
+ for (const s of slices)
69
+ state.set(s.id, "unvisited");
70
+ const cycles = [];
71
+ function dfs(id, path) {
72
+ const st = state.get(id);
73
+ if (st === "done")
74
+ return;
75
+ if (st === "visiting") {
76
+ cycles.push([...path.slice(path.indexOf(id)), id]);
77
+ return;
78
+ }
79
+ state.set(id, "visiting");
80
+ for (const dep of adj.get(id) ?? [])
81
+ dfs(dep, [...path, id]);
82
+ state.set(id, "done");
83
+ }
84
+ for (const s of slices)
85
+ if (state.get(s.id) === "unvisited")
86
+ dfs(s.id, []);
87
+ return cycles;
88
+ }
89
+ export async function checkGsdStateHealth(basePath, issues, fixesApplied, options) {
90
+ const { fix, shouldFix, scope } = options;
91
+ const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
92
+ const requirementsContent = await loadFile(requirementsPath);
93
+ issues.push(...auditRequirements(requirementsContent));
94
+ const state = await deriveState(basePath);
95
+ // Provider / auth health checks — only relevant when there is active work to dispatch.
96
+ // Skipped for idle projects (no active milestone) to avoid noise in environments
97
+ // where CI/test runners have no API key configured.
98
+ if (state.activeMilestone) {
99
+ try {
100
+ const providerResults = runProviderChecks();
101
+ for (const result of providerResults) {
102
+ if (!result.required)
103
+ continue;
104
+ if (result.status === "error") {
105
+ issues.push({
106
+ severity: "warning",
107
+ code: "provider_key_missing",
108
+ scope: "project",
109
+ unitId: "project",
110
+ message: result.message + (result.detail ? ` — ${result.detail}` : ""),
111
+ fixable: false,
112
+ });
113
+ }
114
+ else if (result.status === "warning") {
115
+ issues.push({
116
+ severity: "warning",
117
+ code: "provider_key_backedoff",
118
+ scope: "project",
119
+ unitId: "project",
120
+ message: result.message + (result.detail ? ` — ${result.detail}` : ""),
121
+ fixable: false,
122
+ });
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // Non-fatal — provider check failure should not block other checks
128
+ }
129
+ }
130
+ // When DB is unavailable, state.registry is empty. Fall back to a direct
131
+ // filesystem scan so the doctor can still report issues (e.g. missing ROADMAP)
132
+ // for milestone dirs that exist on disk.
133
+ const milestoneEntries = state.registry.length > 0
134
+ ? state.registry
135
+ : findMilestoneIds(basePath).map(id => ({ id, title: id }));
136
+ for (const milestone of milestoneEntries) {
137
+ const milestoneId = milestone.id;
138
+ const milestonePath = resolveMilestonePath(basePath, milestoneId);
139
+ if (!milestonePath)
140
+ continue;
141
+ // Validate milestone title for delimiter characters that break state documents.
142
+ const milestoneTitleIssue = validateTitle(milestone.title);
143
+ if (milestoneTitleIssue) {
144
+ const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
145
+ let wasFixed = false;
146
+ if (shouldFix("delimiter_in_title") && roadmapFile) {
147
+ try {
148
+ const raw = readFileSync(roadmapFile, "utf-8");
149
+ // Replace em/en dashes with " - " in the H1 title line only
150
+ const sanitized = raw.replace(/^(# .*)$/m, (line) => line.replace(/[\u2014\u2013]/g, "-"));
151
+ if (sanitized !== raw) {
152
+ await saveFile(roadmapFile, sanitized);
153
+ fixesApplied.push(`sanitized delimiter characters in ${milestoneId} title`);
154
+ wasFixed = true;
155
+ }
156
+ }
157
+ catch { /* non-fatal — report the warning below */ }
158
+ }
159
+ if (!wasFixed) {
160
+ issues.push({
161
+ severity: "warning",
162
+ code: "delimiter_in_title",
163
+ scope: "milestone",
164
+ unitId: milestoneId,
165
+ message: `Milestone ${milestoneId} ${milestoneTitleIssue}. Rename the milestone to remove these characters to prevent state corruption.`,
166
+ file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
167
+ fixable: true,
168
+ });
169
+ }
170
+ }
171
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
172
+ const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
173
+ if (!roadmapContent) {
174
+ issues.push({
175
+ severity: "error",
176
+ code: "missing_roadmap",
177
+ scope: "milestone",
178
+ unitId: milestoneId,
179
+ message: `Milestone ${milestoneId} is missing its ROADMAP.md file.`,
180
+ fixable: false,
181
+ });
182
+ continue;
183
+ }
184
+ let slices;
185
+ if (isDbAvailable()) {
186
+ const dbSlices = getMilestoneSlices(milestoneId);
187
+ slices = dbSlices.map(s => ({
188
+ id: s.id,
189
+ title: s.title,
190
+ done: isClosedStatus(s.status),
191
+ pending: s.status === "pending",
192
+ skipped: s.status === "skipped",
193
+ risk: (s.risk || "medium"),
194
+ depends: s.depends,
195
+ demo: s.demo,
196
+ }));
197
+ }
198
+ else {
199
+ const activeMilestoneId = state.activeMilestone?.id;
200
+ const activeSliceId = state.activeSlice?.id;
201
+ slices = parseLegacyRoadmap(roadmapContent).slices.map(s => ({
202
+ ...s,
203
+ // Legacy roadmaps only encode done vs not-done. For doctor's
204
+ // missing-directory checks, treat every undone slice except the
205
+ // current active slice as effectively pending/unstarted.
206
+ pending: !s.done && (milestoneId !== activeMilestoneId || s.id !== activeSliceId),
207
+ }));
208
+ }
209
+ // Wrap in Roadmap-compatible shape for detectCircularDependencies
210
+ const roadmap = { slices };
211
+ // ── Circular dependency detection ──────────────────────────────────────
212
+ for (const cycle of detectCircularDependencies(roadmap.slices)) {
213
+ issues.push({
214
+ severity: "error",
215
+ code: "circular_slice_dependency",
216
+ scope: "milestone",
217
+ unitId: milestoneId,
218
+ message: `Circular dependency detected: ${cycle.join(" → ")}`,
219
+ file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
220
+ fixable: false,
221
+ });
222
+ }
223
+ // ── Orphaned slice directories ─────────────────────────────────────────
224
+ try {
225
+ const slicesDir = join(milestonePath, "slices");
226
+ if (existsSync(slicesDir)) {
227
+ const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
228
+ for (const entry of readdirSync(slicesDir)) {
229
+ try {
230
+ if (!lstatSync(join(slicesDir, entry)).isDirectory())
231
+ continue;
232
+ }
233
+ catch {
234
+ continue;
235
+ }
236
+ if (entry === "parallel-research")
237
+ continue;
238
+ if (!knownSliceIds.has(entry)) {
239
+ const quarantineExample = `.gsd/quarantine/milestones/${milestoneId}/slices/${entry}-manual-review`;
240
+ issues.push({
241
+ severity: "warning",
242
+ code: "orphaned_slice_directory",
243
+ scope: "milestone",
244
+ unitId: milestoneId,
245
+ message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap or DB. ` +
246
+ `Review it; if stale, move or delete it. To preserve it, move it under ${quarantineExample}. ` +
247
+ "If it contains work to keep, copy or merge that content into a DB-backed slice before resuming.",
248
+ file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
249
+ fixable: false,
250
+ });
251
+ }
252
+ }
253
+ }
254
+ }
255
+ catch { /* non-fatal */ }
256
+ for (const slice of roadmap.slices) {
257
+ const unitId = `${milestoneId}/${slice.id}`;
258
+ if (scope && !matchesScope(unitId, scope) && scope !== milestoneId)
259
+ continue;
260
+ // Validate slice title for delimiter characters.
261
+ const sliceTitleIssue = validateTitle(slice.title);
262
+ if (sliceTitleIssue) {
263
+ // Slice titles live inside the roadmap H1/checkbox lines — the milestone-level
264
+ // fix above already sanitizes the roadmap file. For slices we only report, because
265
+ // the title comes from the checkbox text and requires careful regex to fix safely.
266
+ issues.push({
267
+ severity: "warning",
268
+ code: "delimiter_in_title",
269
+ scope: "slice",
270
+ unitId,
271
+ message: `Slice ${unitId} ${sliceTitleIssue}. Rename the slice to remove these characters to prevent state corruption.`,
272
+ file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
273
+ fixable: false,
274
+ });
275
+ }
276
+ // Check for unresolvable dependency IDs
277
+ const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
278
+ for (const dep of slice.depends) {
279
+ if (!knownSliceIds.has(dep)) {
280
+ issues.push({
281
+ severity: "warning",
282
+ code: "unresolvable_dependency",
283
+ scope: "slice",
284
+ unitId,
285
+ 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]\``,
286
+ file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
287
+ fixable: false,
288
+ });
289
+ }
290
+ }
291
+ const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
292
+ if (!slicePath) {
293
+ // Pending slices haven't been planned yet — directories are created
294
+ // lazily by ensurePreconditions() at dispatch time. Skipped slices are
295
+ // intentionally allowed to remain summary-less and directory-less.
296
+ if (slice.pending || slice.skipped)
297
+ continue;
298
+ const expectedPath = relSlicePath(basePath, milestoneId, slice.id);
299
+ issues.push({
300
+ severity: slice.done ? "warning" : "error",
301
+ code: "missing_slice_dir",
302
+ scope: "slice",
303
+ unitId,
304
+ message: slice.done
305
+ ? `Missing slice directory for ${unitId} (slice is complete — cosmetic only)`
306
+ : `Missing slice directory for ${unitId}`,
307
+ file: expectedPath,
308
+ fixable: true,
309
+ });
310
+ if (fix) {
311
+ const absoluteSliceDir = join(milestonePath, "slices", slice.id);
312
+ mkdirSync(absoluteSliceDir, { recursive: true });
313
+ fixesApplied.push(`created ${absoluteSliceDir}`);
314
+ }
315
+ continue;
316
+ }
317
+ const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
318
+ if (!tasksDir) {
319
+ // Pending slices haven't been planned yet — tasks/ is created on demand.
320
+ // Skipped slices may legitimately never create tasks/.
321
+ if (slice.pending || slice.skipped)
322
+ continue;
323
+ // Flat-phase: tasks are embedded in plan files; no tasks/ subdir expected.
324
+ if (!existsSync(legacyMilestonesDir(basePath)))
325
+ continue;
326
+ issues.push({
327
+ severity: slice.done ? "warning" : "error",
328
+ code: "missing_tasks_dir",
329
+ scope: "slice",
330
+ unitId,
331
+ message: slice.done
332
+ ? `Missing tasks directory for ${unitId} (slice is complete \u2014 cosmetic only)`
333
+ : `Missing tasks directory for ${unitId}`,
334
+ file: relSlicePath(basePath, milestoneId, slice.id),
335
+ fixable: true,
336
+ });
337
+ if (fix) {
338
+ mkdirSync(join(slicePath, "tasks"), { recursive: true });
339
+ fixesApplied.push(`created ${join(slicePath, "tasks")}`);
340
+ }
341
+ }
342
+ const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
343
+ const planContent = planPath ? await loadFile(planPath) : null;
344
+ // Normalize plan tasks: prefer DB, fall back to parsers-legacy
345
+ let plan = null;
346
+ if (isDbAvailable()) {
347
+ const dbTasks = getSliceTasks(milestoneId, slice.id);
348
+ if (dbTasks.length > 0) {
349
+ plan = { tasks: dbTasks.map(t => ({ id: t.id, done: t.status === "complete" || t.status === "done", title: t.title, estimate: t.estimate || undefined })) };
350
+ }
351
+ }
352
+ if (!plan && planContent) {
353
+ plan = parseLegacyPlan(planContent);
354
+ }
355
+ if (!plan) {
356
+ if (!slice.done) {
357
+ issues.push({
358
+ severity: "warning",
359
+ code: "missing_slice_plan",
360
+ scope: "slice",
361
+ unitId,
362
+ message: `Slice ${unitId} has no plan file`,
363
+ file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
364
+ fixable: false,
365
+ });
366
+ }
367
+ continue;
368
+ }
369
+ // ── Duplicate task IDs ───────────────────────────────────────────────
370
+ const taskIdCounts = new Map();
371
+ for (const task of plan.tasks)
372
+ taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
373
+ for (const [taskId, count] of taskIdCounts) {
374
+ if (count > 1) {
375
+ issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
376
+ message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
377
+ file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
378
+ }
379
+ }
380
+ // ── Task files on disk not in plan ────────────────────────────────────
381
+ try {
382
+ if (tasksDir) {
383
+ const planTaskIds = new Set(plan.tasks.map(t => t.id));
384
+ for (const f of readdirSync(tasksDir)) {
385
+ if (!f.endsWith("-SUMMARY.md"))
386
+ continue;
387
+ const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
388
+ if (!planTaskIds.has(diskTaskId)) {
389
+ issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
390
+ message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
391
+ file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
392
+ }
393
+ }
394
+ }
395
+ }
396
+ catch { /* non-fatal */ }
397
+ let allTasksDone = plan.tasks.length > 0;
398
+ for (const task of plan.tasks) {
399
+ const taskUnitId = `${unitId}/${task.id}`;
400
+ const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
401
+ const hasSummary = !!(summaryPath && await loadFile(summaryPath));
402
+ // Must-have verification
403
+ if (task.done && hasSummary) {
404
+ const taskPlanPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "PLAN");
405
+ if (taskPlanPath) {
406
+ const taskPlanContent = await loadFile(taskPlanPath);
407
+ if (taskPlanContent) {
408
+ const mustHaves = parseTaskPlanMustHaves(taskPlanContent);
409
+ if (mustHaves.length > 0) {
410
+ const summaryContent = await loadFile(summaryPath);
411
+ const mentionedCount = summaryContent
412
+ ? countMustHavesMentionedInSummary(mustHaves, summaryContent)
413
+ : 0;
414
+ if (mentionedCount < mustHaves.length) {
415
+ issues.push({
416
+ severity: "warning",
417
+ code: "task_done_must_haves_not_verified",
418
+ scope: "task",
419
+ unitId: taskUnitId,
420
+ message: `Task ${task.id} has ${mustHaves.length} must-haves but summary addresses only ${mentionedCount}`,
421
+ file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
422
+ fixable: false,
423
+ });
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
429
+ // ── Future timestamp check ─────────────────────────────────────
430
+ if (task.done && hasSummary && summaryPath) {
431
+ try {
432
+ const rawSummary = await loadFile(summaryPath);
433
+ const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
434
+ if (m) {
435
+ const ts = new Date(m[1].trim());
436
+ if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
437
+ issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
438
+ message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
439
+ file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
440
+ }
441
+ }
442
+ }
443
+ catch { /* non-fatal */ }
444
+ }
445
+ allTasksDone = allTasksDone && task.done;
446
+ }
447
+ // Blocker-without-replan detection
448
+ // Skip when all tasks are done — the blocker was implicitly resolved
449
+ // within the task and the slice is not stuck (#3105 Bug 2).
450
+ const replanPath = resolveSliceFile(basePath, milestoneId, slice.id, "REPLAN");
451
+ if (!replanPath && !allTasksDone) {
452
+ for (const task of plan.tasks) {
453
+ if (!task.done)
454
+ continue;
455
+ const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
456
+ if (!summaryPath)
457
+ continue;
458
+ const summaryContent = await loadFile(summaryPath);
459
+ if (!summaryContent)
460
+ continue;
461
+ const summary = parseSummary(summaryContent);
462
+ if (summary.frontmatter.blocker_discovered) {
463
+ issues.push({
464
+ severity: "warning",
465
+ code: "blocker_discovered_no_replan",
466
+ scope: "slice",
467
+ unitId,
468
+ message: `Task ${task.id} reported blocker_discovered but no REPLAN.md exists for ${slice.id} \u2014 slice may be stuck`,
469
+ file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"),
470
+ fixable: false,
471
+ });
472
+ break;
473
+ }
474
+ }
475
+ }
476
+ // ── Stale REPLAN: exists but all tasks done ────────────────────────
477
+ if (replanPath && allTasksDone) {
478
+ issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
479
+ message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
480
+ file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
481
+ }
482
+ }
483
+ // Milestone-level check: all slices done but no validation file
484
+ const milestoneComplete = roadmap.slices.length > 0 && roadmap.slices.every(s => s.done);
485
+ if (milestoneComplete && !resolveMilestoneFile(basePath, milestoneId, "VALIDATION") && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
486
+ issues.push({
487
+ severity: "info",
488
+ code: "all_slices_done_missing_milestone_validation",
489
+ scope: "milestone",
490
+ unitId: milestoneId,
491
+ message: `All slices are done but the milestone VALIDATION.md is missing \u2014 milestone is in validating-milestone phase`,
492
+ file: relMilestoneFile(basePath, milestoneId, "VALIDATION"),
493
+ fixable: false,
494
+ });
495
+ }
496
+ // Milestone-level check: all slices done but no milestone summary
497
+ if (milestoneComplete && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
498
+ issues.push({
499
+ severity: "warning",
500
+ code: "all_slices_done_missing_milestone_summary",
501
+ scope: "milestone",
502
+ unitId: milestoneId,
503
+ message: `All slices are done but ${milestoneId}-SUMMARY.md is missing \u2014 milestone is stuck in completing-milestone phase`,
504
+ file: relMilestoneFile(basePath, milestoneId, "SUMMARY"),
505
+ fixable: false,
506
+ });
507
+ }
508
+ }
509
+ }
@@ -0,0 +1,91 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { resolve } from "node:path";
4
+ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
5
+ import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
6
+ /**
7
+ * Resolve the git working-tree root for a path, or null if it is not a repo.
8
+ * Used by the workspace-repository probe to check that a declared child path is
9
+ * itself a git repository (not merely nested inside the parent's repo).
10
+ */
11
+ function resolveGitToplevel(cwd) {
12
+ try {
13
+ const out = execFileSync("git", ["rev-parse", "--show-toplevel"], {
14
+ cwd,
15
+ stdio: ["ignore", "pipe", "pipe"],
16
+ encoding: "utf-8",
17
+ env: GIT_NO_PROMPT_ENV,
18
+ }).trim();
19
+ return out ? resolve(out) : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /** realpath that falls back to the resolved path if the link cannot be read. */
26
+ function realpathSafe(p) {
27
+ try {
28
+ return realpathSync(p);
29
+ }
30
+ catch {
31
+ return resolve(p);
32
+ }
33
+ }
34
+ /**
35
+ * Parent-workspace probe (#818): validate that every declared child repository
36
+ * exists on disk and is a git repository at its own root. The registry only
37
+ * checks paths stay inside the project root; it never checks existence or
38
+ * git-ness, so a typo'd path (e.g. `frontned`) would otherwise build cleanly
39
+ * and silently produce "no problems found." No-op for single-repo projects.
40
+ */
41
+ export function checkWorkspaceRepositoryHealth(basePath, prefs, issues) {
42
+ if (prefs?.workspace?.mode !== "parent")
43
+ return;
44
+ let registry;
45
+ try {
46
+ registry = createRepositoryRegistryFromPreferences(basePath, prefs);
47
+ }
48
+ catch (err) {
49
+ issues.push({
50
+ severity: "error",
51
+ code: "invalid_preferences",
52
+ scope: "project",
53
+ unitId: "workspace",
54
+ message: `workspace registry failed to build: ${err instanceof Error ? err.message : String(err)}`,
55
+ fixable: false,
56
+ });
57
+ return;
58
+ }
59
+ for (const repo of registry.repositories) {
60
+ if (repo.id === "project")
61
+ continue;
62
+ if (!existsSync(repo.root)) {
63
+ issues.push({
64
+ severity: "error",
65
+ code: "workspace_repo_path_missing",
66
+ scope: "project",
67
+ unitId: `workspace.repositories.${repo.id}`,
68
+ message: `declared repository "${repo.id}" path does not exist on disk: ${repo.root}`,
69
+ fixable: false,
70
+ });
71
+ continue;
72
+ }
73
+ // Must be a repo at its OWN root — a plain dir nested in the parent repo
74
+ // would otherwise pass a bare `nativeIsRepo` check via the enclosing
75
+ // parent's .git. Compare on realpath so symlinked temp roots (macOS
76
+ // /var vs /private/var) don't cause a false mismatch.
77
+ const toplevel = resolveGitToplevel(repo.root);
78
+ const declaredReal = realpathSafe(repo.root);
79
+ const toplevelReal = toplevel ? realpathSafe(toplevel) : null;
80
+ if (toplevelReal !== declaredReal) {
81
+ issues.push({
82
+ severity: "warning",
83
+ code: "workspace_repo_not_a_repo",
84
+ scope: "project",
85
+ unitId: `workspace.repositories.${repo.id}`,
86
+ message: `declared repository "${repo.id}" path is not a git repository at its own root: ${repo.root}`,
87
+ fixable: false,
88
+ });
89
+ }
90
+ }
91
+ }