@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
@@ -656,8 +656,13 @@ export function migrateHierarchyToDb(basePath) {
656
656
  updateSliceStatus(milestoneId, sliceEntry.id, sliceStatus, importCompletionTimestamp(sliceSummary));
657
657
  }
658
658
  counts.slices++;
659
- // Insert tasks from parsed plan
660
- if (!plan)
659
+ // Insert tasks from parsed plan.
660
+ // Sketch slices carry only a stub PLAN until refined; do not seed DB
661
+ // tasks from a placeholder <tasks> block (would falsely flip
662
+ // planning -> executing before the slice is decomposed). Once the
663
+ // `[sketch]` badge is removed by a real plan-slice, tasks import
664
+ // normally on the next pass. (#1286)
665
+ if (!plan || sliceEntry.isSketch)
661
666
  continue;
662
667
  for (const taskEntry of plan.tasks) {
663
668
  // Per K002: use 'complete' not 'done'
@@ -9,6 +9,7 @@ import { mkdirSync } from "node:fs";
9
9
  import { join, relative } from "node:path";
10
10
  import { getAllMilestones, getMilestoneSlices, getSliceTasks } from "../gsd-db.js";
11
11
  import { saveFile } from "../files.js";
12
+ import { isClosedStatus } from "../status-guards.js";
12
13
  import { applyPlanningProjectionWrites, } from "../compat/planning-compat.js";
13
14
  function planningRoot(basePath) {
14
15
  return join(basePath, ".planning");
@@ -86,7 +87,11 @@ function formatPlanningPlan(phaseNum, planNum, title, tasks) {
86
87
  lines.push("<tasks>");
87
88
  for (const t of tasks) {
88
89
  const est = t.estimate ? ` _(${t.estimate})_` : "";
89
- lines.push(`- [ ] **${t.id}**: ${t.title}${est}`);
90
+ // Render the checkbox from the task's DB status so a DB→.planning
91
+ // projection preserves completion. Hardcoding `[ ]` here silently reset
92
+ // every completed historical task to unchecked on each reconcile (#1276).
93
+ const box = t.done ? "[x]" : "[ ]";
94
+ lines.push(`- ${box} **${t.id}**: ${t.title}${est}`);
90
95
  }
91
96
  lines.push("</tasks>");
92
97
  lines.push("");
@@ -131,7 +136,7 @@ export async function writePlanningDirectory(basePath, layout) {
131
136
  const phaseDir = join(root, "phases", phaseDirName);
132
137
  mkdirSync(phaseDir, { recursive: true });
133
138
  const tasks = getSliceTasks(milestone.id, slice.id);
134
- const isDone = tasks.length > 0 && tasks.every((t) => t.status === "complete");
139
+ const isDone = tasks.length > 0 && tasks.every((t) => isClosedStatus(t.status));
135
140
  roadmapEntries.push({
136
141
  number: phaseNum,
137
142
  title: slice.title || slice.id,
@@ -157,6 +162,7 @@ export async function writePlanningDirectory(basePath, layout) {
157
162
  id: task.id,
158
163
  title: task.title || task.id,
159
164
  estimate: task.estimate || undefined,
165
+ done: isClosedStatus(task.status),
160
166
  },
161
167
  ]));
162
168
  paths.push(planPath);
@@ -117,6 +117,10 @@ export function scanMarkdownHierarchy(basePath) {
117
117
  scan.counts.slices += roadmap.slices.length;
118
118
  for (const slice of roadmap.slices) {
119
119
  scan.slices.add(`${milestoneId}/${slice.id}`);
120
+ // Sketch slices carry only a stub PLAN until refined; match migrateHierarchyToDb
121
+ // and do not count placeholder tasks from the <tasks> block (#1286).
122
+ if (slice.isSketch)
123
+ continue;
120
124
  const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
121
125
  if (!planPath || !existsSync(planPath))
122
126
  continue;
@@ -0,0 +1,98 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Persist a reopened task's failure diagnosis and inject it into the re-dispatched execute-task prompt.
3
+ // GSD Extension — follow-up to #1225 (#1272)
4
+ //
5
+ // When complete-slice's gate reopens a task via gsd_task_reopen, the operator
6
+ // supplies a `reason` explaining exactly what the full-suite gate caught and
7
+ // how to fix it. That reason previously only landed in the workflow event log
8
+ // (audit trail) and never reached the re-dispatched executor, so the executor
9
+ // re-ran the original (green) verify and re-completed the task without touching
10
+ // the regression. This module is the "inject unresolved context into the next
11
+ // dispatch" mechanism for reopen reasons — a lightweight, file-based sibling of
12
+ // escalation.js#claimOverrideForInjection, keyed by (milestone, slice, task).
13
+ import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { legacyMilestonesDir, resolveSlicePath, resolveTasksDir } from "./paths.js";
16
+ import { atomicWriteSync } from "./atomic-write.js";
17
+ import { logWarning } from "./workflow-logger.js";
18
+ /**
19
+ * Canonical reopen-reason artifact path, parallel to T##-SUMMARY.md and
20
+ * T##-ESCALATION.json:
21
+ * .gsd/milestones/{M}/slices/{S}/tasks/{T}-REOPEN.json
22
+ * Flat-phase: the artifact sits directly in the phase dir. Legacy layouts
23
+ * without a tasks/ subdir return null (caller degrades gracefully).
24
+ */
25
+ export function reopenReasonArtifactPath(basePath, milestoneId, sliceId, taskId) {
26
+ const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
27
+ if (tDir)
28
+ return join(tDir, `${taskId}-REOPEN.json`);
29
+ if (existsSync(legacyMilestonesDir(basePath)))
30
+ return null;
31
+ const phaseDir = resolveSlicePath(basePath, milestoneId, sliceId);
32
+ if (!phaseDir)
33
+ return null;
34
+ return join(phaseDir, `${taskId}-REOPEN.json`);
35
+ }
36
+ /**
37
+ * Persist the reopen reason so the next execute-task dispatch can surface it.
38
+ * A no-op when the reason is empty or the artifact path cannot be resolved —
39
+ * reopen itself must still succeed even if we cannot attach context.
40
+ */
41
+ export function writeReopenReason(basePath, milestoneId, sliceId, taskId, reason) {
42
+ const trimmed = reason.trim();
43
+ if (!trimmed)
44
+ return;
45
+ const path = reopenReasonArtifactPath(basePath, milestoneId, sliceId, taskId);
46
+ if (!path) {
47
+ logWarning("tool", `reopen-reason: cannot resolve artifact path for ${milestoneId}/${sliceId}/${taskId}; reopen reason not attached to next dispatch`);
48
+ return;
49
+ }
50
+ const artifact = {
51
+ version: 1,
52
+ milestoneId, sliceId, taskId,
53
+ reason: trimmed,
54
+ createdAt: new Date().toISOString(),
55
+ };
56
+ mkdirSync(join(path, ".."), { recursive: true });
57
+ atomicWriteSync(path, JSON.stringify(artifact, null, 2));
58
+ }
59
+ /**
60
+ * If a reopen reason is pending for this task, claim it (one-shot: the artifact
61
+ * is deleted on read) and return the markdown block to prepend to the executor's
62
+ * prompt. Returns null when nothing is pending or the artifact is malformed.
63
+ */
64
+ export function claimReopenReasonForInjection(basePath, milestoneId, sliceId, taskId) {
65
+ const path = reopenReasonArtifactPath(basePath, milestoneId, sliceId, taskId);
66
+ if (!path || !existsSync(path))
67
+ return null;
68
+ let art = null;
69
+ try {
70
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
71
+ if (parsed && parsed.version === 1 && typeof parsed.reason === "string" && parsed.reason.trim()) {
72
+ art = parsed;
73
+ }
74
+ }
75
+ catch {
76
+ art = null;
77
+ }
78
+ // Claim (delete) regardless of validity so a malformed artifact doesn't
79
+ // wedge every future dispatch of this task.
80
+ try {
81
+ unlinkSync(path);
82
+ }
83
+ catch { /* already gone */ }
84
+ if (!art)
85
+ return null;
86
+ return { injectionBlock: formatReopenReasonBlock(art) };
87
+ }
88
+ function formatReopenReasonBlock(art) {
89
+ return [
90
+ `## Reopened — Reason (from ${art.taskId})`,
91
+ "",
92
+ "This task was previously marked complete but a downstream gate (e.g. complete-slice's full-suite run) reopened it. The verify command in your task plan passed in isolation — it did **not** catch the regression below. Do **not** call `gsd_task_complete` until you have reproduced and fixed exactly what this diagnosis describes, then re-run the specific failing command it names (not just the original scoped verify).",
93
+ "",
94
+ "**Diagnosis / fix instructions from the gate:**",
95
+ "",
96
+ art.reason.trim(),
97
+ ].join("\n");
98
+ }
@@ -18,6 +18,11 @@ const PARALLEL_DIR = "parallel";
18
18
  const STATUS_SUFFIX = ".status.json";
19
19
  const SIGNAL_SUFFIX = ".signal.json";
20
20
  const DEFAULT_STALE_TIMEOUT_MS = 30_000;
21
+ // How long a paused worker waits for the coordinator to lift the pause before
22
+ // it degrades to in-process serialization (#1273). Kept below the stale
23
+ // timeout so the wait never races the coordinator's liveness detection.
24
+ const DEFAULT_RESUME_WAIT_MS = 10_000;
25
+ const DEFAULT_RESUME_POLL_MS = 250;
21
26
  function isSessionStatus(data) {
22
27
  return data !== null && typeof data === "object" && "milestoneId" in data && "pid" in data;
23
28
  }
@@ -105,6 +110,32 @@ export function consumeSignal(basePath, milestoneId) {
105
110
  }
106
111
  return msg;
107
112
  }
113
+ /**
114
+ * Wait for a coordinator to lift a `pause` on a worker by sending `resume`
115
+ * (or `stop`). Polls the signal file until one of those arrives or the timeout
116
+ * elapses. Intervening `pause`/`rebase` signals are consumed and ignored so a
117
+ * repeated pause doesn't reset the wait.
118
+ *
119
+ * A worker's `pause` is only ever lifted by the interactive/dashboard resumer;
120
+ * unattended `gsd headless auto` owns no resumer, so callers use the `"timeout"`
121
+ * result to degrade to in-process serialization instead of stranding the worker
122
+ * at a terminal pause it cannot resume (#1273).
123
+ */
124
+ export async function awaitWorkerResume(basePath, milestoneId, opts = {}) {
125
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_RESUME_WAIT_MS;
126
+ const pollMs = opts.pollMs ?? DEFAULT_RESUME_POLL_MS;
127
+ const deadline = Date.now() + timeoutMs;
128
+ for (;;) {
129
+ const msg = consumeSignal(basePath, milestoneId);
130
+ if (msg?.signal === "resume")
131
+ return "resume";
132
+ if (msg?.signal === "stop")
133
+ return "stop";
134
+ if (Date.now() >= deadline)
135
+ return "timeout";
136
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
137
+ }
138
+ }
108
139
  // ─── Stale Detection ───────────────────────────────────────────────────────
109
140
  /** Check whether a session is stale (PID dead or heartbeat timed out). */
110
141
  export function isSessionStale(status, timeoutMs = DEFAULT_STALE_TIMEOUT_MS) {
@@ -5,8 +5,8 @@
5
5
  // instruction. Markdown hierarchy import is reserved for operator-controlled
6
6
  // migration/recovery commands, not automatic runtime reconciliation.
7
7
  import { existsSync } from "node:fs";
8
- import { getMilestone, isDbAvailable } from "../../gsd-db.js";
9
- import { findMilestoneIds } from "../../milestone-ids.js";
8
+ import { getAllMilestones, getMilestone, isDbAvailable } from "../../gsd-db.js";
9
+ import { findMilestoneIds, parseMilestoneId } from "../../milestone-ids.js";
10
10
  import { resolveMilestoneFile, resolveMilestonePath } from "../../paths.js";
11
11
  function milestoneHasContent(basePath, milestoneId) {
12
12
  const roadmap = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
@@ -16,6 +16,26 @@ function milestoneHasContent(basePath, milestoneId) {
16
16
  (context !== null && existsSync(context)) ||
17
17
  (summary !== null && existsSync(summary)));
18
18
  }
19
+ /**
20
+ * A directory extracted to a bare `M{NNN}` id is not unregistered when the
21
+ * milestone is actually registered under a `unique_milestone_ids` suffixed id
22
+ * (`M{NNN}-<suffix>`) for the same sequence number. The flat-phase extractor in
23
+ * `milestone-ids` cannot recover the suffix from a descriptive slug (e.g.
24
+ * `07-v40fmq-m007-...-footer-system`), so it falls through to the bare `M007`.
25
+ * Probing the DB for a suffixed row on that sequence resolves the directory to
26
+ * its registered identity instead of firing a false-positive drift (issue
27
+ * #1281).
28
+ */
29
+ function hasRegisteredSuffixedVariant(milestoneId) {
30
+ const { suffix, num } = parseMilestoneId(milestoneId);
31
+ // Only bare, well-formed `M{NNN}` ids can be ambiguous with a suffixed row.
32
+ if (suffix || num === 0)
33
+ return false;
34
+ return getAllMilestones().some((m) => {
35
+ const parsed = parseMilestoneId(m.id);
36
+ return parsed.num === num && parsed.suffix !== undefined;
37
+ });
38
+ }
19
39
  export function detectUnregisteredMilestoneDrift(_state, ctx) {
20
40
  if (!isDbAvailable())
21
41
  return [];
@@ -23,6 +43,8 @@ export function detectUnregisteredMilestoneDrift(_state, ctx) {
23
43
  for (const milestoneId of findMilestoneIds(ctx.basePath)) {
24
44
  if (getMilestone(milestoneId))
25
45
  continue;
46
+ if (hasRegisteredSuffixedVariant(milestoneId))
47
+ continue;
26
48
  if (!milestoneHasContent(ctx.basePath, milestoneId))
27
49
  continue;
28
50
  drifts.push({ kind: "unregistered-milestone", milestoneId });
@@ -30,29 +52,47 @@ export function detectUnregisteredMilestoneDrift(_state, ctx) {
30
52
  return drifts;
31
53
  }
32
54
  /**
33
- * Repair intentionally fails closed. The project-root DB is authoritative at
34
- * runtime; markdown-only milestones must be reconciled through an explicit,
35
- * operator-controlled action so operators opt into changing canonical state.
36
- *
37
- * The hint deliberately leads with the *targeted*, non-destructive options. The
38
- * common cause of this drift is a directory left under an old ID after a
39
- * `unique_milestone_ids` rename, where the right fix is to rename (move) the
40
- * directory — not a full DB reimport. `/gsd recover --confirm` is a destructive
41
- * clear-and-reimport of the entire DB and is offered only as a last resort, so
42
- * users do not reach for it expecting a targeted repair (see issue #826).
55
+ * The recovery hint deliberately leads with the *targeted*, non-destructive
56
+ * options. The common cause of this drift is a directory left under an old ID
57
+ * after a `unique_milestone_ids` rename, where the right fix is to rename (move)
58
+ * the directory — not a full DB reimport. `/gsd recover --confirm` is a
59
+ * destructive clear-and-reimport of the entire DB and is offered only as a last
60
+ * resort, so users do not reach for it expecting a targeted repair (see issue
61
+ * #826).
43
62
  */
44
- export function repairUnregisteredMilestone(record, ctx) {
63
+ function unregisteredMilestoneGuidance(record, ctx) {
45
64
  const dir = resolveMilestonePath(ctx.basePath, record.milestoneId);
46
65
  const dirHint = dir ?? `the .gsd directory for ${record.milestoneId}`;
47
- throw new Error(`Milestone ${record.milestoneId} exists only as markdown projection ` +
66
+ return (`Milestone ${record.milestoneId} exists only as markdown projection ` +
48
67
  "(on-disk ROADMAP/CONTEXT/SUMMARY with no authoritative DB row). " +
49
68
  "Runtime reconciliation will not import markdown into the DB. Choose one:\n" +
50
69
  ` • Rename: if this directory is the same milestone under an old ID (e.g. a unique_milestone_ids rename), move \`${dirHint}\` to the current ID's directory and re-run.\n` +
51
70
  ` • Discard: if this milestone is no longer relevant, delete \`${dirHint}\` and re-run.\n` +
52
71
  " • Last resort: `/gsd recover --confirm` performs a destructive full DB clear-and-reimport — it does NOT do a targeted import and can replace or duplicate existing DB milestones.");
53
72
  }
73
+ /**
74
+ * Terminal blocker for an unregistered milestone. The project-root DB is
75
+ * authoritative at runtime; markdown-only milestones must be reconciled through
76
+ * an explicit, operator-controlled action so operators opt into changing
77
+ * canonical state. Exposing this as a `blocker` (matching the `artifact-db`
78
+ * convention) surfaces the drift as a pause-with-hint that gates dispatch,
79
+ * instead of a guaranteed repair throw that escalates auto-mode health to red
80
+ * (see issue #1281).
81
+ */
82
+ export function describeUnregisteredMilestoneBlocker(record, ctx) {
83
+ return unregisteredMilestoneGuidance(record, ctx);
84
+ }
85
+ /**
86
+ * Repair intentionally fails closed. Retained as a defensive fallback for the
87
+ * reconciliation engine; in practice `blocker` short-circuits this drift into a
88
+ * non-fatal pause before repair is ever attempted.
89
+ */
90
+ export function repairUnregisteredMilestone(record, ctx) {
91
+ throw new Error(unregisteredMilestoneGuidance(record, ctx));
92
+ }
54
93
  export const unregisteredMilestoneHandler = {
55
94
  kind: "unregistered-milestone",
56
95
  detect: detectUnregisteredMilestoneDrift,
96
+ blocker: describeUnregisteredMilestoneBlocker,
57
97
  repair: repairUnregisteredMilestone,
58
98
  };
@@ -7,8 +7,9 @@
7
7
  // 2. Flag-OFF downgrade: when progressive_planning is off, dispatch routes
8
8
  // sketch slices to plan-slice, which writes PLAN.md but leaves
9
9
  // is_sketch=1 — the next reconciliation pass clears it.
10
- import { existsSync } from "node:fs";
10
+ import { existsSync, readFileSync } from "node:fs";
11
11
  import { getSketchedSliceIds, isDbAvailable, setSliceSketchFlag, } from "../../gsd-db.js";
12
+ import { parsePlan } from "../../parsers-legacy.js";
12
13
  import { resolveSliceFile } from "../../paths.js";
13
14
  export function detectStaleSketchFlags(state, ctx) {
14
15
  if (!isDbAvailable())
@@ -18,12 +19,54 @@ export function detectStaleSketchFlags(state, ctx) {
18
19
  return [];
19
20
  const sliceIds = getSketchedSliceIds(mid);
20
21
  return sliceIds
21
- .filter((sid) => {
22
- const planPath = resolveSliceFile(ctx.basePath, mid, sid, "PLAN");
23
- return planPath !== null && existsSync(planPath);
24
- })
22
+ .filter((sid) => sketchIsPlanned(ctx.basePath, mid, sid))
25
23
  .map((sid) => ({ kind: "stale-sketch-flag", mid, sid }));
26
24
  }
25
+ /**
26
+ * True when a task title is a synthetic placeholder emitted by a projection
27
+ * round-trip rather than a real, refined task. `migrate/transformer.buildTaskTitle`
28
+ * produces two shapes when a plan was never decomposed:
29
+ * - `Plan NN` (fallback when phase/plan frontmatter is absent)
30
+ * - `${phase} ${plan}` (e.g. `00 01`, `00 03b`, `29-auth-system 01`)
31
+ * Neither carries genuine planning intent, so both must be treated as stubs.
32
+ */
33
+ function isPlaceholderTaskTitle(title) {
34
+ const t = title.trim();
35
+ if (/^Plan\s+\d+[a-z]*$/i.test(t))
36
+ return true;
37
+ // buildTaskTitle returns `${phase} ${plan}` when frontmatter has both fields.
38
+ // Phase slugs are digit-led (e.g. `00`, `29-auth-system`); require that so
39
+ // legitimate titles like `Step 1` are not misclassified as stubs.
40
+ return /^\d[\w-]*\s+\d+[a-z]*$/i.test(t);
41
+ }
42
+ /**
43
+ * A sketch slice counts as "planned" (so its is_sketch flag may be cleared)
44
+ * only when it has a *real* plan on disk — not merely any PLAN file.
45
+ *
46
+ * File existence alone (#1287) is too weak: a stub/placeholder PLAN, a
47
+ * crash-leftover, or a projection round-trip that emits synthetic placeholder
48
+ * task titles (migrate/transformer.buildTaskTitle) all satisfy existsSync yet
49
+ * were never actually refined. Clearing the flag for one strips the `refining`
50
+ * guard in phase derivation and lets a phantom task drive dispatch.
51
+ *
52
+ * A legitimate plan-slice always decomposes into >= 1 genuine task, so require
53
+ * at least one non-placeholder task. This is strictly stricter than the old
54
+ * existence check — the documented crash-recovery scenarios (a real plan-slice
55
+ * that wrote its tasks) still clear, a bare stub no longer does.
56
+ */
57
+ function sketchIsPlanned(basePath, mid, sid) {
58
+ const planPath = resolveSliceFile(basePath, mid, sid, "PLAN");
59
+ if (planPath === null || !existsSync(planPath))
60
+ return false;
61
+ try {
62
+ const plan = parsePlan(readFileSync(planPath, "utf-8"));
63
+ return plan.tasks.some((t) => !isPlaceholderTaskTitle(t.title));
64
+ }
65
+ catch {
66
+ // A PLAN we cannot parse is not a trustworthy "planning done" signal.
67
+ return false;
68
+ }
69
+ }
27
70
  export function repairStaleSketchFlag(record) {
28
71
  setSliceSketchFlag(record.mid, record.sid, false);
29
72
  }
@@ -15,6 +15,7 @@ import { flushWorkflowProjections } from "../projection-flush.js";
15
15
  import { writeManifest } from "../workflow-manifest.js";
16
16
  import { appendEvent } from "../workflow-events.js";
17
17
  import { logWarning } from "../workflow-logger.js";
18
+ import { writeReopenReason } from "../reopen-reason.js";
18
19
  import { existsSync, unlinkSync } from "node:fs";
19
20
  import { join, basename, dirname } from "node:path";
20
21
  import { resolveTasksDir, resolveSlicePath, clearPathCache } from "../paths.js";
@@ -89,6 +90,20 @@ export async function handleReopenTask(params, basePath) {
89
90
  logWarning("tool", `reopen-task artifact cleanup warning: ${cleanupErr.message}`);
90
91
  }
91
92
  clearPathCache();
93
+ // ── Persist the reopen reason for the next execute-task dispatch (#1272) ──
94
+ // Without this, the re-dispatched executor receives the original task plan
95
+ // and verify command with zero signal that it was reopened — it re-runs the
96
+ // (still-green) scoped verify and re-completes the task, never touching the
97
+ // regression the gate actually caught. buildExecuteTaskPrompt claims this
98
+ // artifact and prepends the diagnosis to the next dispatch.
99
+ if (params.reason && params.reason.trim() !== "") {
100
+ try {
101
+ writeReopenReason(basePath, params.milestoneId, params.sliceId, params.taskId, params.reason);
102
+ }
103
+ catch (reasonErr) {
104
+ logWarning("tool", `reopen-task reason persistence warning: ${reasonErr.message}`);
105
+ }
106
+ }
92
107
  // ── Post-mutation hook ───────────────────────────────────────────────────
93
108
  try {
94
109
  await flushWorkflowProjections(basePath, { milestoneId: params.milestoneId });
@@ -307,11 +307,12 @@ export function resolveGsdBrowserMcpLaunchConfig(projectRoot, env = process.env,
307
307
  };
308
308
  }
309
309
  /**
310
- * CLI invocation that starts the gsd-browser session daemon with the same
311
- * session and identity flags as {@link resolveGsdBrowserMcpLaunchConfig}, so
312
- * browser UAT can warm Chrome/CDP before the first MCP navigation.
310
+ * CLI invocation that runs a `daemon <action>` command against the gsd-browser
311
+ * session daemon with the same session and identity flags as
312
+ * {@link resolveGsdBrowserMcpLaunchConfig}, so start (warm-up) and stop
313
+ * (teardown) address the exact same managed daemon.
313
314
  */
314
- export function resolveGsdBrowserDaemonStartInvocation(projectRoot, env = process.env, options = {}) {
315
+ function resolveGsdBrowserDaemonInvocation(action, projectRoot, env, options) {
315
316
  const launch = resolveGsdBrowserMcpLaunchConfig(projectRoot, env, options);
316
317
  const mcpIndex = launch.args.indexOf("mcp");
317
318
  if (mcpIndex < 0) {
@@ -321,8 +322,24 @@ export function resolveGsdBrowserDaemonStartInvocation(projectRoot, env = proces
321
322
  const sessionFlags = launch.args.slice(mcpIndex + 1);
322
323
  return {
323
324
  command: launch.command,
324
- args: [...prefix, "daemon", "start", ...sessionFlags],
325
+ args: [...prefix, "daemon", action, ...sessionFlags],
325
326
  cwd: launch.cwd,
326
327
  ...(launch.env ? { env: launch.env } : {}),
327
328
  };
328
329
  }
330
+ /**
331
+ * CLI invocation that starts the gsd-browser session daemon with the same
332
+ * session and identity flags as {@link resolveGsdBrowserMcpLaunchConfig}, so
333
+ * browser UAT can warm Chrome/CDP before the first MCP navigation.
334
+ */
335
+ export function resolveGsdBrowserDaemonStartInvocation(projectRoot, env = process.env, options = {}) {
336
+ return resolveGsdBrowserDaemonInvocation("start", projectRoot, env, options);
337
+ }
338
+ /**
339
+ * CLI invocation that stops the gsd-browser session daemon warmed for browser
340
+ * UAT, so the Chrome process is torn down instead of lingering after the
341
+ * triggering task/session ends.
342
+ */
343
+ export function resolveGsdBrowserDaemonStopInvocation(projectRoot, env = process.env, options = {}) {
344
+ return resolveGsdBrowserDaemonInvocation("stop", projectRoot, env, options);
345
+ }