@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
@@ -6,8 +6,8 @@
6
6
  // (drift to import). gsd-core is oblivious to this file and ignores it.
7
7
 
8
8
  import { createHash } from "node:crypto";
9
- import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
10
- import { dirname, join, isAbsolute, normalize } from "node:path";
9
+ import { existsSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
10
+ import { dirname, join, isAbsolute, normalize, relative, resolve } from "node:path";
11
11
 
12
12
  /** Current marker schema version. Bump on breaking format changes + migrate. */
13
13
  export const COMPAT_MARKER_SCHEMA = 2;
@@ -54,6 +54,13 @@ export interface CompatMarker {
54
54
  piVersion: string;
55
55
  }
56
56
 
57
+ export interface ReadCompatMarkerOptions {
58
+ /** Rewrite invalid-but-derivable marker keys back to safe root-relative keys. */
59
+ healInvalidKeys?: boolean;
60
+ /** Quarantine malformed/unsafe markers. Disable only for read-only previews. */
61
+ quarantineInvalid?: boolean;
62
+ }
63
+
57
64
  /** Marker returned when no marker exists yet (fresh project, first gsd-pi run). */
58
65
  export const EMPTY_MARKER: CompatMarker = {
59
66
  schema: COMPAT_MARKER_SCHEMA,
@@ -81,6 +88,37 @@ export function computeProjectionSha(content: string): string {
81
88
  return createHash("sha256").update(normalizeForHash(content)).digest("hex").slice(0, 16);
82
89
  }
83
90
 
91
+ function normalizeRealPath(p: string): string {
92
+ try {
93
+ return realpathSync.native(p);
94
+ } catch {
95
+ return resolve(p);
96
+ }
97
+ }
98
+
99
+ function rootRelativeKey(rootPath: string, absPath: string): string | null {
100
+ const root = normalizeRealPath(rootPath);
101
+ const abs = normalizeRealPath(absPath);
102
+ const rel = relative(root, abs);
103
+ if (!rel || rel === ".." || rel.startsWith(`..${sepForRelative(rel)}`) || isAbsolute(rel)) {
104
+ return null;
105
+ }
106
+ return rel.replace(/\\/g, "/");
107
+ }
108
+
109
+ function sepForRelative(rel: string): string {
110
+ return rel.includes("\\") ? "\\" : "/";
111
+ }
112
+
113
+ export function deriveCompatProjectionKey(absPath: string, roots: readonly string[]): string {
114
+ for (const root of roots) {
115
+ const key = rootRelativeKey(root, absPath);
116
+ if (key) return key;
117
+ }
118
+ const fallbackRoot = roots[roots.length - 1] ?? dirname(absPath);
119
+ return relative(fallbackRoot, absPath).replace(/\\/g, "/");
120
+ }
121
+
84
122
  /**
85
123
  * Read & validate the marker. A missing marker → EMPTY_MARKER (treat every
86
124
  * projection as external on next reconcile). A malformed marker is quarantined
@@ -88,7 +126,12 @@ export function computeProjectionSha(content: string): string {
88
126
  * EMPTY_MARKER. A schema-mismatch returns EMPTY_MARKER (forward-compat: refuse
89
127
  * to act on a future format we don't understand).
90
128
  */
91
- export function readCompatMarker(basePath: string): CompatMarker {
129
+ export function readCompatMarker(
130
+ basePath: string,
131
+ options: ReadCompatMarkerOptions = {},
132
+ ): CompatMarker {
133
+ const healInvalidKeys = options.healInvalidKeys ?? true;
134
+ const quarantineInvalid = options.quarantineInvalid ?? true;
92
135
  const path = compatMarkerPath(basePath);
93
136
  if (!existsSync(path)) return emptyMarker();
94
137
 
@@ -103,12 +146,16 @@ export function readCompatMarker(basePath: string): CompatMarker {
103
146
  try {
104
147
  parsed = JSON.parse(raw);
105
148
  } catch {
106
- quarantine(basePath, raw);
149
+ if (quarantineInvalid) quarantine(basePath, raw);
107
150
  return emptyMarker();
108
151
  }
109
152
 
153
+ if (healInvalidKeys && healMarkerProjectionKeys(basePath, parsed) && isValidMarker(parsed)) {
154
+ writeCompatMarker(basePath, parsed);
155
+ }
156
+
110
157
  if (!isValidMarker(parsed)) {
111
- quarantine(basePath, raw);
158
+ if (quarantineInvalid) quarantine(basePath, raw);
112
159
  return emptyMarker();
113
160
  }
114
161
  // Promote older markers by defaulting absent fields. Schema 1 → 2 only adds
@@ -245,6 +292,53 @@ function isValidProjectionMap(x: unknown): boolean {
245
292
  return true;
246
293
  }
247
294
 
295
+ function healMarkerProjectionKeys(basePath: string, marker: unknown): marker is CompatMarker {
296
+ if (typeof marker !== "object" || marker === null) return false;
297
+ const m = marker as Record<string, unknown>;
298
+ let changed = false;
299
+
300
+ if (typeof m.projections === "object" && m.projections !== null) {
301
+ changed = healProjectionMapKeys(basePath, ".gsd", m.projections as Record<string, ProjectionEntry>) || changed;
302
+ }
303
+
304
+ const planning = m.planning;
305
+ if (typeof planning === "object" && planning !== null) {
306
+ const p = planning as Record<string, unknown>;
307
+ if (typeof p.projections === "object" && p.projections !== null) {
308
+ changed = healProjectionMapKeys(basePath, ".planning", p.projections as Record<string, ProjectionEntry>) || changed;
309
+ }
310
+ if (typeof p.passthrough === "object" && p.passthrough !== null) {
311
+ changed = healProjectionMapKeys(basePath, ".planning", p.passthrough as Record<string, ProjectionEntry>) || changed;
312
+ }
313
+ }
314
+
315
+ return changed;
316
+ }
317
+
318
+ function healProjectionMapKeys(
319
+ basePath: string,
320
+ rootName: ".gsd" | ".planning",
321
+ map: Record<string, ProjectionEntry>,
322
+ ): boolean {
323
+ let changed = false;
324
+ const root = join(basePath, rootName);
325
+
326
+ for (const key of Object.keys(map)) {
327
+ if (isSafeProjectionKey(key)) continue;
328
+ if (key.includes("\0") || isAbsolute(key) || /^[A-Za-z]:/.test(key)) continue;
329
+ const healedKey = rootRelativeKey(root, resolve(root, key));
330
+ if (!healedKey || !isSafeProjectionKey(healedKey)) continue;
331
+
332
+ if (!map[healedKey]) {
333
+ map[healedKey] = map[key]!;
334
+ }
335
+ delete map[key];
336
+ changed = true;
337
+ }
338
+
339
+ return changed;
340
+ }
341
+
248
342
  function isValidPlanningMarker(x: unknown): x is PlanningMarker {
249
343
  if (typeof x !== "object" || x === null) return false;
250
344
  const p = x as Record<string, unknown>;
@@ -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 type { GsdWorkspace, MilestoneScope } from "../workspace.js";
16
16
  import { logError, logWarning } from "../workflow-logger.js";
@@ -71,7 +71,10 @@ let _gsdRequire: ReturnType<typeof createRequire> | null | undefined;
71
71
  function getGsdRequire(): ReturnType<typeof createRequire> | null {
72
72
  if (_gsdRequire !== undefined) return _gsdRequire;
73
73
  try {
74
- _gsdRequire = createRequire(import.meta.url);
74
+ // Next.js may emit this module into a CommonJS chunk. Avoid ESM-only module
75
+ // metadata syntax here; it is a hard parse error there.
76
+ const packageRoot = process.env.GSD_WEB_PACKAGE_ROOT || process.env.GSD_PKG_ROOT || process.cwd();
77
+ _gsdRequire = createRequire(resolve(packageRoot, "package.json"));
75
78
  } catch {
76
79
  _gsdRequire = null;
77
80
  }
@@ -95,6 +95,25 @@ function reportCheckboxDbStatusDivergence(
95
95
  });
96
96
  }
97
97
 
98
+ function bareDuplicateMilestoneId(milestoneId: string): string | null {
99
+ const match = milestoneId.match(/^(M\d{3})-[a-z0-9]{6}$/i);
100
+ return match?.[1]?.toUpperCase() ?? null;
101
+ }
102
+
103
+ function checkboxDbStatusMilestoneIds(basePath: string, milestoneIds: string[]): string[] {
104
+ if (!hasFlatPhaseLayout(basePath)) return milestoneIds;
105
+
106
+ // Flat-phase directories are keyed by phase number. A restored
107
+ // M003-xxxxxx row alongside M003 is a stale duplicate for the same
108
+ // phases/03-* projection, so checking both can compare DB status against two
109
+ // competing PLAN files and report a persistent false divergence.
110
+ const allIds = new Set(milestoneIds.map((id) => id.toUpperCase()));
111
+ return milestoneIds.filter((milestoneId) => {
112
+ const bareId = bareDuplicateMilestoneId(milestoneId);
113
+ return !bareId || !allIds.has(bareId);
114
+ });
115
+ }
116
+
98
117
  function checkProjectionCheckboxDbStatus(basePath: string, milestoneIds: string[], issues: DoctorIssue[]): void {
99
118
  for (const milestoneId of milestoneIds) {
100
119
  const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
@@ -434,7 +453,9 @@ function staleMilestonesArtifactRowFixable(basePath: string, row: ArtifactRow, a
434
453
 
435
454
  function stalePhasesArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
436
455
  const issuePath = artifactPathRelativeToGsd(row.path);
437
- return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) && hasPresentMilestonesReplacement(basePath, row, artifactRows);
456
+ return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) &&
457
+ (hasPresentFlatPhaseReplacement(basePath, row, artifactRows) ||
458
+ hasPresentMilestonesReplacement(basePath, row, artifactRows));
438
459
  }
439
460
 
440
461
  function staleArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
@@ -807,7 +828,11 @@ export async function checkEngineHealth(
807
828
  // block doctor.
808
829
  try {
809
830
  if (isDbAvailable()) {
810
- checkProjectionCheckboxDbStatus(basePath, getAllMilestones().map((milestone) => milestone.id), issues);
831
+ checkProjectionCheckboxDbStatus(
832
+ basePath,
833
+ checkboxDbStatusMilestoneIds(basePath, getAllMilestones().map((milestone) => milestone.id)),
834
+ issues,
835
+ );
811
836
  }
812
837
  } catch {
813
838
  // Non-fatal: checkbox-vs-DB divergence check must never block doctor
@@ -65,8 +65,12 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|web
65
65
  // transient errors so auto-mode can retry with reduced budget or fall back to a larger-context model.
66
66
  // See: https://github.com/open-gsd/gsd-pi/issues/4528
67
67
  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;
68
+ // Provider adapters throw this when an upstream stop reason reaches Pi before
69
+ // the adapter knows how to map it. Keep the match scoped so plain "unknown"
70
+ // runtime errors still pause for inspection instead of being retried forever.
71
+ const PROVIDER_STOP_REASON_RE = /unhandled stop reason:\s*(?:abort|unknown)\b/i;
68
72
  // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
69
- const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
73
+ const CONNECTION_RE = /terminated|\babort\b|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
70
74
  // Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
71
75
  // This eliminates the need to enumerate every error message variant individually.
72
76
  const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
@@ -103,7 +107,7 @@ const UNSUPPORTED_MODEL_SCOPE_RE = /\b(?:account|plan|tier|subscription)\b/i;
103
107
  * 3. Network (ECONNRESET, ETIMEDOUT, socket hang up, fetch failed, dns)
104
108
  * 4. Stream truncation (malformed JSON from mid-stream cut)
105
109
  * 5. Server (500/502/503, overloaded, server_error)
106
- * 6. Connection (terminated, ECONNREFUSED, EPIPE, other side closed)
110
+ * 6. Connection (terminated, abort/unknown stop reason, ECONNREFUSED, EPIPE, other side closed)
107
111
  * 7. Model error (400 invalid argument / payload rejected for this model)
108
112
  * 8. Unknown
109
113
  */
@@ -163,6 +167,13 @@ export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorCla
163
167
  return { kind: "rate-limit", retryAfterMs: delayMs };
164
168
  }
165
169
 
170
+ // Provider stop-reason adapter gaps are usually request/stream failures from
171
+ // the current model. Treat the scoped adapter message as fallback-eligible,
172
+ // but keep generic unknown errors on the indefinite pause path.
173
+ if (PROVIDER_STOP_REASON_RE.test(errorMsg)) {
174
+ return { kind: "connection", retryAfterMs: retryAfterMs ?? 15_000 };
175
+ }
176
+
166
177
  // 3. Network errors — same-model retry candidate
167
178
  if (NETWORK_RE.test(errorMsg)) {
168
179
  // Exclude if also matches permanent signals (already handled above for
@@ -111,6 +111,7 @@ export const EXEC_DEFAULTS = {
111
111
  "LOGNAME",
112
112
  "TMPDIR",
113
113
  "NODE_OPTIONS",
114
+ "NODE_PATH",
114
115
  "PYTHONPATH",
115
116
  "PYTHONIOENCODING",
116
117
  ] as const,
@@ -155,6 +155,19 @@ function isInsideFlatProjectionBackup(backupDir: string, src: string): boolean {
155
155
  return src === phaseBackupDir || src.startsWith(`${phaseBackupDir}/`) || src.startsWith(`${phaseBackupDir}\\`);
156
156
  }
157
157
 
158
+ function removeFlatProjectionBackup(backupDir: string): void {
159
+ const phaseBackupDir = join(backupDir, "__phases");
160
+ if (!existsSync(phaseBackupDir)) return;
161
+ try {
162
+ removePathWithRetries(phaseBackupDir);
163
+ } catch (err) {
164
+ logWarning(
165
+ "migration",
166
+ `flat-phase migration succeeded but could not remove temporary ${LAYOUT_SEGMENTS.level1}/ backup: ${(err as Error).message}`,
167
+ );
168
+ }
169
+ }
170
+
158
171
  function restoreFlatProjectionFromBackup(basePath: string, backupDir: string): void {
159
172
  const phaseBackupDir = join(backupDir, "__phases");
160
173
  if (!existsSync(phaseBackupDir)) return;
@@ -494,4 +507,5 @@ export async function migrateToFlatPhase(basePath: string): Promise<void> {
494
507
  `flat-phase migration succeeded but could not remove ${LEGACY_MIGRATING_SEGMENT}: ${(err as Error).message}`,
495
508
  );
496
509
  }
510
+ removeFlatProjectionBackup(backupDir);
497
511
  }
@@ -67,6 +67,13 @@ export type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
67
67
 
68
68
  import { TERMINAL_STATUS_SQL } from "./db/sql-constants.js";
69
69
  import { applyStatusTransition } from "./db/writers/status.js";
70
+ import {
71
+ LAYOUT_SEGMENTS,
72
+ derivePhaseSlug,
73
+ milestoneIdToPhaseNum,
74
+ milestoneIdUniqueSuffix,
75
+ phaseDirName,
76
+ } from "./layout-policy.js";
70
77
 
71
78
  export function insertDecision(d: Omit<Decision, "seq">): void {
72
79
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
@@ -206,6 +213,47 @@ export function insertArtifact(a: {
206
213
  });
207
214
  }
208
215
 
216
+ function canonicalPhaseDirNameForDb(milestoneId: string, title: string): string {
217
+ const phaseNum = milestoneIdToPhaseNum(milestoneId);
218
+ const slug = derivePhaseSlug(title || milestoneId);
219
+ const suffix = milestoneIdUniqueSuffix(milestoneId);
220
+ return phaseDirName(phaseNum, suffix ? `${suffix}-${slug}` : slug);
221
+ }
222
+
223
+ function reconcileMilestonePhaseArtifactPaths(milestoneId: string, title: string): void {
224
+ const db = getDbOrNull()!;
225
+ const phaseNumPrefix = String(milestoneIdToPhaseNum(milestoneId)).padStart(2, "0");
226
+ const canonicalDir = canonicalPhaseDirNameForDb(milestoneId, title);
227
+ const staleRows = db.prepare(
228
+ `SELECT path
229
+ FROM artifacts
230
+ WHERE milestone_id = :milestone_id
231
+ AND path LIKE :phase_prefix`,
232
+ ).all({
233
+ ":milestone_id": milestoneId,
234
+ ":phase_prefix": `${LAYOUT_SEGMENTS.level1}/${phaseNumPrefix}-%/%`,
235
+ }) as Array<{ path: string }>;
236
+
237
+ const updatePath = db.prepare("UPDATE artifacts SET path = :new_path WHERE path = :old_path");
238
+ const deletePath = db.prepare("DELETE FROM artifacts WHERE path = :path");
239
+ const existingPath = db.prepare("SELECT 1 AS present FROM artifacts WHERE path = :path");
240
+
241
+ for (const row of staleRows) {
242
+ const parts = row.path.split("/");
243
+ if (parts.length < 3) continue;
244
+ if (parts[0] !== LAYOUT_SEGMENTS.level1) continue;
245
+ if (parts[1] === canonicalDir) continue;
246
+ if (!parts[1]?.startsWith(`${phaseNumPrefix}-`)) continue;
247
+
248
+ const newPath = [LAYOUT_SEGMENTS.level1, canonicalDir, ...parts.slice(2)].join("/");
249
+ if (existingPath.get({ ":path": newPath })) {
250
+ deletePath.run({ ":path": row.path });
251
+ continue;
252
+ }
253
+ updatePath.run({ ":new_path": newPath, ":old_path": row.path });
254
+ }
255
+ }
256
+
209
257
  export interface MilestonePlanningRecord {
210
258
  vision: string;
211
259
  successCriteria: string[];
@@ -287,39 +335,43 @@ export function insertMilestone(m: {
287
335
 
288
336
  export function upsertMilestonePlanning(milestoneId: string, planning: Partial<MilestonePlanningRecord> & { title?: string; status?: string; depends_on?: string[] }): void {
289
337
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
290
- getDbOrNull()!.prepare(
291
- `UPDATE milestones SET
292
- title = COALESCE(NULLIF(:title, ''), title),
293
- status = COALESCE(NULLIF(:status, ''), status),
294
- depends_on = COALESCE(:depends_on, depends_on),
295
- vision = COALESCE(:vision, vision),
296
- success_criteria = COALESCE(:success_criteria, success_criteria),
297
- key_risks = COALESCE(:key_risks, key_risks),
298
- proof_strategy = COALESCE(:proof_strategy, proof_strategy),
299
- verification_contract = COALESCE(:verification_contract, verification_contract),
300
- verification_integration = COALESCE(:verification_integration, verification_integration),
301
- verification_operational = COALESCE(:verification_operational, verification_operational),
302
- verification_uat = COALESCE(:verification_uat, verification_uat),
303
- definition_of_done = COALESCE(:definition_of_done, definition_of_done),
304
- requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
305
- boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
306
- WHERE id = :id`,
307
- ).run({
308
- ":id": milestoneId,
309
- ":title": planning.title ?? "",
310
- ":status": planning.status ?? "",
311
- ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
312
- ":vision": planning.vision ?? null,
313
- ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
314
- ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
315
- ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
316
- ":verification_contract": planning.verificationContract ?? null,
317
- ":verification_integration": planning.verificationIntegration ?? null,
318
- ":verification_operational": planning.verificationOperational ?? null,
319
- ":verification_uat": planning.verificationUat ?? null,
320
- ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
321
- ":requirement_coverage": planning.requirementCoverage ?? null,
322
- ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
338
+ transaction(() => {
339
+ getDbOrNull()!.prepare(
340
+ `UPDATE milestones SET
341
+ title = COALESCE(NULLIF(:title, ''), title),
342
+ status = COALESCE(NULLIF(:status, ''), status),
343
+ depends_on = COALESCE(:depends_on, depends_on),
344
+ vision = COALESCE(:vision, vision),
345
+ success_criteria = COALESCE(:success_criteria, success_criteria),
346
+ key_risks = COALESCE(:key_risks, key_risks),
347
+ proof_strategy = COALESCE(:proof_strategy, proof_strategy),
348
+ verification_contract = COALESCE(:verification_contract, verification_contract),
349
+ verification_integration = COALESCE(:verification_integration, verification_integration),
350
+ verification_operational = COALESCE(:verification_operational, verification_operational),
351
+ verification_uat = COALESCE(:verification_uat, verification_uat),
352
+ definition_of_done = COALESCE(:definition_of_done, definition_of_done),
353
+ requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
354
+ boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
355
+ WHERE id = :id`,
356
+ ).run({
357
+ ":id": milestoneId,
358
+ ":title": planning.title ?? "",
359
+ ":status": planning.status ?? "",
360
+ ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
361
+ ":vision": planning.vision ?? null,
362
+ ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
363
+ ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
364
+ ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
365
+ ":verification_contract": planning.verificationContract ?? null,
366
+ ":verification_integration": planning.verificationIntegration ?? null,
367
+ ":verification_operational": planning.verificationOperational ?? null,
368
+ ":verification_uat": planning.verificationUat ?? null,
369
+ ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
370
+ ":requirement_coverage": planning.requirementCoverage ?? null,
371
+ ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
372
+ });
373
+ const finalTitle = planning.title?.trim();
374
+ if (finalTitle) reconcileMilestonePhaseArtifactPaths(milestoneId, finalTitle);
323
375
  });
324
376
  }
325
377
 
@@ -1282,7 +1282,7 @@ export async function buildDiscussSlicePrompt(
1282
1282
  : "";
1283
1283
 
1284
1284
  const inlinedTemplates = inlineTemplate("slice-context", "Slice Context");
1285
- return loadPrompt("guided-discuss-slice", {
1285
+ return prependLanguageDirective(base, loadPrompt("guided-discuss-slice", {
1286
1286
  milestoneId: mid,
1287
1287
  sliceId: sid,
1288
1288
  sliceTitle: sTitle,
@@ -1293,7 +1293,7 @@ export async function buildDiscussSlicePrompt(
1293
1293
  inlinedTemplates,
1294
1294
  structuredQuestionsAvailable: options?.structuredQuestionsAvailable ?? "false",
1295
1295
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}/${sid}): slice context from discuss`),
1296
- });
1296
+ }));
1297
1297
  }
1298
1298
 
1299
1299
  /**
@@ -12,7 +12,7 @@
12
12
  import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from "node:fs";
13
13
  import { logWarning } from "./workflow-logger.js";
14
14
  import { isClosedStatus } from "./status-guards.js";
15
- import { dirname, join, relative } from "node:path";
15
+ import { dirname, join } from "node:path";
16
16
  import {
17
17
  getAllMilestones,
18
18
  getMilestone,
@@ -50,7 +50,7 @@ import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.j
50
50
  import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
51
51
  import { invalidateStateCache } from "./state.js";
52
52
  import { clearPathCache, milestonesDir, legacyMilestonesDir, isLegacyMilestonesLayout, resolveMilestonePath, relSliceFile, canonicalPhaseDirName } from "./paths.js";
53
- import { readCompatMarker, writeCompatMarker, computeProjectionSha } from "./compat/compat-marker.js";
53
+ import { readCompatMarker, writeCompatMarker, computeProjectionSha, deriveCompatProjectionKey } from "./compat/compat-marker.js";
54
54
  import type { RiskLevel } from "./types.js";
55
55
  import {
56
56
  phaseDirName,
@@ -109,13 +109,7 @@ function flushProjectionWritesToMarker(): void {
109
109
  */
110
110
  function toArtifactPath(absPath: string, basePath: string): string {
111
111
  const projectionRoot = gsdProjectionRoot(basePath);
112
- const projectionRel = relative(projectionRoot, absPath);
113
- const root = projectionRel && !projectionRel.startsWith("..") && !projectionRel.startsWith("/")
114
- ? projectionRoot
115
- : gsdRoot(basePath);
116
- const rel = relative(root, absPath);
117
- // Normalize to forward slashes for consistent DB keys
118
- return rel.replace(/\\/g, "/");
112
+ return deriveCompatProjectionKey(absPath, [projectionRoot, gsdRoot(basePath)]);
119
113
  }
120
114
 
121
115
  /**
@@ -136,6 +136,14 @@ function cachedReaddir(dirPath: string): string[] {
136
136
  return entries;
137
137
  }
138
138
 
139
+ function isExistingFile(path: string): boolean {
140
+ try {
141
+ return statSync(path).isFile();
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
139
147
  /**
140
148
  * Clear the volatile directory listing caches.
141
149
  * Call after milestone transitions, file creation in planning directories,
@@ -284,7 +292,7 @@ export function resolveFile(dir: string, idPrefix: string, suffix: string): stri
284
292
  if (!existsSync(dir)) return null;
285
293
  const target = `${idPrefix}-${suffix}.md`.toUpperCase();
286
294
  try {
287
- const entries = cachedReaddir(dir);
295
+ const entries = cachedReaddirWithTypes(dir).filter(e => e.isFile()).map(e => e.name);
288
296
  // Direct match: ID-SUFFIX.md
289
297
  const direct = entries.find(e => e.toUpperCase() === target);
290
298
  if (direct) return direct;
@@ -311,7 +319,9 @@ export function resolveFile(dir: string, idPrefix: string, suffix: string): stri
311
319
  export function resolveTaskFiles(tasksDir: string, suffix: string): string[] {
312
320
  if (!existsSync(tasksDir)) return [];
313
321
  try {
314
- return cachedReaddir(tasksDir)
322
+ return cachedReaddirWithTypes(tasksDir)
323
+ .filter(e => e.isFile())
324
+ .map(e => e.name)
315
325
  .filter(f => taskIdFromTaskFileName(f, suffix) !== null)
316
326
  .sort();
317
327
  } catch {
@@ -860,7 +870,7 @@ export function resolveMilestoneFile(
860
870
  const prefix = `${String(phaseNum).padStart(2, "0")}`;
861
871
  const flatName = `${prefix}-${suffix}.md`;
862
872
  const flatPath = join(mDir, flatName);
863
- if (existsSync(flatPath)) return flatPath;
873
+ if (isExistingFile(flatPath)) return flatPath;
864
874
  // Legacy fallback: M001-SUFFIX.md
865
875
  const file = resolveFile(mDir, milestoneId, suffix);
866
876
  return file ? join(mDir, file) : null;
@@ -895,11 +905,11 @@ export function resolveSliceFile(
895
905
  const planNum = sliceIdToPlanNum(sliceId);
896
906
  const flatName = planFileName(phaseNum, planNum, suffix);
897
907
  const flatPath = join(phaseDir, flatName);
898
- if (existsSync(flatPath)) return flatPath;
908
+ if (isExistingFile(flatPath)) return flatPath;
899
909
  // Also check plan-number-only format MM-SUFFIX.md (written by buildSliceFileName)
900
910
  const planOnlyName = `${String(planNum).padStart(2, "0")}-${suffix}.md`;
901
911
  const planOnlyPath = join(phaseDir, planOnlyName);
902
- if (existsSync(planOnlyPath)) return planOnlyPath;
912
+ if (isExistingFile(planOnlyPath)) return planOnlyPath;
903
913
  // Try prefix match for the phase+plan number (handles suffix variations)
904
914
  const planPrefix = `${String(phaseNum).padStart(2, "0")}-${String(planNum).padStart(2, "0")}-`;
905
915
  try {
@@ -949,9 +959,9 @@ export function resolveTaskFile(
949
959
 
950
960
  if (suffix !== "PLAN" && !isLegacy) {
951
961
  const flatPath = join(phaseDir, buildFlatTaskFileName(sliceId, taskId, suffix));
952
- if (existsSync(flatPath)) return flatPath;
962
+ if (isExistingFile(flatPath)) return flatPath;
953
963
  const legacyFlatPath = join(phaseDir, buildTaskFileName(taskId, suffix));
954
- return existsSync(legacyFlatPath) ? legacyFlatPath : null;
964
+ return isExistingFile(legacyFlatPath) ? legacyFlatPath : null;
955
965
  }
956
966
 
957
967
  const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
@@ -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.