gsd-pi 2.75.0-dev.b6ad8c5f7 → 2.75.0-dev.e41b70b10

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 (181) hide show
  1. package/dist/resources/extensions/gsd/auto/phases.js +2 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.js +22 -1
  3. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +8 -2
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -11
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +3 -1
  6. package/dist/resources/extensions/gsd/auto-prompts.js +19 -9
  7. package/dist/resources/extensions/gsd/auto-worktree.js +16 -1
  8. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
  9. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  10. package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
  11. package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -92
  12. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  13. package/dist/resources/extensions/gsd/commands-memory.js +462 -0
  14. package/dist/resources/extensions/gsd/doctor-git-checks.js +22 -2
  15. package/dist/resources/extensions/gsd/gsd-db.js +237 -4
  16. package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
  17. package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
  18. package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
  19. package/dist/resources/extensions/gsd/memory-relations.js +189 -0
  20. package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
  21. package/dist/resources/extensions/gsd/memory-store.js +299 -6
  22. package/dist/resources/extensions/gsd/pre-execution-checks.js +12 -8
  23. package/dist/resources/extensions/gsd/prompts/add-tests.md +1 -0
  24. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  25. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -0
  26. package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
  27. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +14 -0
  28. package/dist/resources/extensions/search-the-web/command-search-provider.js +4 -1
  29. package/dist/resources/extensions/search-the-web/native-search.js +13 -2
  30. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  31. package/dist/web/standalone/.next/BUILD_ID +1 -1
  32. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  33. package/dist/web/standalone/.next/build-manifest.json +2 -2
  34. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  35. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.html +1 -1
  52. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  59. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  61. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  62. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  63. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  66. package/packages/mcp-server/dist/server.js +12 -10
  67. package/packages/mcp-server/dist/server.js.map +1 -1
  68. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  69. package/packages/mcp-server/dist/session-manager.js +8 -1
  70. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  71. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
  72. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  73. package/packages/mcp-server/dist/workflow-tools.js +207 -71
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/src/mcp-server.test.ts +40 -4
  76. package/packages/mcp-server/src/server.ts +12 -10
  77. package/packages/mcp-server/src/session-manager.ts +10 -3
  78. package/packages/mcp-server/src/workflow-tools.test.ts +346 -1
  79. package/packages/mcp-server/src/workflow-tools.ts +228 -75
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  81. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  82. package/packages/pi-ai/dist/index.d.ts +1 -0
  83. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  84. package/packages/pi-ai/dist/index.js +1 -0
  85. package/packages/pi-ai/dist/index.js.map +1 -1
  86. package/packages/pi-ai/dist/providers/api-family.d.ts +27 -0
  87. package/packages/pi-ai/dist/providers/api-family.d.ts.map +1 -0
  88. package/packages/pi-ai/dist/providers/api-family.js +47 -0
  89. package/packages/pi-ai/dist/providers/api-family.js.map +1 -0
  90. package/packages/pi-ai/dist/providers/api-family.test.d.ts +2 -0
  91. package/packages/pi-ai/dist/providers/api-family.test.d.ts.map +1 -0
  92. package/packages/pi-ai/dist/providers/api-family.test.js +101 -0
  93. package/packages/pi-ai/dist/providers/api-family.test.js.map +1 -0
  94. package/packages/pi-ai/src/index.ts +1 -0
  95. package/packages/pi-ai/src/providers/api-family.test.ts +129 -0
  96. package/packages/pi-ai/src/providers/api-family.ts +57 -0
  97. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  98. package/packages/pi-coding-agent/dist/cli/args.d.ts +6 -0
  99. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/cli/args.js +14 -4
  101. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/cli/args.test.d.ts +2 -0
  103. package/packages/pi-coding-agent/dist/cli/args.test.d.ts.map +1 -0
  104. package/packages/pi-coding-agent/dist/cli/args.test.js +38 -0
  105. package/packages/pi-coding-agent/dist/cli/args.test.js.map +1 -0
  106. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  107. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
  110. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/retry-handler.js +4 -1
  114. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -0
  116. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/sdk.js +7 -1
  118. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/main.js +3 -0
  121. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  122. package/packages/pi-coding-agent/src/cli/args.test.ts +44 -0
  123. package/packages/pi-coding-agent/src/cli/args.ts +21 -6
  124. package/packages/pi-coding-agent/src/core/extensions/runner.ts +4 -1
  125. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -2
  126. package/packages/pi-coding-agent/src/core/retry-handler.ts +4 -1
  127. package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
  128. package/packages/pi-coding-agent/src/main.ts +4 -0
  129. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  130. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -10
  131. package/src/resources/extensions/gsd/auto/phases.ts +3 -0
  132. package/src/resources/extensions/gsd/auto-dashboard.ts +25 -1
  133. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +15 -2
  134. package/src/resources/extensions/gsd/auto-dispatch.ts +21 -7
  135. package/src/resources/extensions/gsd/auto-model-selection.ts +3 -1
  136. package/src/resources/extensions/gsd/auto-prompts.ts +33 -9
  137. package/src/resources/extensions/gsd/auto-worktree.ts +16 -1
  138. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
  139. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  140. package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
  141. package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -131
  142. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  143. package/src/resources/extensions/gsd/commands-memory.ts +551 -0
  144. package/src/resources/extensions/gsd/doctor-git-checks.ts +23 -2
  145. package/src/resources/extensions/gsd/gsd-db.ts +273 -4
  146. package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
  147. package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
  148. package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
  149. package/src/resources/extensions/gsd/memory-relations.ts +240 -0
  150. package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
  151. package/src/resources/extensions/gsd/memory-store.ts +351 -7
  152. package/src/resources/extensions/gsd/pre-execution-checks.ts +12 -8
  153. package/src/resources/extensions/gsd/prompts/add-tests.md +1 -0
  154. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  155. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -0
  156. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +49 -0
  157. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  158. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  159. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
  160. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  161. package/src/resources/extensions/gsd/tests/integration/doctor-git-symlink-cwd.test.ts +79 -0
  162. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  163. package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
  164. package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
  165. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
  166. package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
  167. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  168. package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
  169. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +66 -0
  170. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +132 -8
  171. package/src/resources/extensions/gsd/tests/prompts-no-gitignored-test-refs.test.ts +56 -0
  172. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +54 -0
  173. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +97 -0
  174. package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
  175. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +14 -0
  176. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  177. package/src/resources/extensions/search-the-web/command-search-provider.ts +4 -1
  178. package/src/resources/extensions/search-the-web/native-search.ts +13 -3
  179. package/src/resources/extensions/gsd/tests/onboarding-handler-loader.test.ts +0 -41
  180. /package/dist/web/standalone/.next/static/{J2z3GMC9QtSLr7gyoM38c → By_yegSJ-AA1OP0QjYbSl}/_buildManifest.js +0 -0
  181. /package/dist/web/standalone/.next/static/{J2z3GMC9QtSLr7gyoM38c → By_yegSJ-AA1OP0QjYbSl}/_ssgManifest.js +0 -0
@@ -1,13 +1,8 @@
1
1
  /**
2
2
  * Prompt budget enforcement tests — verifies that budget-aware prompt builders
3
- * truncate content at section boundaries and that plan-slice includes executor
4
- * context constraints.
5
- *
6
- * Tests:
7
- * 1. inlineDependencySummaries() truncates when budget is small, passes through when large
8
- * 2. plan-slice.md template includes {{executorContextConstraints}} placeholder
9
- * 3. Executor constraints formatting varies with context window size
10
- * 4. Different context windows produce different budget-constrained outputs
3
+ * truncate content at section boundaries, that plan-slice includes executor
4
+ * context constraints, and that prompt builders thread the real executor
5
+ * context window through to the budget engine (issue #4142).
11
6
  */
12
7
 
13
8
  import { describe, it, beforeEach, afterEach } from "node:test";
@@ -488,4 +483,133 @@ describe("prompt-budget: execute-task builder truncation pattern", () => {
488
483
  assert.ok(result.droppedSections > 0, "should report dropped sections");
489
484
  }
490
485
  });
486
+ });
487
+
488
+ // ─── Regression: prompt builders must thread modelRegistry + sessionContextWindow (issue #4142) ───
489
+ //
490
+ // `resolveExecutorContextWindow()` resolves the executor context window in 3
491
+ // steps: (1) look up the configured executor model in `modelRegistry`, (2) fall
492
+ // back to `sessionContextWindow`, (3) fall back to `DEFAULT_CONTEXT_WINDOW`
493
+ // (200K). Before this fix, prompt-builder call sites passed `undefined` for
494
+ // both knobs and always landed on Step 3 — even on 1M-token models. These
495
+ // source-level assertions pin the wiring so future refactors cannot regress it.
496
+
497
+ describe("prompt-budget: modelRegistry + sessionContextWindow wiring", () => {
498
+ const autoPromptsSrc = readFileSync(join(__dirname, "..", "auto-prompts.ts"), "utf-8");
499
+ const autoDispatchSrc = readFileSync(join(__dirname, "..", "auto-dispatch.ts"), "utf-8");
500
+ const autoDirectDispatchSrc = readFileSync(join(__dirname, "..", "auto-direct-dispatch.ts"), "utf-8");
501
+ const phasesSrc = readFileSync(join(__dirname, "..", "auto", "phases.ts"), "utf-8");
502
+
503
+ it("formatExecutorConstraints accepts and forwards both knobs", () => {
504
+ assert.match(
505
+ autoPromptsSrc,
506
+ /function formatExecutorConstraints\([^)]*sessionContextWindow[^)]*modelRegistry[^)]*\)/s,
507
+ "formatExecutorConstraints must accept sessionContextWindow and modelRegistry",
508
+ );
509
+ assert.match(
510
+ autoPromptsSrc,
511
+ /resolveExecutorContextWindow\(\s*modelRegistry\s*,\s*prefs\?\.preferences\s*,\s*sessionContextWindow/,
512
+ "formatExecutorConstraints must forward both to resolveExecutorContextWindow",
513
+ );
514
+ });
515
+
516
+ it("renderSlicePrompt options declare both knobs and forward to formatExecutorConstraints", () => {
517
+ assert.match(
518
+ autoPromptsSrc,
519
+ /async function renderSlicePrompt\(options:\s*\{[^}]*sessionContextWindow\?[^}]*modelRegistry\?/s,
520
+ "renderSlicePrompt options must declare both fields",
521
+ );
522
+ assert.match(
523
+ autoPromptsSrc,
524
+ /formatExecutorConstraints\(sessionContextWindow,\s*modelRegistry\)/,
525
+ "renderSlicePrompt must forward both to formatExecutorConstraints",
526
+ );
527
+ });
528
+
529
+ it("buildPlanSlicePrompt options declare both knobs and thread them into renderSlicePrompt", () => {
530
+ assert.match(
531
+ autoPromptsSrc,
532
+ /export async function buildPlanSlicePrompt\([\s\S]*?options\?:\s*\{[^}]*sessionContextWindow\?[^}]*modelRegistry\?/,
533
+ "buildPlanSlicePrompt options must declare both fields",
534
+ );
535
+ assert.match(
536
+ autoPromptsSrc,
537
+ /sessionContextWindow:\s*options\?\.sessionContextWindow,\s*modelRegistry:\s*options\?\.modelRegistry/,
538
+ "buildPlanSlicePrompt must forward both into renderSlicePrompt",
539
+ );
540
+ });
541
+
542
+ it("ExecuteTaskPromptOptions declares both knobs", () => {
543
+ assert.match(
544
+ autoPromptsSrc,
545
+ /interface ExecuteTaskPromptOptions\s*\{[^}]*sessionContextWindow\?[^}]*modelRegistry\?/s,
546
+ "ExecuteTaskPromptOptions must declare both fields",
547
+ );
548
+ });
549
+
550
+ it("buildExecuteTaskPrompt forwards opts.modelRegistry + opts.sessionContextWindow to resolveExecutorContextWindow", () => {
551
+ assert.match(
552
+ autoPromptsSrc,
553
+ /resolveExecutorContextWindow\(\s*opts\.modelRegistry\s*,\s*prefs\?\.preferences\s*,\s*opts\.sessionContextWindow/,
554
+ "buildExecuteTaskPrompt must forward both into resolveExecutorContextWindow",
555
+ );
556
+ });
557
+
558
+ it("buildReactiveExecutePrompt accepts opts and forwards to embedded buildExecuteTaskPrompt", () => {
559
+ assert.match(
560
+ autoPromptsSrc,
561
+ /export async function buildReactiveExecutePrompt\([\s\S]*?opts\?:\s*\{[^}]*sessionContextWindow\?[^}]*modelRegistry\?/,
562
+ "buildReactiveExecutePrompt must accept sessionContextWindow + modelRegistry",
563
+ );
564
+ assert.match(
565
+ autoPromptsSrc,
566
+ /sessionContextWindow:\s*opts\?\.sessionContextWindow,\s*modelRegistry:\s*opts\?\.modelRegistry/,
567
+ "buildReactiveExecutePrompt must forward both into the embedded buildExecuteTaskPrompt call",
568
+ );
569
+ });
570
+
571
+ it("DispatchContext declares both knobs", () => {
572
+ assert.match(
573
+ autoDispatchSrc,
574
+ /interface DispatchContext\s*\{[^}]*sessionContextWindow\?[^}]*modelRegistry\?/s,
575
+ "DispatchContext must declare both fields so dispatch rules can thread them",
576
+ );
577
+ });
578
+
579
+ it("DISPATCH_RULES destructure and forward both knobs at every prompt-builder call site", () => {
580
+ // Every plan-slice / execute-task / reactive-execute rule must destructure
581
+ // both names from its match context so new call sites can't silently drop them.
582
+ const matchLines = autoDispatchSrc.match(/match:\s*async\s*\(\{[^}]*sessionContextWindow[^}]*modelRegistry[^}]*\}/g) ?? [];
583
+ assert.ok(
584
+ matchLines.length >= 5,
585
+ `expected ≥5 dispatch rules destructuring both knobs, got ${matchLines.length}`,
586
+ );
587
+
588
+ const forwardCount = (autoDispatchSrc.match(/sessionContextWindow,\s*modelRegistry/g) ?? []).length;
589
+ assert.ok(
590
+ forwardCount >= 5,
591
+ `expected ≥5 forward sites of { sessionContextWindow, modelRegistry }, got ${forwardCount}`,
592
+ );
593
+ });
594
+
595
+ it("runDispatch populates both knobs from ctx.model / ctx.modelRegistry", () => {
596
+ assert.match(
597
+ phasesSrc,
598
+ /sessionContextWindow:\s*ctx\.model\?\.contextWindow/,
599
+ "runDispatch must populate sessionContextWindow from ctx.model?.contextWindow",
600
+ );
601
+ assert.match(
602
+ phasesSrc,
603
+ /modelRegistry:\s*ctx\.modelRegistry/,
604
+ "runDispatch must populate modelRegistry from ctx.modelRegistry",
605
+ );
606
+ });
607
+
608
+ it("dispatchDirectPhase forwards both knobs to buildPlanSlicePrompt and buildExecuteTaskPrompt", () => {
609
+ const passes = (autoDirectDispatchSrc.match(/sessionContextWindow:\s*ctx\.model\?\.contextWindow[\s\S]*?modelRegistry:\s*ctx\.modelRegistry/g) ?? []).length;
610
+ assert.ok(
611
+ passes >= 2,
612
+ `dispatchDirectPhase must forward the pair at both prompt-builder call sites (≥2), got ${passes}`,
613
+ );
614
+ });
491
615
  });
@@ -0,0 +1,56 @@
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
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const promptsDir = join(__dirname, "..", "prompts");
9
+
10
+ function readPrompt(name: string): string {
11
+ return readFileSync(join(promptsDir, name), "utf-8");
12
+ }
13
+
14
+ test("add-tests prompt forbids referencing gitignored paths", () => {
15
+ const prompt = readPrompt("add-tests.md");
16
+
17
+ assert.match(
18
+ prompt,
19
+ /gitignore/i,
20
+ "add-tests prompt should mention .gitignore to rule out referencing local-only files",
21
+ );
22
+ assert.match(prompt, /\.gsd\//, "add-tests prompt should name .gsd/ as off-limits for tests");
23
+ assert.match(prompt, /\.planning\//, "add-tests prompt should name .planning/ as off-limits for tests");
24
+ assert.match(prompt, /\.audits\//, "add-tests prompt should name .audits/ as off-limits for tests");
25
+ assert.match(
26
+ prompt,
27
+ /tracked/i,
28
+ "add-tests prompt should frame the rule in terms of tracked files",
29
+ );
30
+ });
31
+
32
+ test("plan-slice prompt warns against planning tests that depend on gitignored files", () => {
33
+ const prompt = readPrompt("plan-slice.md");
34
+
35
+ assert.match(
36
+ prompt,
37
+ /gitignore/i,
38
+ "plan-slice prompt should warn against planning tests that depend on .gitignore paths",
39
+ );
40
+ assert.match(prompt, /\.gsd\//, "plan-slice prompt should name .gsd/ as off-limits for planned tests");
41
+ assert.match(prompt, /\.planning\//, "plan-slice prompt should name .planning/ as off-limits for planned tests");
42
+ assert.match(prompt, /\.audits\//, "plan-slice prompt should name .audits/ as off-limits for planned tests");
43
+ });
44
+
45
+ test("execute-task prompt forbids tests that reference gitignored paths", () => {
46
+ const prompt = readPrompt("execute-task.md");
47
+
48
+ assert.match(
49
+ prompt,
50
+ /gitignore/i,
51
+ "execute-task prompt should forbid referencing gitignored paths from tests",
52
+ );
53
+ assert.match(prompt, /\.gsd\//, "execute-task prompt should name .gsd/ as off-limits for tests");
54
+ assert.match(prompt, /\.planning\//, "execute-task prompt should name .planning/ as off-limits for tests");
55
+ assert.match(prompt, /\.audits\//, "execute-task prompt should name .audits/ as off-limits for tests");
56
+ });
@@ -21,6 +21,8 @@ import {
21
21
  existsSync,
22
22
  readFileSync,
23
23
  realpathSync,
24
+ symlinkSync,
25
+ lstatSync,
24
26
  } from "node:fs";
25
27
  import { join } from "node:path";
26
28
  import { tmpdir } from "node:os";
@@ -76,6 +78,20 @@ function createTempRepo(): string {
76
78
  return dir;
77
79
  }
78
80
 
81
+ function createTempRepoWithSymlinkedGsd(): { repo: string; stateDir: string } {
82
+ const repo = realpathSync(mkdtempSync(join(tmpdir(), "wt-symlink-stash-test-")));
83
+ const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "wt-symlink-state-")));
84
+ run("git init", repo);
85
+ run("git config user.email test@test.com", repo);
86
+ run("git config user.name Test", repo);
87
+ writeFileSync(join(repo, "README.md"), "# test\n");
88
+ symlinkSync(stateDir, join(repo, ".gsd"));
89
+ run("git add README.md", repo);
90
+ run("git commit -m init", repo);
91
+ run("git branch -M main", repo);
92
+ return { repo, stateDir };
93
+ }
94
+
79
95
  function makeRoadmap(
80
96
  milestoneId: string,
81
97
  title: string,
@@ -259,6 +275,44 @@ test("#2505: mergeMilestoneToMain preserves queued CONTEXT files (not swept into
259
275
  }
260
276
  });
261
277
 
278
+ test("#2505: pre-merge stash handles symlinked .gsd without traversing it", () => {
279
+ const { repo, stateDir } = createTempRepoWithSymlinkedGsd();
280
+ try {
281
+ const wtPath = createAutoWorktree(repo, "M016");
282
+ const normalizedPath = wtPath.replaceAll("\\", "/");
283
+ const worktreeName = normalizedPath.split("/").pop() || "M016";
284
+ const sliceBranch = `slice/${worktreeName}/S01`;
285
+ run(`git checkout -b "${sliceBranch}"`, wtPath);
286
+ writeFileSync(join(wtPath, "app.ts"), "export const app = true;\n");
287
+ run("git add app.ts", wtPath);
288
+ run('git commit -m "add app feature"', wtPath);
289
+ run("git checkout milestone/M016", wtPath);
290
+ run(`git merge --no-ff "${sliceBranch}" -m "merge S01"`, wtPath);
291
+
292
+ const queuedDir = join(stateDir, "milestones", "M017");
293
+ mkdirSync(queuedDir, { recursive: true });
294
+ writeFileSync(join(queuedDir, "M017-CONTEXT.md"), "# M017: Queued\n");
295
+
296
+ // Trigger the pre-merge stash with both tracked and untracked project files.
297
+ writeFileSync(join(repo, "README.md"), "# test\n\nDirty change.\n");
298
+ writeFileSync(join(repo, "local-note.txt"), "local scratch\n");
299
+
300
+ const result = mergeMilestoneToMain(repo, "M016", makeRoadmap("M016", "App Feature", [
301
+ { id: "S01", title: "Feature" },
302
+ ]));
303
+
304
+ assert.ok(result.commitMessage.includes("GSD-Milestone: M016"), "merge should succeed");
305
+ assert.ok(existsSync(join(repo, "app.ts")), "milestone code merged to main");
306
+ assert.equal(lstatSync(join(repo, ".gsd")).isSymbolicLink(), true, ".gsd symlink remains in place");
307
+ assert.ok(existsSync(join(queuedDir, "M017-CONTEXT.md")), "queued context remains in external state");
308
+ assert.equal(readFileSync(join(repo, "README.md"), "utf-8").replace(/\r\n/g, "\n"), "# test\n\nDirty change.\n");
309
+ assert.equal(readFileSync(join(repo, "local-note.txt"), "utf-8"), "local scratch\n");
310
+ } finally {
311
+ rmSync(repo, { recursive: true, force: true });
312
+ rmSync(stateDir, { recursive: true, force: true });
313
+ }
314
+ });
315
+
262
316
  test("#2505: back-to-back merges preserve queued CONTEXT files", () => {
263
317
  const repo = createTempRepo();
264
318
  try {
@@ -11,6 +11,7 @@ import {
11
11
  _getAdapter,
12
12
  insertGateRow,
13
13
  } from "../gsd-db.ts";
14
+ import { markDepthVerified, clearDiscussionFlowState } from "../bootstrap/write-gate.ts";
14
15
  import {
15
16
  executeCompleteMilestone,
16
17
  executePlanMilestone,
@@ -645,3 +646,99 @@ test("executeReplanSlice rewrites pending tasks and renders replan artifacts", a
645
646
  cleanup(base);
646
647
  }
647
648
  });
649
+
650
+ test("executeSummarySave removes sibling CONTEXT-DRAFT when writing milestone CONTEXT (#4442)", async () => {
651
+ const base = makeTmpBase();
652
+ try {
653
+ openTestDb(base);
654
+ markDepthVerified("M001", base);
655
+
656
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
657
+ mkdirSync(milestoneDir, { recursive: true });
658
+ const draftPath = join(milestoneDir, "M001-CONTEXT-DRAFT.md");
659
+ writeFileSync(draftPath, "# Draft\n\nincremental notes");
660
+ assert.ok(existsSync(draftPath), "precondition: draft exists");
661
+
662
+ const result = await inProjectDir(base, () => executeSummarySave({
663
+ milestone_id: "M001",
664
+ artifact_type: "CONTEXT",
665
+ content: "# Context\n\nfinal discussion output",
666
+ }, base));
667
+
668
+ assert.equal(result.details.operation, "save_summary");
669
+ assert.equal(result.details.artifact_type, "CONTEXT");
670
+
671
+ const contextPath = join(milestoneDir, "M001-CONTEXT.md");
672
+ assert.ok(existsSync(contextPath), "CONTEXT.md should be written");
673
+ assert.equal(
674
+ existsSync(draftPath),
675
+ false,
676
+ "CONTEXT-DRAFT.md should be removed after final CONTEXT.md is written",
677
+ );
678
+ } finally {
679
+ clearDiscussionFlowState();
680
+ closeDatabase();
681
+ cleanup(base);
682
+ }
683
+ });
684
+
685
+ test("executeSummarySave removes sibling CONTEXT-DRAFT when writing slice CONTEXT (#4442)", async () => {
686
+ const base = makeTmpBase();
687
+ try {
688
+ openTestDb(base);
689
+
690
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
691
+ mkdirSync(sliceDir, { recursive: true });
692
+ const draftPath = join(sliceDir, "S01-CONTEXT-DRAFT.md");
693
+ writeFileSync(draftPath, "# Slice Draft\n\nincremental slice notes");
694
+ assert.ok(existsSync(draftPath), "precondition: slice draft exists");
695
+
696
+ const result = await inProjectDir(base, () => executeSummarySave({
697
+ milestone_id: "M001",
698
+ slice_id: "S01",
699
+ artifact_type: "CONTEXT",
700
+ content: "# Slice Context\n\nfinal slice output",
701
+ }, base));
702
+
703
+ assert.equal(result.details.operation, "save_summary");
704
+ assert.equal(result.details.artifact_type, "CONTEXT");
705
+
706
+ const contextPath = join(sliceDir, "S01-CONTEXT.md");
707
+ assert.ok(existsSync(contextPath), "slice CONTEXT.md should be written");
708
+ assert.equal(
709
+ existsSync(draftPath),
710
+ false,
711
+ "slice CONTEXT-DRAFT.md should be removed after final CONTEXT.md is written",
712
+ );
713
+ } finally {
714
+ closeDatabase();
715
+ cleanup(base);
716
+ }
717
+ });
718
+
719
+ test("executeSummarySave leaves sibling CONTEXT-DRAFT intact for non-CONTEXT artifacts (#4442)", async () => {
720
+ const base = makeTmpBase();
721
+ try {
722
+ openTestDb(base);
723
+
724
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
725
+ mkdirSync(milestoneDir, { recursive: true });
726
+ const draftPath = join(milestoneDir, "M001-CONTEXT-DRAFT.md");
727
+ writeFileSync(draftPath, "# Draft\n\nstill in progress");
728
+
729
+ const result = await inProjectDir(base, () => executeSummarySave({
730
+ milestone_id: "M001",
731
+ artifact_type: "RESEARCH",
732
+ content: "# Research\n\nresearch notes",
733
+ }, base));
734
+
735
+ assert.equal(result.details.artifact_type, "RESEARCH");
736
+ assert.ok(
737
+ existsSync(draftPath),
738
+ "CONTEXT-DRAFT.md must survive RESEARCH/SUMMARY/ASSESSMENT writes",
739
+ );
740
+ } finally {
741
+ closeDatabase();
742
+ cleanup(base);
743
+ }
744
+ });