@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
@@ -242,6 +242,31 @@ test("readCompatMarker preserves legitimate nested projection keys", () => {
242
242
  assert.equal(Object.keys(marker.projections).length, 2);
243
243
  });
244
244
 
245
+ test("readCompatMarker heals invalid keys that canonicalize back under .gsd", () => {
246
+ const base = makeTmpBase();
247
+ const safeKey = "m1/roadmap.md";
248
+ mkdirSync(join(base, ".gsd", "m1"), { recursive: true });
249
+ writeFileSync(join(base, ".gsd", safeKey), "# Roadmap\n", "utf-8");
250
+ writeCompatMarker(base, {
251
+ schema: 2,
252
+ lastWriter: "gsd-pi",
253
+ lastProjectedAt: "2026-07-07T00:00:00.000Z",
254
+ projections: {
255
+ [`../.gsd/${safeKey}`]: { sha: "aaaaaaaaaaaaaaaa", entities: ["M001"] },
256
+ },
257
+ planning: { active: false, layout: null, projections: {}, passthrough: {} },
258
+ piVersion: "1.8.1",
259
+ });
260
+
261
+ const marker = readCompatMarker(base);
262
+ assert.ok(marker.projections[safeKey], "derivable traversal-form key must be rewritten");
263
+ assert.equal(marker.projections[`../.gsd/${safeKey}`], undefined);
264
+ assert.ok(
265
+ !readdirSync(join(base, ".gsd")).some((f: string) => f.startsWith(".compat.json.bad-")),
266
+ "healed marker must not be quarantined",
267
+ );
268
+ });
269
+
245
270
  test("an escaping key in planning.passthrough also invalidates the whole marker", () => {
246
271
  const base = makeTmpBase();
247
272
  writeCompatMarker(base, {
@@ -0,0 +1,12 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ test("db engine stays free of import.meta syntax for CommonJS chunks", () => {
8
+ const enginePath = join(dirname(fileURLToPath(import.meta.url)), "..", "db", "engine.ts");
9
+ const source = readFileSync(enginePath, "utf-8");
10
+
11
+ assert.doesNotMatch(source, /\bimport\.meta\b/);
12
+ });
@@ -167,6 +167,45 @@ test("checkEngineHealth keeps PLAN checkbox divergence after stale projection fl
167
167
  assert.match(readFileSync(plan.planPath, "utf-8"), /- \[ \] \*\*T01\*\*:/);
168
168
  });
169
169
 
170
+ test("checkEngineHealth ignores stale suffixed flat-phase duplicate when bare milestone exists", async (t) => {
171
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-flat-duplicate-"));
172
+ t.after(() => rmSync(base, { recursive: true, force: true }));
173
+
174
+ const gsdDir = join(base, ".gsd");
175
+ mkdirSync(gsdDir, { recursive: true });
176
+
177
+ openDatabase(join(gsdDir, "gsd.db"));
178
+ insertMilestone({ id: "M003", title: "M003-vaz73w: New milestone M003-vaz73w", status: "active" });
179
+ insertSlice({ id: "S01", milestoneId: "M003", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
180
+ for (const taskId of ["T01", "T02", "T03"]) {
181
+ insertTask({ id: taskId, milestoneId: "M003", sliceId: "S01", title: taskId, status: "complete", sequence: Number(taskId.slice(1)) });
182
+ }
183
+
184
+ insertMilestone({ id: "M003-vaz73w", title: "New milestone M003-vaz73w", status: "active" });
185
+ insertSlice({ id: "S01", milestoneId: "M003-vaz73w", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
186
+ for (const taskId of ["T01", "T02", "T03"]) {
187
+ insertTask({ id: taskId, milestoneId: "M003-vaz73w", sliceId: "S01", title: taskId, status: "complete", sequence: Number(taskId.slice(1)) });
188
+ }
189
+
190
+ const canonicalPlan = await renderPlanFromDb(base, "M003", "S01");
191
+ const staleDir = join(gsdDir, "phases", "03-vaz73w-new-milestone-m003-vaz73w");
192
+ mkdirSync(staleDir, { recursive: true });
193
+ writeFileSync(
194
+ join(staleDir, "03-01-PLAN.md"),
195
+ canonicalPlan.content.replace("- [x] **T03**:", "- [ ] **T03**:"),
196
+ "utf-8",
197
+ );
198
+
199
+ const issues: any[] = [];
200
+ await checkEngineHealth(base, issues, []);
201
+
202
+ assert.deepEqual(
203
+ issues.filter((issue) => issue.code === "checkbox_db_status_divergence").map((issue) => issue.unitId),
204
+ [],
205
+ "stale suffixed duplicate phase projection must not compete with the bare milestone projection",
206
+ );
207
+ });
208
+
170
209
  test("checkEngineHealth reads task checkboxes from the <tasks> block, not a stray line above it", async (t) => {
171
210
  const base = mkdtempSync(join(tmpdir(), "gsd-doctor-task-section-"));
172
211
  t.after(() => rmSync(base, { recursive: true, force: true }));
@@ -428,6 +467,50 @@ test("checkEngineHealth repair prunes stale phases artifact rows with present mi
428
467
  assert.deepEqual(rows.map((row) => row.path), []);
429
468
  });
430
469
 
470
+ test("checkEngineHealth repair prunes stale phases artifact rows with renamed flat-phase files", async (t) => {
471
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-prune-renamed-flat-phase-artifact-"));
472
+ t.after(() => rmSync(base, { recursive: true, force: true }));
473
+
474
+ const gsdDir = join(base, ".gsd");
475
+ const stalePath = "phases/01-new-milestone-m001/01-01-PLAN.md";
476
+ const replacementPath = "phases/01-lokably-brand-foundation-and-welcome-pag/01-01-PLAN.md";
477
+ mkdirSync(join(gsdDir, "phases", "01-lokably-brand-foundation-and-welcome-pag"), { recursive: true });
478
+ writeFileSync(join(gsdDir, replacementPath), "# Plan\n", "utf-8");
479
+
480
+ openDatabase(join(gsdDir, "gsd.db"));
481
+ insertArtifact({
482
+ path: stalePath,
483
+ artifact_type: "PLAN",
484
+ milestone_id: "M001",
485
+ slice_id: "S01",
486
+ task_id: null,
487
+ full_content: "# stale plan\n",
488
+ });
489
+
490
+ const beforeIssues: any[] = [];
491
+ await checkEngineHealth(base, beforeIssues, []);
492
+
493
+ const beforeIssue = beforeIssues.find((issue) => issue.code === "artifact_file_missing" && issue.file === stalePath);
494
+ assert.ok(beforeIssue, "stale renamed phases row should be reported when repair is off");
495
+ assert.equal(beforeIssue.fixable, true, "renamed phases rows with a flat replacement should be fixable");
496
+
497
+ const repairIssues: any[] = [];
498
+ const fixes: string[] = [];
499
+ await checkEngineHealth(base, repairIssues, fixes, { repair: true });
500
+
501
+ assert.equal(
502
+ repairIssues.some((issue) => issue.code === "artifact_file_missing" && issue.file === stalePath),
503
+ false,
504
+ "repair should not report stale rows it pruned",
505
+ );
506
+ assert.ok(fixes.includes(`pruned stale flat-phase artifact row ${stalePath}`));
507
+
508
+ const rows = _getAdapter()!
509
+ .prepare("SELECT path FROM artifacts ORDER BY path")
510
+ .all() as Array<{ path: string }>;
511
+ assert.deepEqual(rows.map((row) => row.path), []);
512
+ });
513
+
431
514
  test("checkEngineHealth repair keeps phases rows whose own file is still present", async (t) => {
432
515
  const base = mkdtempSync(join(tmpdir(), "gsd-doctor-keep-present-phase-artifact-"));
433
516
  t.after(() => rmSync(base, { recursive: true, force: true }));
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { test } from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
- import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
6
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join } from 'node:path';
9
9
 
@@ -152,6 +152,38 @@ test('runExecSandbox: node runtime executes JS', async () => {
152
152
  }
153
153
  });
154
154
 
155
+ test('runExecSandbox: node runtime forwards NODE_PATH for GSD-installed dependencies', async () => {
156
+ const base = freshBase();
157
+ try {
158
+ const dependencyRoot = join(base, 'parent-node_modules');
159
+ const packageDir = join(dependencyRoot, 'fake-gsd-dep');
160
+ const worktree = join(base, 'project', '.gsd', 'worktrees', 'M003');
161
+ mkdirSync(packageDir, { recursive: true });
162
+ mkdirSync(worktree, { recursive: true });
163
+ writeFileSync(
164
+ join(packageDir, 'package.json'),
165
+ JSON.stringify({ name: 'fake-gsd-dep', version: '1.0.0', main: 'index.js' }),
166
+ );
167
+ writeFileSync(join(packageDir, 'index.js'), "module.exports = 'resolved-via-node-path';\n");
168
+
169
+ const result = await runExecSandbox(
170
+ { runtime: 'node', script: "console.log(require('fake-gsd-dep'))" },
171
+ baseOpts(worktree, {
172
+ env: {
173
+ PATH: '/usr/bin:/bin',
174
+ HOME: '/tmp',
175
+ NODE_PATH: dependencyRoot,
176
+ },
177
+ }),
178
+ );
179
+
180
+ assert.equal(result.exit_code, 0, readFileSync(result.stderr_path, 'utf-8'));
181
+ assert.ok(result.digest.includes('resolved-via-node-path'));
182
+ } finally {
183
+ cleanup(base);
184
+ }
185
+ });
186
+
155
187
  test('runExecSandbox: rewrites NUL redirects for bash on Windows', async () => {
156
188
  const originalPlatform = process.platform;
157
189
  Object.defineProperty(process, 'platform', { value: 'win32' });
@@ -8,7 +8,8 @@ import { tmpdir } from "node:os";
8
8
  import { randomUUID } from "node:crypto";
9
9
 
10
10
  import { externalMarkdownEditHandler } from "../state-reconciliation/drift/external-markdown-edit.ts";
11
- import { writeCompatMarker, computeProjectionSha } from "../compat/compat-marker.ts";
11
+ import { readCompatMarker, writeCompatMarker, computeProjectionSha } from "../compat/compat-marker.ts";
12
+ import { closeDatabase, insertArtifact, openDatabase } from "../gsd-db.ts";
12
13
  import type { DriftContext } from "../state-reconciliation/types.ts";
13
14
  import type { GSDState } from "../types.ts";
14
15
 
@@ -28,6 +29,11 @@ function ctx(base: string): DriftContext {
28
29
  }
29
30
 
30
31
  afterEach(() => {
32
+ try {
33
+ closeDatabase();
34
+ } catch {
35
+ /* noop */
36
+ }
31
37
  for (const dir of tmpDirs) {
32
38
  try { rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
33
39
  }
@@ -71,6 +77,37 @@ test("detect returns drift when file content differs from marker", async () => {
71
77
  assert.deepEqual(drift[0].entities, ["m1"]);
72
78
  });
73
79
 
80
+ test("detect refreshes stale marker when current file already matches DB artifact", async () => {
81
+ const base = makeTmpBase();
82
+ const rel = "milestones/M001/M001-ROADMAP.md";
83
+ const dbRel = "phases/01-alpha/01-ROADMAP.md";
84
+ const content = "# M001: Alpha\n\n## Slices\n\n- [ ] **S01: Work** `risk:low` `depends:[]`\n";
85
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
86
+ writeFileSync(join(base, ".gsd", rel), content, "utf-8");
87
+ openDatabase(join(base, ".gsd", "gsd.db"));
88
+ insertArtifact({
89
+ path: dbRel,
90
+ artifact_type: "ROADMAP",
91
+ milestone_id: "M001",
92
+ slice_id: null,
93
+ task_id: null,
94
+ full_content: content,
95
+ });
96
+ writeCompatMarker(base, {
97
+ schema: 2,
98
+ lastWriter: "gsd-pi",
99
+ lastProjectedAt: "2026-06-21T00:00:00.000Z",
100
+ projections: { [rel]: { sha: "stale000000000000", entities: ["M001"] } },
101
+ planning: { active: false, layout: null, projections: {}, passthrough: {} },
102
+ piVersion: "1.9.0",
103
+ });
104
+
105
+ const drift = await externalMarkdownEditHandler.detect(stubState, ctx(base));
106
+ assert.equal(drift.length, 0);
107
+ const marker = readCompatMarker(base);
108
+ assert.equal(marker.projections[rel]?.sha, computeProjectionSha(content));
109
+ });
110
+
74
111
  test("detect treats missing marker as drift on every tracked projection file", async () => {
75
112
  const base = makeTmpBase();
76
113
  const rel = "m1/roadmap.md";
@@ -122,7 +122,7 @@ test("migrateToFlatPhase creates a backup", async () => {
122
122
  assert.ok(backups.length >= 1, "at least one migrate-* backup dir should exist");
123
123
  });
124
124
 
125
- test("migrateToFlatPhase backs up existing phases projection before clearing it", async () => {
125
+ test("migrateToFlatPhase removes disposable phases snapshot after successful migration", async () => {
126
126
  const base = makeTmp();
127
127
  const reviewPath = join(base, ".gsd", "phases", "01-foundation", "PLAN-REVIEW.md");
128
128
  mkdirSync(join(base, ".gsd", "phases", "01-foundation"), { recursive: true });
@@ -133,11 +133,10 @@ test("migrateToFlatPhase backs up existing phases projection before clearing it"
133
133
  const backupRoot = join(base, ".gsd-backups");
134
134
  const backups = readdirSync(backupRoot).filter(d => d.startsWith("migrate-"));
135
135
  assert.equal(backups.length, 1, "one migrate backup should exist");
136
- const backedUpReview = join(backupRoot, backups[0]!, "__phases", "01-foundation", "PLAN-REVIEW.md");
137
136
  assert.equal(
138
- readFileSync(backedUpReview, "utf-8"),
139
- "# Plan Review\n\nHand-authored review.",
140
- "pre-existing phases/ files should have a recovery copy",
137
+ existsSync(join(backupRoot, backups[0]!, "__phases")),
138
+ false,
139
+ "temporary phases/ recovery snapshot should be removed after success",
141
140
  );
142
141
  });
143
142
 
@@ -202,7 +201,7 @@ test("failed migration rolls back without leaking a .gsd-backups/migrate-* dir",
202
201
  assert.equal(leaked.length, 0, "rollback must delete the migrate-* backup it created");
203
202
  });
204
203
 
205
- test("resumed migration stores phases backup in retained migrate snapshot", async () => {
204
+ test("resumed migration removes disposable phases snapshot after success", async () => {
206
205
  const base = makeTmp();
207
206
  const milestonesPath = join(base, ".gsd", "milestones");
208
207
  const migratingPath = join(base, ".gsd", "milestones.migrating");
@@ -218,9 +217,9 @@ test("resumed migration stores phases backup in retained migrate snapshot", asyn
218
217
  const backups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
219
218
  assert.equal(backups.length, 1, "resumed migration should create one retained migrate backup");
220
219
  assert.equal(
221
- readFileSync(join(backupRoot, backups[0]!, "__phases", "01-foundation", "PLAN-REVIEW.md"), "utf-8"),
222
- "# Plan Review\n\nResume recovery copy.",
223
- "resume should retain the phases snapshot under .gsd-backups",
220
+ existsSync(join(backupRoot, backups[0]!, "__phases")),
221
+ false,
222
+ "temporary phases/ recovery snapshot should be removed after resumed success",
224
223
  );
225
224
  assert.equal(
226
225
  existsSync(join(migratingPath, "__phases")),
@@ -9,7 +9,7 @@ import { join } from "node:path";
9
9
  import assert from "node:assert/strict";
10
10
  import test from "node:test";
11
11
 
12
- import { normalizeRealPath, relMilestoneFile, relSliceFile, resolveMilestoneFile, targetMilestoneFile } from "../paths.ts";
12
+ import { normalizeRealPath, relMilestoneFile, relSliceFile, resolveMilestoneFile, resolveSliceFile, targetMilestoneFile } from "../paths.ts";
13
13
 
14
14
  function makeFlatPhaseFixture(): { basePath: string; cleanup: () => void } {
15
15
  const basePath = mkdtempSync(join(tmpdir(), "flat-phase-validation-"));
@@ -99,6 +99,25 @@ test("flat-phase: resolveMilestoneFile finds the on-disk file", (t) => {
99
99
  assert.equal(existsSync(abs!), true);
100
100
  });
101
101
 
102
+ test("flat-phase: file resolvers ignore directories with artifact-like names", (t) => {
103
+ const basePath = mkdtempSync(join(tmpdir(), "flat-phase-dir-artifacts-"));
104
+ t.after(() => rmSync(basePath, { recursive: true, force: true }));
105
+ const phaseDir = join(basePath, ".gsd", "phases", "03-match-capture-manual-entry");
106
+ mkdirSync(join(phaseDir, "03-VALIDATION.md"), { recursive: true });
107
+ mkdirSync(join(phaseDir, "03-01-SUMMARY.md"), { recursive: true });
108
+
109
+ assert.equal(
110
+ resolveMilestoneFile(basePath, "M003", "VALIDATION"),
111
+ null,
112
+ "milestone resolver must not return a directory as a validation file",
113
+ );
114
+ assert.equal(
115
+ resolveSliceFile(basePath, "M003", "S01", "SUMMARY"),
116
+ null,
117
+ "slice resolver must not return a directory as a summary file",
118
+ );
119
+ });
120
+
102
121
  test("flat-phase: the legacy hardcoded path does NOT resolve (regression for #876)", (t) => {
103
122
  const { basePath, cleanup } = makeFlatPhaseFixture();
104
123
  t.after(cleanup);
@@ -46,6 +46,9 @@ import {
46
46
  tryCreateMemoriesFts,
47
47
  _isLikelyWslDrvFsPathForTest,
48
48
  _shouldAttemptVacuumRecoveryForTest,
49
+ insertArtifact,
50
+ getArtifact,
51
+ upsertMilestonePlanning,
49
52
  } from '../gsd-db.ts';
50
53
  import { MigrationBackupError } from '../db-migration-backup.ts';
51
54
  import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
@@ -357,6 +360,35 @@ describe('gsd-db', () => {
357
360
  closeDatabase();
358
361
  });
359
362
 
363
+ test("gsd-db: upsertMilestonePlanning rewrites placeholder phase artifact paths to final title slug", () => {
364
+ openDatabase(":memory:");
365
+
366
+ const stalePath = "phases/01-new-milestone-m001/01-ROADMAP.md";
367
+ const finalPath = "phases/01-lokably-brand-foundation-and-welcome-pag/01-ROADMAP.md";
368
+ insertMilestone({ id: "M001", title: "New milestone M001", status: "active" });
369
+ insertArtifact({
370
+ path: stalePath,
371
+ artifact_type: "ROADMAP",
372
+ milestone_id: "M001",
373
+ slice_id: null,
374
+ task_id: null,
375
+ full_content: "# Placeholder roadmap\n",
376
+ });
377
+
378
+ upsertMilestonePlanning("M001", {
379
+ title: "Lokably brand foundation and welcome page rebuild",
380
+ });
381
+
382
+ assert.equal(getArtifact(stalePath), null, "placeholder-derived artifact row should be removed");
383
+ assert.equal(
384
+ getArtifact(finalPath)?.full_content,
385
+ "# Placeholder roadmap\n",
386
+ "artifact row should move to the final title-derived phase path",
387
+ );
388
+
389
+ closeDatabase();
390
+ });
391
+
360
392
  test('gsd-db: active_requirements view excludes superseded', () => {
361
393
  openDatabase(':memory:');
362
394
 
@@ -87,6 +87,28 @@ test("guided milestone prompt builder preloads milestone planning context", asyn
87
87
  }
88
88
  });
89
89
 
90
+ test("guided milestone prompt builder prepends configured response language", async () => {
91
+ const base = mkdtempSync(join(tmpdir(), "gsd-guided-milestone-language-"));
92
+ const gsdHome = mkdtempSync(join(tmpdir(), "gsd-guided-milestone-language-home-"));
93
+ const previousGsdHome = process.env.GSD_HOME;
94
+ process.env.GSD_HOME = gsdHome;
95
+
96
+ try {
97
+ writeFileSync(join(gsdHome, "PREFERENCES.md"), "---\nversion: 1\nlanguage: 中文\n---\n", "utf-8");
98
+
99
+ const prompt = await buildDiscussMilestonePrompt("M001", "Language Preference", base, "true", {
100
+ includeContextMode: false,
101
+ });
102
+
103
+ assert.match(prompt, /^## Response Language\n\nAlways respond in 中文/);
104
+ } finally {
105
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
106
+ else process.env.GSD_HOME = previousGsdHome;
107
+ rmSync(base, { recursive: true, force: true });
108
+ rmSync(gsdHome, { recursive: true, force: true });
109
+ }
110
+ });
111
+
90
112
  test("guided milestone prompt builder caps prior draft seed before interpolation", async () => {
91
113
  const base = mkdtempSync(join(tmpdir(), "gsd-guided-milestone-draft-cap-"));
92
114
  const previousGsdHome = process.env.GSD_HOME;
@@ -1428,6 +1428,19 @@ process.exit(result.status ?? 0);
1428
1428
  rmSync(repo, { recursive: true, force: true });
1429
1429
  });
1430
1430
 
1431
+ test('Integration branch: write does not create legacy milestones scaffold', () => {
1432
+ const repo = initBranchTestRepo();
1433
+ mkdirSync(join(repo, ".gsd", "phases"), { recursive: true });
1434
+
1435
+ writeIntegrationBranch(repo, "M001", "f-123-new-thing");
1436
+
1437
+ assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), "f-123-new-thing");
1438
+ assert.ok(existsSync(join(repo, ".gsd", "M001-META.json")), "metadata is written at the flat .gsd root");
1439
+ assert.ok(!existsSync(join(repo, ".gsd", "milestones")), "legacy milestones/ root is not created");
1440
+
1441
+ rmSync(repo, { recursive: true, force: true });
1442
+ });
1443
+
1431
1444
  // ─── writeIntegrationBranch: updates when branch changes (#300) ──────
1432
1445
 
1433
1446
  test('Integration branch: updates on branch change', () => {
@@ -775,6 +775,27 @@ describe("prompt-budget: execute-task inline cap (039)", () => {
775
775
  });
776
776
 
777
777
  describe("prompt-budget: discuss-slice inline cap (039)", () => {
778
+ it("prepends the configured response language", async () => {
779
+ const base = createFixtureBase();
780
+ const gsdHome = mkdtempSync(join(tmpdir(), "gsd-discuss-slice-language-home-"));
781
+ const previousGsdHome = process.env.GSD_HOME;
782
+ process.env.GSD_HOME = gsdHome;
783
+
784
+ try {
785
+ writeFileSync(join(gsdHome, "PREFERENCES.md"), "---\nversion: 1\nlanguage: 中文\n---\n", "utf-8");
786
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true });
787
+
788
+ const prompt = await buildDiscussSlicePrompt("M001", "S01", "Current", base);
789
+
790
+ assert.match(prompt, /^## Response Language\n\nAlways respond in 中文/);
791
+ } finally {
792
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
793
+ else process.env.GSD_HOME = previousGsdHome;
794
+ cleanup(base);
795
+ cleanup(gsdHome);
796
+ }
797
+ });
798
+
778
799
  it("caps the inlined block — roadmap survives first, the trailing decisions register truncates", async () => {
779
800
  const base = createFixtureBase();
780
801
  try {
@@ -23,7 +23,10 @@ import {
23
23
  } from "../bootstrap/agent-end-recovery.ts";
24
24
  import { blockModelUntil, clearTemporaryModelBlocksForTest } from "../blocked-models.ts";
25
25
  import { _buildCancelledUnitStopReason } from "../auto/phase-helpers.ts";
26
- import { _classifyZeroToolProviderMessageForTest } from "../auto/unit-phase.ts";
26
+ import {
27
+ _classifyZeroToolProviderMessageForTest,
28
+ _zeroToolPseudoToolCallSnippetForTest,
29
+ } from "../auto/unit-phase.ts";
27
30
  import { autoSession } from "../auto-runtime-state.ts";
28
31
  import { getNextFallbackModel } from "../preferences.ts";
29
32
  import { clearGuidedUnitContext, getGuidedUnitContext, setGuidedUnitContext } from "../guided-unit-context.ts";
@@ -77,6 +80,21 @@ test("zero-tool provider classifier treats weekly limit wording as transient rat
77
80
  assert.equal(result.kind, "rate-limit");
78
81
  });
79
82
 
83
+ test("zero-tool pseudo tool-call detector identifies serialization drift", () => {
84
+ const snippet = _zeroToolPseudoToolCallSnippetForTest(
85
+ 'bash<arg_key>command</arg_key><arg_value>ls -la /tmp && echo "---SRC---"</arg_value></tool_call>',
86
+ );
87
+ assert.ok(snippet, "pseudo tool-call text should be recognized");
88
+ assert.match(snippet, /bash<arg_key>command<\/arg_key>/);
89
+ });
90
+
91
+ test("zero-tool provider classifier does not treat pseudo tool-call text as an error class", () => {
92
+ const result = _classifyZeroToolProviderMessageForTest(
93
+ 'bash<arg_key>command</arg_key><arg_value>ls -la /tmp && echo "---SRC---"</arg_value></tool_call>',
94
+ );
95
+ assert.equal(result, null, "pseudo tool-call text is drift, not a provider error");
96
+ });
97
+
80
98
  test("classifyError treats extra-usage phrasing as transient rate-limit (#4397)", () => {
81
99
  const result = classifyError("You are out of extra usage. Please wait before retrying.");
82
100
  assert.ok(isTransient(result));
@@ -651,6 +669,108 @@ test("rate-limit agent_end walks past unavailable fallback models before pausing
651
669
  }
652
670
  });
653
671
 
672
+ test("retry-exhausted abort/unknown provider stop reasons trigger model fallback (#1364)", async () => {
673
+ const originalCwd = process.cwd();
674
+ const originalSetTimeout = globalThis.setTimeout;
675
+
676
+ globalThis.setTimeout = ((fn: (...args: unknown[]) => void, delay?: number) => {
677
+ if ((delay ?? 0) === 0) {
678
+ fn();
679
+ }
680
+ return 0 as unknown as ReturnType<typeof setTimeout>;
681
+ }) as typeof setTimeout;
682
+
683
+ try {
684
+ for (const reason of ["abort", "unknown"]) {
685
+ const base = mkdtempSync(join(tmpdir(), `gsd-stop-reason-${reason}-`));
686
+ const notifications: Array<{ message: string; level?: string }> = [];
687
+ const setModelCalls: string[] = [];
688
+ const sendMessageCalls: unknown[][] = [];
689
+
690
+ try {
691
+ resetTransientRetryState();
692
+ clearTemporaryModelBlocksForTest();
693
+ autoSession.reset();
694
+ mkdirSync(join(base, ".gsd"), { recursive: true });
695
+ writeFileSync(
696
+ join(base, ".gsd", "PREFERENCES.md"),
697
+ [
698
+ "---",
699
+ "models:",
700
+ " execution:",
701
+ " model: gpt-5.5",
702
+ " provider: openai-codex",
703
+ " fallbacks:",
704
+ " - anthropic/claude-sonnet-4-6",
705
+ "---",
706
+ ].join("\n"),
707
+ "utf-8",
708
+ );
709
+ process.chdir(base);
710
+
711
+ autoSession.active = true;
712
+ autoSession.basePath = base;
713
+ autoSession.currentUnit = { type: "execute-task", id: "M001/S01/T01", startedAt: Date.now() };
714
+ autoSession.autoModeStartModel = { provider: "openai-codex", id: "gpt-5.5" };
715
+
716
+ const availableModels = [
717
+ { id: "gpt-5.5", provider: "openai-codex", api: "responses" },
718
+ { id: "claude-sonnet-4-6", provider: "anthropic", api: "anthropic-messages" },
719
+ ];
720
+ const ctx = {
721
+ model: availableModels[0],
722
+ modelRegistry: { getAvailable: () => availableModels },
723
+ ui: {
724
+ notify(message: string, level?: "info" | "warning" | "error" | "success") {
725
+ notifications.push({ message, level });
726
+ },
727
+ setStatus: () => {},
728
+ setWidget: () => {},
729
+ setWorkingMessage: () => {},
730
+ },
731
+ } as any;
732
+ const pi = {
733
+ setModel: async (model: { provider: string; id: string }) => {
734
+ setModelCalls.push(`${model.provider}/${model.id}`);
735
+ return true;
736
+ },
737
+ sendMessage: (...args: unknown[]) => {
738
+ sendMessageCalls.push(args);
739
+ },
740
+ } as any;
741
+
742
+ await handleAgentEnd(pi, {
743
+ messages: [{
744
+ role: "assistant",
745
+ stopReason: "error",
746
+ errorMessage: `Retry failed after 3 attempts: Unhandled stop reason: ${reason}`,
747
+ }],
748
+ } as any, ctx);
749
+
750
+ assert.deepEqual(setModelCalls, ["anthropic/claude-sonnet-4-6"], `${reason} should switch to configured fallback`);
751
+ assert.equal(sendMessageCalls.length, 1, `${reason} fallback recovery must trigger a continuation turn`);
752
+ assert.deepEqual(sendMessageCalls[0][1], { triggerTurn: true });
753
+ assert.ok(
754
+ notifications.some((n) => n.message.includes("anthropic/claude-sonnet-4-6")),
755
+ `${reason} notification should name the selected fallback`,
756
+ );
757
+ } finally {
758
+ process.chdir(originalCwd);
759
+ clearTemporaryModelBlocksForTest();
760
+ resetTransientRetryState();
761
+ autoSession.reset();
762
+ rmSync(base, { recursive: true, force: true });
763
+ }
764
+ }
765
+ } finally {
766
+ globalThis.setTimeout = originalSetTimeout;
767
+ process.chdir(originalCwd);
768
+ clearTemporaryModelBlocksForTest();
769
+ resetTransientRetryState();
770
+ autoSession.reset();
771
+ }
772
+ });
773
+
654
774
  test("isTerminalDeletedWorktreeProviderError matches removed auto-worktree paths only", () => {
655
775
  assert.equal(
656
776
  isTerminalDeletedWorktreeProviderError('Path "/Users/dev/.gsd/projects/abc123/worktrees/M005" does not exist'),