@opengsd/gsd-pi 1.9.0-dev.5c1a3ff2 → 1.9.0-dev.97ef11e3

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 (173) hide show
  1. package/dist/headless.js +2 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +24 -4
  4. package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
  5. package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  7. package/dist/resources/extensions/gsd/auto-prompts.js +12 -4
  8. package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
  9. package/dist/resources/extensions/gsd/auto-start.js +29 -9
  10. package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
  11. package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
  12. package/dist/resources/extensions/gsd/auto.js +15 -5
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -2
  14. package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
  15. package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
  16. package/dist/resources/extensions/gsd/db/engine.js +5 -2
  17. package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
  18. package/dist/resources/extensions/gsd/error-classifier.js +12 -2
  19. package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
  20. package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
  21. package/dist/resources/extensions/gsd/gsd-db.js +75 -31
  22. package/dist/resources/extensions/gsd/guided-flow.js +2 -2
  23. package/dist/resources/extensions/gsd/markdown-renderer.js +3 -9
  24. package/dist/resources/extensions/gsd/paths.js +17 -7
  25. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  26. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
  27. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
  28. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
  29. package/dist/resources/extensions/gsd/stop-notice.js +9 -0
  30. package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
  31. package/dist/resources/extensions/shared/interview-ui.js +43 -6
  32. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.html +1 -1
  54. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  61. package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
  62. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  63. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  64. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  66. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  67. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  68. package/package.json +2 -1
  69. package/packages/cloud-mcp-gateway/package.json +2 -2
  70. package/packages/contracts/package.json +1 -1
  71. package/packages/daemon/package.json +4 -4
  72. package/packages/gsd-agent-core/package.json +5 -5
  73. package/packages/gsd-agent-modes/package.json +7 -7
  74. package/packages/gsd-cloud/README.md +9 -3
  75. package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
  76. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
  77. package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
  78. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
  79. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  80. package/packages/gsd-cloud/dist/cli.js +121 -15
  81. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  82. package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
  83. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
  84. package/packages/gsd-cloud/dist/cloud-config.js +7 -1
  85. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
  86. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
  87. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  88. package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
  89. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  90. package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
  91. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
  92. package/packages/gsd-cloud/dist/runtime-process.js +272 -0
  93. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
  94. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
  95. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
  96. package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
  97. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
  98. package/packages/gsd-cloud/package.json +2 -2
  99. package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
  100. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
  101. package/packages/mcp-server/dist/cli-runner.js +119 -33
  102. package/packages/mcp-server/dist/cli-runner.js.map +1 -1
  103. package/packages/mcp-server/package.json +4 -4
  104. package/packages/native/package.json +1 -1
  105. package/packages/pi-agent-core/package.json +1 -1
  106. package/packages/pi-ai/dist/models.generated.d.ts +17 -0
  107. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/models.generated.js +17 -0
  109. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
  112. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  113. package/packages/pi-ai/package.json +1 -1
  114. package/packages/pi-coding-agent/package.json +7 -7
  115. package/packages/pi-tui/package.json +2 -2
  116. package/packages/rpc-client/package.json +2 -2
  117. package/pkg/package.json +1 -1
  118. package/src/resources/extensions/gsd/auto/loop.ts +48 -4
  119. package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
  120. package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
  121. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  122. package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
  123. package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
  124. package/src/resources/extensions/gsd/auto-start.ts +31 -8
  125. package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
  126. package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
  127. package/src/resources/extensions/gsd/auto.ts +20 -7
  128. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
  129. package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
  130. package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
  131. package/src/resources/extensions/gsd/db/engine.ts +5 -2
  132. package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
  133. package/src/resources/extensions/gsd/error-classifier.ts +13 -2
  134. package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
  135. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
  136. package/src/resources/extensions/gsd/gsd-db.ts +85 -33
  137. package/src/resources/extensions/gsd/guided-flow.ts +2 -2
  138. package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
  139. package/src/resources/extensions/gsd/paths.ts +17 -7
  140. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  141. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
  142. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
  143. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
  144. package/src/resources/extensions/gsd/stop-notice.ts +11 -0
  145. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
  146. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
  147. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
  148. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
  149. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
  150. package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
  151. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
  152. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
  153. package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
  154. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
  155. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
  156. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
  157. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
  158. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
  159. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
  160. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
  161. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
  162. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  163. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
  164. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
  165. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  166. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
  167. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  168. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
  169. package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
  170. package/src/resources/extensions/shared/interview-ui.ts +47 -8
  171. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
  172. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → DXD9fZPnqtjZSlT5xdC3v}/_buildManifest.js +0 -0
  173. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → DXD9fZPnqtjZSlT5xdC3v}/_ssgManifest.js +0 -0
@@ -5,8 +5,8 @@
5
5
  // distinguish gsd-pi's own writes (expected) from external edits made by gsd-core
6
6
  // (drift to import). gsd-core is oblivious to this file and ignores it.
7
7
  import { createHash } from "node:crypto";
8
- import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
9
- import { dirname, join, isAbsolute, normalize } from "node:path";
8
+ import { existsSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
9
+ import { dirname, join, isAbsolute, normalize, relative, resolve } from "node:path";
10
10
  /** Current marker schema version. Bump on breaking format changes + migrate. */
11
11
  export const COMPAT_MARKER_SCHEMA = 2;
12
12
  /** Marker returned when no marker exists yet (fresh project, first gsd-pi run). */
@@ -32,6 +32,35 @@ export function normalizeForHash(content) {
32
32
  export function computeProjectionSha(content) {
33
33
  return createHash("sha256").update(normalizeForHash(content)).digest("hex").slice(0, 16);
34
34
  }
35
+ function normalizeRealPath(p) {
36
+ try {
37
+ return realpathSync.native(p);
38
+ }
39
+ catch {
40
+ return resolve(p);
41
+ }
42
+ }
43
+ function rootRelativeKey(rootPath, absPath) {
44
+ const root = normalizeRealPath(rootPath);
45
+ const abs = normalizeRealPath(absPath);
46
+ const rel = relative(root, abs);
47
+ if (!rel || rel === ".." || rel.startsWith(`..${sepForRelative(rel)}`) || isAbsolute(rel)) {
48
+ return null;
49
+ }
50
+ return rel.replace(/\\/g, "/");
51
+ }
52
+ function sepForRelative(rel) {
53
+ return rel.includes("\\") ? "\\" : "/";
54
+ }
55
+ export function deriveCompatProjectionKey(absPath, roots) {
56
+ for (const root of roots) {
57
+ const key = rootRelativeKey(root, absPath);
58
+ if (key)
59
+ return key;
60
+ }
61
+ const fallbackRoot = roots[roots.length - 1] ?? dirname(absPath);
62
+ return relative(fallbackRoot, absPath).replace(/\\/g, "/");
63
+ }
35
64
  /**
36
65
  * Read & validate the marker. A missing marker → EMPTY_MARKER (treat every
37
66
  * projection as external on next reconcile). A malformed marker is quarantined
@@ -39,7 +68,9 @@ export function computeProjectionSha(content) {
39
68
  * EMPTY_MARKER. A schema-mismatch returns EMPTY_MARKER (forward-compat: refuse
40
69
  * to act on a future format we don't understand).
41
70
  */
42
- export function readCompatMarker(basePath) {
71
+ export function readCompatMarker(basePath, options = {}) {
72
+ const healInvalidKeys = options.healInvalidKeys ?? true;
73
+ const quarantineInvalid = options.quarantineInvalid ?? true;
43
74
  const path = compatMarkerPath(basePath);
44
75
  if (!existsSync(path))
45
76
  return emptyMarker();
@@ -55,11 +86,16 @@ export function readCompatMarker(basePath) {
55
86
  parsed = JSON.parse(raw);
56
87
  }
57
88
  catch {
58
- quarantine(basePath, raw);
89
+ if (quarantineInvalid)
90
+ quarantine(basePath, raw);
59
91
  return emptyMarker();
60
92
  }
93
+ if (healInvalidKeys && healMarkerProjectionKeys(basePath, parsed) && isValidMarker(parsed)) {
94
+ writeCompatMarker(basePath, parsed);
95
+ }
61
96
  if (!isValidMarker(parsed)) {
62
- quarantine(basePath, raw);
97
+ if (quarantineInvalid)
98
+ quarantine(basePath, raw);
63
99
  return emptyMarker();
64
100
  }
65
101
  // Promote older markers by defaulting absent fields. Schema 1 → 2 only adds
@@ -196,6 +232,45 @@ function isValidProjectionMap(x) {
196
232
  }
197
233
  return true;
198
234
  }
235
+ function healMarkerProjectionKeys(basePath, marker) {
236
+ if (typeof marker !== "object" || marker === null)
237
+ return false;
238
+ const m = marker;
239
+ let changed = false;
240
+ if (typeof m.projections === "object" && m.projections !== null) {
241
+ changed = healProjectionMapKeys(basePath, ".gsd", m.projections) || changed;
242
+ }
243
+ const planning = m.planning;
244
+ if (typeof planning === "object" && planning !== null) {
245
+ const p = planning;
246
+ if (typeof p.projections === "object" && p.projections !== null) {
247
+ changed = healProjectionMapKeys(basePath, ".planning", p.projections) || changed;
248
+ }
249
+ if (typeof p.passthrough === "object" && p.passthrough !== null) {
250
+ changed = healProjectionMapKeys(basePath, ".planning", p.passthrough) || changed;
251
+ }
252
+ }
253
+ return changed;
254
+ }
255
+ function healProjectionMapKeys(basePath, rootName, map) {
256
+ let changed = false;
257
+ const root = join(basePath, rootName);
258
+ for (const key of Object.keys(map)) {
259
+ if (isSafeProjectionKey(key))
260
+ continue;
261
+ if (key.includes("\0") || isAbsolute(key) || /^[A-Za-z]:/.test(key))
262
+ continue;
263
+ const healedKey = rootRelativeKey(root, resolve(root, key));
264
+ if (!healedKey || !isSafeProjectionKey(healedKey))
265
+ continue;
266
+ if (!map[healedKey]) {
267
+ map[healedKey] = map[key];
268
+ }
269
+ delete map[key];
270
+ changed = true;
271
+ }
272
+ return changed;
273
+ }
199
274
  function isValidPlanningMarker(x) {
200
275
  if (typeof x !== "object" || x === null)
201
276
  return false;
@@ -10,7 +10,7 @@
10
10
  // writer layer.
11
11
  import { createRequire } from "node:module";
12
12
  import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
13
- import { dirname, join } from "node:path";
13
+ import { dirname, join, resolve } from "node:path";
14
14
  import { GSDError, GSD_STALE_STATE } from "../errors.js";
15
15
  import { logError, logWarning } from "../workflow-logger.js";
16
16
  import { createDbAdapter } from "../db-adapter.js";
@@ -31,7 +31,10 @@ function getGsdRequire() {
31
31
  if (_gsdRequire !== undefined)
32
32
  return _gsdRequire;
33
33
  try {
34
- _gsdRequire = createRequire(import.meta.url);
34
+ // Next.js may emit this module into a CommonJS chunk. Avoid ESM-only module
35
+ // metadata syntax here; it is a hard parse error there.
36
+ const packageRoot = process.env.GSD_WEB_PACKAGE_ROOT || process.env.GSD_PKG_ROOT || process.cwd();
37
+ _gsdRequire = createRequire(resolve(packageRoot, "package.json"));
35
38
  }
36
39
  catch {
37
40
  _gsdRequire = null;
@@ -58,6 +58,23 @@ function reportCheckboxDbStatusDivergence(issues, basePath, filePath, scope, uni
58
58
  fixable: false,
59
59
  });
60
60
  }
61
+ function bareDuplicateMilestoneId(milestoneId) {
62
+ const match = milestoneId.match(/^(M\d{3})-[a-z0-9]{6}$/i);
63
+ return match?.[1]?.toUpperCase() ?? null;
64
+ }
65
+ function checkboxDbStatusMilestoneIds(basePath, milestoneIds) {
66
+ if (!hasFlatPhaseLayout(basePath))
67
+ return milestoneIds;
68
+ // Flat-phase directories are keyed by phase number. A restored
69
+ // M003-xxxxxx row alongside M003 is a stale duplicate for the same
70
+ // phases/03-* projection, so checking both can compare DB status against two
71
+ // competing PLAN files and report a persistent false divergence.
72
+ const allIds = new Set(milestoneIds.map((id) => id.toUpperCase()));
73
+ return milestoneIds.filter((milestoneId) => {
74
+ const bareId = bareDuplicateMilestoneId(milestoneId);
75
+ return !bareId || !allIds.has(bareId);
76
+ });
77
+ }
61
78
  function checkProjectionCheckboxDbStatus(basePath, milestoneIds, issues) {
62
79
  for (const milestoneId of milestoneIds) {
63
80
  const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
@@ -322,7 +339,9 @@ function staleMilestonesArtifactRowFixable(basePath, row, artifactRows) {
322
339
  }
323
340
  function stalePhasesArtifactRowFixable(basePath, row, artifactRows) {
324
341
  const issuePath = artifactPathRelativeToGsd(row.path);
325
- return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) && hasPresentMilestonesReplacement(basePath, row, artifactRows);
342
+ return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) &&
343
+ (hasPresentFlatPhaseReplacement(basePath, row, artifactRows) ||
344
+ hasPresentMilestonesReplacement(basePath, row, artifactRows));
326
345
  }
327
346
  function staleArtifactRowFixable(basePath, row, artifactRows) {
328
347
  return staleMilestonesArtifactRowFixable(basePath, row, artifactRows) ||
@@ -658,7 +677,7 @@ export async function checkEngineHealth(basePath, issues, fixesApplied, options)
658
677
  // block doctor.
659
678
  try {
660
679
  if (isDbAvailable()) {
661
- checkProjectionCheckboxDbStatus(basePath, getAllMilestones().map((milestone) => milestone.id), issues);
680
+ checkProjectionCheckboxDbStatus(basePath, checkboxDbStatusMilestoneIds(basePath, getAllMilestones().map((milestone) => milestone.id)), issues);
662
681
  }
663
682
  }
664
683
  catch {
@@ -38,8 +38,12 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|web
38
38
  // transient errors so auto-mode can retry with reduced budget or fall back to a larger-context model.
39
39
  // See: https://github.com/open-gsd/gsd-pi/issues/4528
40
40
  const SERVER_RE = /internal(?: server)?[ _-]?error|server[ _-]?error|500|502|503|overloaded|server_error|api_error|service.?unavailable|context (?:window|length) exceed|context window exceed/i;
41
+ // Provider adapters throw this when an upstream stop reason reaches Pi before
42
+ // the adapter knows how to map it. Keep the match scoped so plain "unknown"
43
+ // runtime errors still pause for inspection instead of being retried forever.
44
+ const PROVIDER_STOP_REASON_RE = /unhandled stop reason:\s*(?:abort|unknown)\b/i;
41
45
  // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
42
- const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
46
+ const CONNECTION_RE = /terminated|\babort\b|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
43
47
  // Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
44
48
  // This eliminates the need to enumerate every error message variant individually.
45
49
  const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
@@ -72,7 +76,7 @@ const UNSUPPORTED_MODEL_SCOPE_RE = /\b(?:account|plan|tier|subscription)\b/i;
72
76
  * 3. Network (ECONNRESET, ETIMEDOUT, socket hang up, fetch failed, dns)
73
77
  * 4. Stream truncation (malformed JSON from mid-stream cut)
74
78
  * 5. Server (500/502/503, overloaded, server_error)
75
- * 6. Connection (terminated, ECONNREFUSED, EPIPE, other side closed)
79
+ * 6. Connection (terminated, abort/unknown stop reason, ECONNREFUSED, EPIPE, other side closed)
76
80
  * 7. Model error (400 invalid argument / payload rejected for this model)
77
81
  * 8. Unknown
78
82
  */
@@ -124,6 +128,12 @@ export function classifyError(errorMsg, retryAfterMs) {
124
128
  const delayMs = resetMatch ? Number(resetMatch[1]) * 1000 : 60_000;
125
129
  return { kind: "rate-limit", retryAfterMs: delayMs };
126
130
  }
131
+ // Provider stop-reason adapter gaps are usually request/stream failures from
132
+ // the current model. Treat the scoped adapter message as fallback-eligible,
133
+ // but keep generic unknown errors on the indefinite pause path.
134
+ if (PROVIDER_STOP_REASON_RE.test(errorMsg)) {
135
+ return { kind: "connection", retryAfterMs: retryAfterMs ?? 15_000 };
136
+ }
127
137
  // 3. Network errors — same-model retry candidate
128
138
  if (NETWORK_RE.test(errorMsg)) {
129
139
  // Exclude if also matches permanent signals (already handled above for
@@ -33,6 +33,7 @@ export const EXEC_DEFAULTS = {
33
33
  "LOGNAME",
34
34
  "TMPDIR",
35
35
  "NODE_OPTIONS",
36
+ "NODE_PATH",
36
37
  "PYTHONPATH",
37
38
  "PYTHONIOENCODING",
38
39
  ],
@@ -130,6 +130,17 @@ function isInsideFlatProjectionBackup(backupDir, src) {
130
130
  const phaseBackupDir = join(backupDir, "__phases");
131
131
  return src === phaseBackupDir || src.startsWith(`${phaseBackupDir}/`) || src.startsWith(`${phaseBackupDir}\\`);
132
132
  }
133
+ function removeFlatProjectionBackup(backupDir) {
134
+ const phaseBackupDir = join(backupDir, "__phases");
135
+ if (!existsSync(phaseBackupDir))
136
+ return;
137
+ try {
138
+ removePathWithRetries(phaseBackupDir);
139
+ }
140
+ catch (err) {
141
+ logWarning("migration", `flat-phase migration succeeded but could not remove temporary ${LAYOUT_SEGMENTS.level1}/ backup: ${err.message}`);
142
+ }
143
+ }
133
144
  function restoreFlatProjectionFromBackup(basePath, backupDir) {
134
145
  const phaseBackupDir = join(backupDir, "__phases");
135
146
  if (!existsSync(phaseBackupDir))
@@ -433,4 +444,5 @@ export async function migrateToFlatPhase(basePath) {
433
444
  catch (err) {
434
445
  logWarning("migration", `flat-phase migration succeeded but could not remove ${LEGACY_MIGRATING_SEGMENT}: ${err.message}`);
435
446
  }
447
+ removeFlatProjectionBackup(backupDir);
436
448
  }
@@ -38,6 +38,7 @@ export * from "./db/queries.js";
38
38
  export * from "./db/writers/cascades.js";
39
39
  import { TERMINAL_STATUS_SQL } from "./db/sql-constants.js";
40
40
  import { applyStatusTransition } from "./db/writers/status.js";
41
+ import { LAYOUT_SEGMENTS, derivePhaseSlug, milestoneIdToPhaseNum, milestoneIdUniqueSuffix, phaseDirName, } from "./layout-policy.js";
41
42
  export function insertDecision(d) {
42
43
  if (!getDbOrNull())
43
44
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
@@ -169,6 +170,44 @@ export function insertArtifact(a) {
169
170
  ":content_hash": contentHash,
170
171
  });
171
172
  }
173
+ function canonicalPhaseDirNameForDb(milestoneId, title) {
174
+ const phaseNum = milestoneIdToPhaseNum(milestoneId);
175
+ const slug = derivePhaseSlug(title || milestoneId);
176
+ const suffix = milestoneIdUniqueSuffix(milestoneId);
177
+ return phaseDirName(phaseNum, suffix ? `${suffix}-${slug}` : slug);
178
+ }
179
+ function reconcileMilestonePhaseArtifactPaths(milestoneId, title) {
180
+ const db = getDbOrNull();
181
+ const phaseNumPrefix = String(milestoneIdToPhaseNum(milestoneId)).padStart(2, "0");
182
+ const canonicalDir = canonicalPhaseDirNameForDb(milestoneId, title);
183
+ const staleRows = db.prepare(`SELECT path
184
+ FROM artifacts
185
+ WHERE milestone_id = :milestone_id
186
+ AND path LIKE :phase_prefix`).all({
187
+ ":milestone_id": milestoneId,
188
+ ":phase_prefix": `${LAYOUT_SEGMENTS.level1}/${phaseNumPrefix}-%/%`,
189
+ });
190
+ const updatePath = db.prepare("UPDATE artifacts SET path = :new_path WHERE path = :old_path");
191
+ const deletePath = db.prepare("DELETE FROM artifacts WHERE path = :path");
192
+ const existingPath = db.prepare("SELECT 1 AS present FROM artifacts WHERE path = :path");
193
+ for (const row of staleRows) {
194
+ const parts = row.path.split("/");
195
+ if (parts.length < 3)
196
+ continue;
197
+ if (parts[0] !== LAYOUT_SEGMENTS.level1)
198
+ continue;
199
+ if (parts[1] === canonicalDir)
200
+ continue;
201
+ if (!parts[1]?.startsWith(`${phaseNumPrefix}-`))
202
+ continue;
203
+ const newPath = [LAYOUT_SEGMENTS.level1, canonicalDir, ...parts.slice(2)].join("/");
204
+ if (existingPath.get({ ":path": newPath })) {
205
+ deletePath.run({ ":path": row.path });
206
+ continue;
207
+ }
208
+ updatePath.run({ ":new_path": newPath, ":old_path": row.path });
209
+ }
210
+ }
172
211
  export function insertMilestone(m) {
173
212
  if (!getDbOrNull())
174
213
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
@@ -207,37 +246,42 @@ export function insertMilestone(m) {
207
246
  export function upsertMilestonePlanning(milestoneId, planning) {
208
247
  if (!getDbOrNull())
209
248
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
210
- getDbOrNull().prepare(`UPDATE milestones SET
211
- title = COALESCE(NULLIF(:title, ''), title),
212
- status = COALESCE(NULLIF(:status, ''), status),
213
- depends_on = COALESCE(:depends_on, depends_on),
214
- vision = COALESCE(:vision, vision),
215
- success_criteria = COALESCE(:success_criteria, success_criteria),
216
- key_risks = COALESCE(:key_risks, key_risks),
217
- proof_strategy = COALESCE(:proof_strategy, proof_strategy),
218
- verification_contract = COALESCE(:verification_contract, verification_contract),
219
- verification_integration = COALESCE(:verification_integration, verification_integration),
220
- verification_operational = COALESCE(:verification_operational, verification_operational),
221
- verification_uat = COALESCE(:verification_uat, verification_uat),
222
- definition_of_done = COALESCE(:definition_of_done, definition_of_done),
223
- requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
224
- boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
225
- WHERE id = :id`).run({
226
- ":id": milestoneId,
227
- ":title": planning.title ?? "",
228
- ":status": planning.status ?? "",
229
- ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
230
- ":vision": planning.vision ?? null,
231
- ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
232
- ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
233
- ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
234
- ":verification_contract": planning.verificationContract ?? null,
235
- ":verification_integration": planning.verificationIntegration ?? null,
236
- ":verification_operational": planning.verificationOperational ?? null,
237
- ":verification_uat": planning.verificationUat ?? null,
238
- ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
239
- ":requirement_coverage": planning.requirementCoverage ?? null,
240
- ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
249
+ transaction(() => {
250
+ getDbOrNull().prepare(`UPDATE milestones SET
251
+ title = COALESCE(NULLIF(:title, ''), title),
252
+ status = COALESCE(NULLIF(:status, ''), status),
253
+ depends_on = COALESCE(:depends_on, depends_on),
254
+ vision = COALESCE(:vision, vision),
255
+ success_criteria = COALESCE(:success_criteria, success_criteria),
256
+ key_risks = COALESCE(:key_risks, key_risks),
257
+ proof_strategy = COALESCE(:proof_strategy, proof_strategy),
258
+ verification_contract = COALESCE(:verification_contract, verification_contract),
259
+ verification_integration = COALESCE(:verification_integration, verification_integration),
260
+ verification_operational = COALESCE(:verification_operational, verification_operational),
261
+ verification_uat = COALESCE(:verification_uat, verification_uat),
262
+ definition_of_done = COALESCE(:definition_of_done, definition_of_done),
263
+ requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
264
+ boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
265
+ WHERE id = :id`).run({
266
+ ":id": milestoneId,
267
+ ":title": planning.title ?? "",
268
+ ":status": planning.status ?? "",
269
+ ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
270
+ ":vision": planning.vision ?? null,
271
+ ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
272
+ ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
273
+ ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
274
+ ":verification_contract": planning.verificationContract ?? null,
275
+ ":verification_integration": planning.verificationIntegration ?? null,
276
+ ":verification_operational": planning.verificationOperational ?? null,
277
+ ":verification_uat": planning.verificationUat ?? null,
278
+ ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
279
+ ":requirement_coverage": planning.requirementCoverage ?? null,
280
+ ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
281
+ });
282
+ const finalTitle = planning.title?.trim();
283
+ if (finalTitle)
284
+ reconcileMilestonePhaseArtifactPaths(milestoneId, finalTitle);
241
285
  });
242
286
  }
243
287
  export function insertSlice(s) {
@@ -922,7 +922,7 @@ export async function buildDiscussSlicePrompt(mid, sid, sTitle, base, options) {
922
922
  ? `\n\n## Re-discuss Mode\n\nThis slice already has an existing context file (\`${sliceContextPath}\`) from a prior discussion. The user has chosen to re-discuss it. Read the existing context file, interview for any updates, changes, or new decisions, and rewrite the file with merged findings. Do NOT skip the interview — the user explicitly asked to revisit this slice.\n`
923
923
  : "";
924
924
  const inlinedTemplates = inlineTemplate("slice-context", "Slice Context");
925
- return loadPrompt("guided-discuss-slice", {
925
+ return prependLanguageDirective(base, loadPrompt("guided-discuss-slice", {
926
926
  milestoneId: mid,
927
927
  sliceId: sid,
928
928
  sliceTitle: sTitle,
@@ -933,7 +933,7 @@ export async function buildDiscussSlicePrompt(mid, sid, sTitle, base, options) {
933
933
  inlinedTemplates,
934
934
  structuredQuestionsAvailable: options?.structuredQuestionsAvailable ?? "false",
935
935
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}/${sid}): slice context from discuss`),
936
- });
936
+ }));
937
937
  }
938
938
  /**
939
939
  * /gsd discuss — show a picker of non-done slices and run a slice interview.
@@ -11,14 +11,14 @@
11
11
  import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from "node:fs";
12
12
  import { logWarning } from "./workflow-logger.js";
13
13
  import { isClosedStatus } from "./status-guards.js";
14
- import { dirname, join, relative } from "node:path";
14
+ import { dirname, join } from "node:path";
15
15
  import { getAllMilestones, getMilestone, getMilestoneScopedArtifacts, getSliceScopedArtifacts, getMilestoneSlices, getSliceTasks, getTasksBySliceIds, getTask, getSlice, insertArtifact, deleteArtifactByPath, getGateResults, isDbAvailable, } from "./gsd-db.js";
16
16
  import { resolveDir, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, targetMilestoneFile, targetSliceFile, targetTaskFile, gsdProjectionRoot, gsdRoot, buildTaskFileName, } from "./paths.js";
17
17
  import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.js";
18
18
  import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
19
19
  import { invalidateStateCache } from "./state.js";
20
20
  import { clearPathCache, milestonesDir, legacyMilestonesDir, isLegacyMilestonesLayout, resolveMilestonePath, relSliceFile, canonicalPhaseDirName } from "./paths.js";
21
- import { readCompatMarker, writeCompatMarker, computeProjectionSha } from "./compat/compat-marker.js";
21
+ import { readCompatMarker, writeCompatMarker, computeProjectionSha, deriveCompatProjectionKey } from "./compat/compat-marker.js";
22
22
  // ─── Compat marker invalidation ───────────────────────────────────────────
23
23
  // Every successful projection write pushes its (basePath, projectionPath,
24
24
  // entities) here; invalidateCaches() drains it and refreshes .gsd/.compat.json
@@ -71,13 +71,7 @@ function flushProjectionWritesToMarker() {
71
71
  */
72
72
  function toArtifactPath(absPath, basePath) {
73
73
  const projectionRoot = gsdProjectionRoot(basePath);
74
- const projectionRel = relative(projectionRoot, absPath);
75
- const root = projectionRel && !projectionRel.startsWith("..") && !projectionRel.startsWith("/")
76
- ? projectionRoot
77
- : gsdRoot(basePath);
78
- const rel = relative(root, absPath);
79
- // Normalize to forward slashes for consistent DB keys
80
- return rel.replace(/\\/g, "/");
74
+ return deriveCompatProjectionKey(absPath, [projectionRoot, gsdRoot(basePath)]);
81
75
  }
82
76
  /**
83
77
  * Invalidate all caches after a disk write.
@@ -122,6 +122,14 @@ function cachedReaddir(dirPath) {
122
122
  dirListCache.set(dirPath, entries);
123
123
  return entries;
124
124
  }
125
+ function isExistingFile(path) {
126
+ try {
127
+ return statSync(path).isFile();
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
125
133
  /**
126
134
  * Clear the volatile directory listing caches.
127
135
  * Call after milestone transitions, file creation in planning directories,
@@ -257,7 +265,7 @@ export function resolveFile(dir, idPrefix, suffix) {
257
265
  return null;
258
266
  const target = `${idPrefix}-${suffix}.md`.toUpperCase();
259
267
  try {
260
- const entries = cachedReaddir(dir);
268
+ const entries = cachedReaddirWithTypes(dir).filter(e => e.isFile()).map(e => e.name);
261
269
  // Direct match: ID-SUFFIX.md
262
270
  const direct = entries.find(e => e.toUpperCase() === target);
263
271
  if (direct)
@@ -286,7 +294,9 @@ export function resolveTaskFiles(tasksDir, suffix) {
286
294
  if (!existsSync(tasksDir))
287
295
  return [];
288
296
  try {
289
- return cachedReaddir(tasksDir)
297
+ return cachedReaddirWithTypes(tasksDir)
298
+ .filter(e => e.isFile())
299
+ .map(e => e.name)
290
300
  .filter(f => taskIdFromTaskFileName(f, suffix) !== null)
291
301
  .sort();
292
302
  }
@@ -826,7 +836,7 @@ export function resolveMilestoneFile(basePath, milestoneId, suffix) {
826
836
  const prefix = `${String(phaseNum).padStart(2, "0")}`;
827
837
  const flatName = `${prefix}-${suffix}.md`;
828
838
  const flatPath = join(mDir, flatName);
829
- if (existsSync(flatPath))
839
+ if (isExistingFile(flatPath))
830
840
  return flatPath;
831
841
  // Legacy fallback: M001-SUFFIX.md
832
842
  const file = resolveFile(mDir, milestoneId, suffix);
@@ -859,12 +869,12 @@ export function resolveSliceFile(basePath, milestoneId, sliceId, suffix) {
859
869
  const planNum = sliceIdToPlanNum(sliceId);
860
870
  const flatName = planFileName(phaseNum, planNum, suffix);
861
871
  const flatPath = join(phaseDir, flatName);
862
- if (existsSync(flatPath))
872
+ if (isExistingFile(flatPath))
863
873
  return flatPath;
864
874
  // Also check plan-number-only format MM-SUFFIX.md (written by buildSliceFileName)
865
875
  const planOnlyName = `${String(planNum).padStart(2, "0")}-${suffix}.md`;
866
876
  const planOnlyPath = join(phaseDir, planOnlyName);
867
- if (existsSync(planOnlyPath))
877
+ if (isExistingFile(planOnlyPath))
868
878
  return planOnlyPath;
869
879
  // Try prefix match for the phase+plan number (handles suffix variations)
870
880
  const planPrefix = `${String(phaseNum).padStart(2, "0")}-${String(planNum).padStart(2, "0")}-`;
@@ -910,10 +920,10 @@ export function resolveTaskFile(basePath, milestoneId, sliceId, taskId, suffix)
910
920
  const isLegacy = phaseDir.startsWith(legacyBase + "/") || phaseDir.startsWith(legacyBase + "\\");
911
921
  if (suffix !== "PLAN" && !isLegacy) {
912
922
  const flatPath = join(phaseDir, buildFlatTaskFileName(sliceId, taskId, suffix));
913
- if (existsSync(flatPath))
923
+ if (isExistingFile(flatPath))
914
924
  return flatPath;
915
925
  const legacyFlatPath = join(phaseDir, buildTaskFileName(taskId, suffix));
916
- return existsSync(legacyFlatPath) ? legacyFlatPath : null;
926
+ return isExistingFile(legacyFlatPath) ? legacyFlatPath : null;
917
927
  }
918
928
  const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
919
929
  if (tDir) {
@@ -24,18 +24,18 @@ Roadmap, slice summaries, assessments, requirements, decisions, and project cont
24
24
 
25
25
  ### Step 1 - Dispatch Parallel Reviewers
26
26
 
27
- The Inlined Context above already preloads the evidence reviewers need — the roadmap, per-slice SUMMARY/ASSESSMENT excerpts, requirements, and verification classes. **Embed the relevant preloaded evidence directly into each reviewer's task prompt** so reviewers work from it instead of re-reading the same artifacts from disk. Each reviewer should read a full file only when its excerpt is missing, truncated, or internally inconsistent — never as a routine first step. This avoids three reviewers independently re-surveying artifacts the orchestrator already holds.
27
+ The Inlined Context above already preloads the evidence reviewers need — the roadmap, per-slice SUMMARY/ASSESSMENT excerpts, requirements, and verification classes. **Embed the relevant preloaded evidence directly into each reviewer's task prompt** so reviewers work from it instead of re-reading the same artifacts from disk. Each reviewer should read a full file only when its excerpt is missing, truncated, or internally inconsistent — never as a routine first step. When a reviewer needs a full artifact, use only explicit file paths from the On-demand Validation Artifacts / On-demand Milestone Context blocks or discover files with `find .gsd -type f`; never pass a phase, slice, `tasks/`, or `slices/` directory to `read` or `readFileSync`. This avoids three reviewers independently re-surveying artifacts the orchestrator already holds.
28
28
 
29
29
  Call `subagent` with `tasks: [...]` containing ALL THREE reviewers simultaneously:
30
30
 
31
31
  **Reviewer A - Requirements Coverage**
32
- Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Use the preloaded requirements and slice SUMMARY evidence embedded in this task — do not re-read them from disk. For each requirement, mark COVERED, PARTIAL, or MISSING against that evidence. Read a full SUMMARY under `.gsd/milestones/{{milestoneId}}/slices/` only if its preloaded excerpt is missing, truncated, or inconsistent. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
32
+ Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Use the preloaded requirements and slice SUMMARY evidence embedded in this task — do not re-read them from disk. For each requirement, mark COVERED, PARTIAL, or MISSING against that evidence. Read a full SUMMARY only from an explicit file path in the On-demand Validation Artifacts list, or discover candidates with `find .gsd -type f -name '*-SUMMARY.md'`; never read a directory path. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
33
33
 
34
34
  **Reviewer B - Cross-Slice Integration**
35
- Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Use the preloaded roadmap and slice SUMMARY evidence embedded in this task — do not re-read them from disk. Find the boundary map (produces/consumes contracts) in the roadmap. For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Read `{{roadmapPath}}` or a full SUMMARY only if the preloaded evidence is missing, truncated, or inconsistent. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
35
+ Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Use the preloaded roadmap and slice SUMMARY evidence embedded in this task — do not re-read them from disk. Find the boundary map (produces/consumes contracts) in the roadmap. For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Read `{{roadmapPath}}` or a full SUMMARY from the On-demand Validation Artifacts list only if the preloaded evidence is missing, truncated, or inconsistent; never read a directory path. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
36
36
 
37
37
  **Reviewer C - Assessment & Acceptance Criteria**
38
- Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Use the preloaded milestone context, slice SUMMARY, and ASSESSMENT evidence embedded in this task — do not re-read them from disk. Read `.gsd/milestones/{{milestoneId}}/{{milestoneId}}-CONTEXT.md`, a full SUMMARY, or a full ASSESSMENT only if the preloaded excerpt is missing, truncated, or inconsistent. UAT files are specs, not evidence. Verify each criterion maps to passing evidence. Then review the inlined `Verification Classes (from planning)` table. For every planned row in that table, output a `Verification Classes` table with columns `Class | Planned Check | Evidence | Verdict`. Preserve every planned non-empty class row; do not summarize, rename, combine, or omit planned classes. The first cell of each row must be exactly `Contract`, `Integration`, `Operational`, or `UAT` when that class is present in planning. If a planned class lacks evidence, still include its canonical row and mark the verdict NEEDS-ATTENTION or FAIL. If a planned browser/UAT class has no ASSESSMENT with browser/runtime actions and assertions, return NEEDS-ATTENTION. If no verification classes were planned, say that explicitly. Output sections `Acceptance Criteria` with checklist `[ ] Criterion | Evidence`, and `Verification Classes` with the table. End with one-line verdict: PASS if all criteria and classes are covered by evidence, NEEDS-ATTENTION if gaps exist."
38
+ Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Use the preloaded milestone context, slice SUMMARY, and ASSESSMENT evidence embedded in this task — do not re-read them from disk. Read the milestone context from the On-demand Milestone Context file path, and read full SUMMARY or ASSESSMENT files only from explicit paths in the On-demand Validation Artifacts list, if the preloaded excerpt is missing, truncated, or inconsistent. UAT files are specs, not evidence. Verify each criterion maps to passing evidence. Then review the inlined `Verification Classes (from planning)` table. For every planned row in that table, output a `Verification Classes` table with columns `Class | Planned Check | Evidence | Verdict`. Preserve every planned non-empty class row; do not summarize, rename, combine, or omit planned classes. The first cell of each row must be exactly `Contract`, `Integration`, `Operational`, or `UAT` when that class is present in planning. If a planned class lacks evidence, still include its canonical row and mark the verdict NEEDS-ATTENTION or FAIL. If a planned browser/UAT class has no ASSESSMENT with browser/runtime actions and assertions, return NEEDS-ATTENTION. If no verification classes were planned, say that explicitly. Output sections `Acceptance Criteria` with checklist `[ ] Criterion | Evidence`, and `Verification Classes` with the table. End with one-line verdict: PASS if all criteria and classes are covered by evidence, NEEDS-ATTENTION if gaps exist."
39
39
 
40
40
  ### Step 2 - Synthesize Findings
41
41
 
@@ -85,6 +85,6 @@ If verdict is `needs-remediation`:
85
85
 
86
86
  **You MUST call `gsd_validate_milestone` before finishing. Do not manually write `{{validationPath}}`.**
87
87
 
88
- **File system safety:** When scanning milestone directories, use `ls` or `find` first. Never pass a directory path such as `tasks/` or `slices/` directly to `read`; it only accepts files.
88
+ **File system safety:** When scanning milestone directories, use `find .gsd -type f` first and read only files. In flat-phase projects, `.gsd/phases/<NN>-<slug>/` is a directory, not an artifact. Never pass a directory path such as a phase directory, `tasks/`, or `slices/` directly to `read`, `readFile`, or `readFileSync`.
89
89
 
90
90
  When done, say: "Milestone {{milestoneId}} validation complete — verdict: <verdict>." Say this exactly once — if you already said it in a prior message, do not repeat it.