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

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 (177) 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 +11 -11
  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 +11 -11
  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/dist/core/skills.d.ts +5 -0
  115. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/skills.js +54 -0
  117. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  118. package/packages/pi-coding-agent/package.json +7 -7
  119. package/packages/pi-tui/package.json +2 -2
  120. package/packages/rpc-client/package.json +2 -2
  121. package/pkg/package.json +1 -1
  122. package/src/resources/extensions/gsd/auto/loop.ts +48 -4
  123. package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
  124. package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
  125. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  126. package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
  127. package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
  128. package/src/resources/extensions/gsd/auto-start.ts +31 -8
  129. package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
  130. package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
  131. package/src/resources/extensions/gsd/auto.ts +20 -7
  132. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
  133. package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
  134. package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
  135. package/src/resources/extensions/gsd/db/engine.ts +5 -2
  136. package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
  137. package/src/resources/extensions/gsd/error-classifier.ts +13 -2
  138. package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
  139. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
  140. package/src/resources/extensions/gsd/gsd-db.ts +85 -33
  141. package/src/resources/extensions/gsd/guided-flow.ts +2 -2
  142. package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
  143. package/src/resources/extensions/gsd/paths.ts +17 -7
  144. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  145. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
  146. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
  147. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
  148. package/src/resources/extensions/gsd/stop-notice.ts +11 -0
  149. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
  150. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
  151. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
  152. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
  153. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
  154. package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
  155. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
  156. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
  157. package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
  158. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
  159. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
  160. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
  161. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
  162. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
  163. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
  164. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
  165. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
  166. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  167. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
  168. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
  169. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  170. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
  171. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  172. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
  173. package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
  174. package/src/resources/extensions/shared/interview-ui.ts +47 -8
  175. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
  176. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_buildManifest.js +0 -0
  177. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_ssgManifest.js +0 -0
@@ -88,6 +88,7 @@ import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.
88
88
  import { extractVerdict } from "./verdict-parser.js";
89
89
  import { auditOrphanedPreflightStashes } from "./orphan-stash-audit.js";
90
90
  import { parseProject } from "./schemas/parsers.js";
91
+ import { LAYOUT_SEGMENTS } from "./layout-policy.js";
91
92
 
92
93
  import {
93
94
  debugLog,
@@ -152,6 +153,33 @@ export function hasGitIndexLockForTest(basePath: string): boolean {
152
153
  return existsSync(join(basePath, ".git", "index.lock"));
153
154
  }
154
155
 
156
+ function isEmptyDirectory(dirPath: string): boolean {
157
+ try {
158
+ return existsSync(dirPath) && readdirSync(dirPath).length === 0;
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
164
+ export function reconcileFlatPhaseBootstrapLayout(basePath: string): boolean {
165
+ const gsdDir = join(basePath, ".gsd");
166
+ const phasesPath = join(gsdDir, LAYOUT_SEGMENTS.level1);
167
+ const milestonesPath = join(gsdDir, "milestones");
168
+
169
+ if (isEmptyDirectory(milestonesPath)) {
170
+ if (!existsSync(phasesPath)) mkdirSync(phasesPath, { recursive: true });
171
+ rmSync(milestonesPath, { recursive: true, force: true });
172
+ return true;
173
+ }
174
+
175
+ if (!existsSync(phasesPath) && !existsSync(milestonesPath)) {
176
+ mkdirSync(phasesPath, { recursive: true });
177
+ return true;
178
+ }
179
+
180
+ return false;
181
+ }
182
+
155
183
  export function _shouldAbortBootstrapForUnavailableDbForTest(
156
184
  gsdDbPath: string,
157
185
  dbAvailable: boolean,
@@ -1152,19 +1180,14 @@ export async function bootstrapAutoSession(
1152
1180
  ensureGitignore(base, { manageGitignore });
1153
1181
  if (manageGitignore !== false) untrackRuntimeFiles(base);
1154
1182
 
1155
- // Bootstrap milestones/ if it doesn't exist.
1156
- // Check milestones/ directly — ensureGsdSymlink above already created .gsd/,
1157
- // so checking .gsd/ existence would be dead code (#2942).
1158
- const gsdDir = join(base, ".gsd");
1159
- const milestonesPath = join(gsdDir, "milestones");
1160
- if (!existsSync(milestonesPath)) {
1161
- mkdirSync(milestonesPath, { recursive: true });
1183
+ // Bootstrap the flat-phase projection root and clean empty legacy scaffolds.
1184
+ if (reconcileFlatPhaseBootstrapLayout(base)) {
1162
1185
  try {
1163
1186
  nativeAddAll(base);
1164
1187
  nativeCommit(base, "chore: init gsd");
1165
1188
  } catch (err) {
1166
1189
  /* nothing to commit */
1167
- logWarning("engine", `mkdir failed: ${err instanceof Error ? err.message : String(err)}`);
1190
+ logWarning("engine", `layout bootstrap commit failed: ${err instanceof Error ? err.message : String(err)}`);
1168
1191
  }
1169
1192
  }
1170
1193
 
@@ -132,6 +132,8 @@ export function clearInFlightTools(): void {
132
132
  */
133
133
  const TOOL_INVOCATION_ERROR_RE = /Validation failed for tool|Input validation error|Invalid arguments for tool|MCP error -32602|No such tool available|Expected ',' or '\}'(?: after property value)?(?: in JSON)?|Unexpected end of JSON|Unexpected token.*in JSON|does not provide an export named|Named export .* not found|Cannot find module|ERR_MODULE_NOT_FOUND|ERR_MODULE_NOT_EXPORTED|ERR_PACKAGE_PATH_NOT_EXPORTED/i;
134
134
  const DETERMINISTIC_POLICY_ERROR_RE = /(?:^|\b)(?:HARD BLOCK:|Blocked: \/gsd queue is a planning tool|Direct writes to \.gsd\/STATE\.md and \.gsd\/gsd\.db are blocked|This is a mechanical gate)/i;
135
+ const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
136
+ const SCHEDULE_WAKEUP_CONTINUATION_ERROR = "`prompt` is required when `stop` is not true";
135
137
 
136
138
  /**
137
139
  * Matches the runtime's "tool not registered" error. Unlike the deterministic
@@ -151,6 +153,60 @@ export function isToolInvocationError(errorMsg: string): boolean {
151
153
  return TOOL_INVOCATION_ERROR_RE.test(errorMsg) || isDeterministicPolicyError(errorMsg);
152
154
  }
153
155
 
156
+ /**
157
+ * Claude Code validates its native ScheduleWakeup tool outside GSD's schema.
158
+ * Keep this exact and tool-scoped so similarly worded business validation from
159
+ * unrelated tools does not pause auto-mode.
160
+ */
161
+ export function isScheduleWakeupContinuationError(toolName: string, errorMsg: string): boolean {
162
+ return stripMcpToolPrefix(toolName) === SCHEDULE_WAKEUP_TOOL_NAME
163
+ && errorMsg.trim() === SCHEDULE_WAKEUP_CONTINUATION_ERROR;
164
+ }
165
+
166
+ function shouldRecordToolInvocationError(toolName: string, errorMsg: string): boolean {
167
+ return isScheduleWakeupContinuationError(toolName, errorMsg)
168
+ || isToolInvocationError(errorMsg)
169
+ || isQueuedUserMessageSkip(errorMsg);
170
+ }
171
+
172
+ function isRecordedScheduleWakeupContinuationError(recordedError: string | null): boolean {
173
+ if (!recordedError) return false;
174
+
175
+ const separator = recordedError.indexOf(": ");
176
+ return separator >= 0 && isScheduleWakeupContinuationError(
177
+ recordedError.slice(0, separator),
178
+ recordedError.slice(separator + 2),
179
+ );
180
+ }
181
+
182
+ export function updateToolInvocationError(
183
+ recordedError: string | null,
184
+ toolName: string,
185
+ errorMsg: string,
186
+ ): string | null {
187
+ if (!shouldRecordToolInvocationError(toolName, errorMsg)) return recordedError;
188
+ if (
189
+ isRecordedScheduleWakeupContinuationError(recordedError)
190
+ && !isScheduleWakeupContinuationError(toolName, errorMsg)
191
+ ) return recordedError;
192
+
193
+ return `${toolName}: ${errorMsg}`;
194
+ }
195
+
196
+ /**
197
+ * A failed continuation must survive successful diagnostic/read calls in the
198
+ * same turn. Only a later successful ScheduleWakeup proves the continuation
199
+ * was armed; all other invocation errors retain their existing clear-on-success
200
+ * behavior.
201
+ */
202
+ export function shouldClearToolInvocationErrorAfterSuccess(
203
+ recordedError: string | null,
204
+ successfulToolName?: string,
205
+ ): boolean {
206
+ return !isRecordedScheduleWakeupContinuationError(recordedError)
207
+ || stripMcpToolPrefix(successfulToolName ?? "") === SCHEDULE_WAKEUP_TOOL_NAME;
208
+ }
209
+
154
210
  /**
155
211
  * Returns true if the error message indicates the called tool was not on the
156
212
  * session's tool surface (MCP startup race — see TOOL_UNAVAILABLE_ERROR_RE).
@@ -16,7 +16,7 @@ import {
16
16
  _isSamePath as isSamePath,
17
17
  _shouldReconcileWorktreeDb,
18
18
  } from "./auto-worktree-cleanup.js";
19
- import { resolveGsdPathContract } from "./paths.js";
19
+ import { dirIsContentBearingLegacyMilestone, resolveGsdPathContract } from "./paths.js";
20
20
  import type { MilestoneScope } from "./workspace.js";
21
21
  import { WorktreeStateProjection } from "./worktree-state-projection.js";
22
22
  import { logWarning } from "./workflow-logger.js";
@@ -211,10 +211,17 @@ function syncMilestoneLayout(
211
211
  if (!existsSync(mainMilestonesDir)) return;
212
212
 
213
213
  try {
214
- mkdirSync(wtMilestonesDir, { recursive: true });
215
214
  const mainMilestones = readdirSync(mainMilestonesDir, { withFileTypes: true })
216
215
  .filter((entry) => entry.isDirectory())
217
- .map((entry) => entry.name);
216
+ .map((entry) => entry.name)
217
+ .filter((name) =>
218
+ layoutSegment !== "milestones" ||
219
+ dirIsContentBearingLegacyMilestone(join(mainMilestonesDir, name)),
220
+ );
221
+
222
+ if (mainMilestones.length === 0) return;
223
+
224
+ mkdirSync(wtMilestonesDir, { recursive: true });
218
225
 
219
226
  for (const milestoneId of mainMilestones) {
220
227
  syncMilestoneDirectory(
@@ -110,9 +110,7 @@ import {
110
110
  getOldestInFlightToolStart,
111
111
  hasInteractiveToolInFlight,
112
112
  clearInFlightTools,
113
- isToolInvocationError,
114
- isQueuedUserMessageSkip,
115
- isDeterministicPolicyError,
113
+ updateToolInvocationError,
116
114
  } from "./auto-tool-tracking.js";
117
115
  import { closeoutUnit } from "./auto-unit-closeout.js";
118
116
  import { recoverTimedOutUnit } from "./auto-timeout-recovery.js";
@@ -1001,14 +999,17 @@ export function markToolEnd(toolCallId: string): void {
1001
999
  * Called from tool_execution_end when a GSD tool fails with isError.
1002
1000
  * Stores the error if it matches:
1003
1001
  * - tool-invocation-error pattern (malformed/truncated JSON)
1002
+ * - failed ScheduleWakeup continuation validation (#1379)
1004
1003
  * - queued-user-message skip pattern
1005
1004
  * - deterministic policy rejection (#4973, e.g. context_write_blocked)
1006
1005
  */
1007
1006
  export function recordToolInvocationError(toolName: string, errorMsg: string): void {
1008
1007
  if (!s.active) return;
1009
- if (isToolInvocationError(errorMsg) || isQueuedUserMessageSkip(errorMsg) || isDeterministicPolicyError(errorMsg)) {
1010
- s.lastToolInvocationError = `${toolName}: ${errorMsg}`;
1011
- }
1008
+ s.lastToolInvocationError = updateToolInvocationError(
1009
+ s.lastToolInvocationError,
1010
+ toolName,
1011
+ errorMsg,
1012
+ );
1012
1013
  }
1013
1014
 
1014
1015
  export function getOldestInFlightToolAgeMs(): number {
@@ -3162,7 +3163,19 @@ export async function dispatchHookUnit(
3162
3163
  workspaceRoot: s.basePath,
3163
3164
  });
3164
3165
 
3165
- const result = await s.cmdCtx!.newSession({ workspaceRoot: s.basePath });
3166
+ const cmdCtx = s.cmdCtx;
3167
+ if (typeof cmdCtx?.newSession !== "function") {
3168
+ const message = "Auto-mode has no command context for dispatch.";
3169
+ ctx.ui.notify(message, "error");
3170
+ const unitToCommit = previousCurrentUnit ?? s.currentUnit;
3171
+ if (unitToCommit) {
3172
+ await autoCommitUnit(s.basePath, unitToCommit.type, unitToCommit.id, ctx);
3173
+ }
3174
+ await stopAuto(ctx, pi, message, { preserveWorktree: true });
3175
+ return false;
3176
+ }
3177
+
3178
+ const result = await cmdCtx.newSession({ workspaceRoot: s.basePath });
3166
3179
  if (result.cancelled) {
3167
3180
  await stopAuto(ctx, pi);
3168
3181
  return false;
@@ -1769,7 +1769,7 @@ export function registerHooks(
1769
1769
  recordToolInvocationError(event.toolName, errorText);
1770
1770
  recordRetryableHarnessToolError(toolName, resultPayload, errorText);
1771
1771
  } else if (isAutoActive()) {
1772
- clearToolInvocationError();
1772
+ clearToolInvocationError(event.toolName);
1773
1773
  }
1774
1774
  // Interactive Closeout adapter (ADR-032): auto-mode owns closeout for its
1775
1775
  // own units; interactive completions get the durable git subset (commit +
@@ -1980,7 +1980,7 @@ export function registerHooks(
1980
1980
  recordToolInvocationError(event.toolName, errorText);
1981
1981
  recordRetryableHarnessToolError(toolName, event.result, errorText);
1982
1982
  } else if (isAutoActive()) {
1983
- clearToolInvocationError();
1983
+ clearToolInvocationError(event.toolName);
1984
1984
  }
1985
1985
  // Safety harness: record tool execution results for evidence cross-referencing
1986
1986
  if (isAutoActive()) {
@@ -11,6 +11,7 @@ import { executeValidateMilestone } from "./tools/workflow-tool-executors.js";
11
11
  import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
12
12
  import { getLatestAssessmentByScope } from "./gsd-db.js";
13
13
  import { checkpointWorkflowDatabase } from "./db-workspace.js";
14
+ import { formatVerdictRecordedNotice, formatVerdictRejectedNotice } from "./stop-notice.js";
14
15
  import {
15
16
  VALIDATION_VERDICTS,
16
17
  extractVerdict,
@@ -165,17 +166,17 @@ export async function handleVerdict(
165
166
  basePath: string,
166
167
  ): Promise<void> {
167
168
  if (!rawArgs.trim()) {
168
- ctx.ui.notify(USAGE, "warning");
169
+ ctx.ui.notify(formatVerdictRejectedNotice(USAGE), "warning");
169
170
  return;
170
171
  }
171
172
 
172
173
  const parsed = parseArgs(rawArgs);
173
174
  if ("error" in parsed) {
174
- ctx.ui.notify(`${parsed.error}\n${USAGE}`, "warning");
175
+ ctx.ui.notify(formatVerdictRejectedNotice(`${parsed.error}\n${USAGE}`), "warning");
175
176
  return;
176
177
  }
177
178
  if (!parsed.verdict) {
178
- ctx.ui.notify(USAGE, "warning");
179
+ ctx.ui.notify(formatVerdictRejectedNotice(USAGE), "warning");
179
180
  return;
180
181
  }
181
182
 
@@ -184,7 +185,7 @@ export async function handleVerdict(
184
185
  const state = await deriveState(basePath);
185
186
  if (!state.activeMilestone) {
186
187
  ctx.ui.notify(
187
- "No active milestone — pass --milestone Mxxx to target a specific milestone.",
188
+ formatVerdictRejectedNotice("No active milestone — pass --milestone Mxxx to target a specific milestone."),
188
189
  "warning",
189
190
  );
190
191
  return;
@@ -195,7 +196,9 @@ export async function handleVerdict(
195
196
  const existingValidation = await loadExistingValidation(basePath, milestoneId);
196
197
  if (!existingValidation) {
197
198
  ctx.ui.notify(
198
- `No milestone validation found for ${milestoneId}. Run /gsd auto to validate first, then retry /gsd verdict.`,
199
+ formatVerdictRejectedNotice(
200
+ `No milestone validation found for ${milestoneId}. Run /gsd auto to validate first, then retry /gsd verdict.`,
201
+ ),
199
202
  "warning",
200
203
  );
201
204
  return;
@@ -205,7 +208,7 @@ export async function handleVerdict(
205
208
 
206
209
  if (parsed.verdict !== "pass" && !parsed.rationale) {
207
210
  ctx.ui.notify(
208
- `--rationale is required when overriding to ${parsed.verdict}.`,
211
+ formatVerdictRejectedNotice(`--rationale is required when overriding to ${parsed.verdict}.`),
209
212
  "warning",
210
213
  );
211
214
  return;
@@ -234,7 +237,7 @@ export async function handleVerdict(
234
237
  if (result.isError) {
235
238
  const msg =
236
239
  result.content[0]?.type === "text" ? result.content[0].text : "Unknown error";
237
- ctx.ui.notify(msg, "error");
240
+ ctx.ui.notify(formatVerdictRejectedNotice(msg), "error");
238
241
  return;
239
242
  }
240
243
 
@@ -244,12 +247,16 @@ export async function handleVerdict(
244
247
  const effectiveVerdict = extractEffectiveVerdict(result.details, parsed.verdict);
245
248
  if (effectiveVerdict !== parsed.verdict) {
246
249
  ctx.ui.notify(
247
- `Milestone ${milestoneId} verdict requested: ${parsed.verdict}, effective: ${effectiveVerdict} (${existingValidation.source})`,
250
+ formatVerdictRecordedNotice(
251
+ `Milestone ${milestoneId} verdict requested: ${parsed.verdict}, effective: ${effectiveVerdict} (${existingValidation.source})`,
252
+ ),
248
253
  "warning",
249
254
  );
250
255
  } else {
251
256
  ctx.ui.notify(
252
- `Milestone ${milestoneId} verdict: ${prevVerdict} -> ${effectiveVerdict} (${existingValidation.source})`,
257
+ formatVerdictRecordedNotice(
258
+ `Milestone ${milestoneId} verdict: ${prevVerdict} -> ${effectiveVerdict} (${existingValidation.source})`,
259
+ ),
253
260
  "success",
254
261
  );
255
262
  }
@@ -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
  }