gsd-pi 2.58.0-dev.778d6ac → 2.58.0-dev.e002a57

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 (212) hide show
  1. package/dist/cli.js +11 -0
  2. package/dist/resources/extensions/gsd/auto-worktree.js +11 -8
  3. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -16
  4. package/dist/resources/extensions/gsd/bootstrap/system-context.js +22 -1
  5. package/dist/resources/extensions/gsd/codebase-generator.js +279 -0
  6. package/dist/resources/extensions/gsd/commands/catalog.js +10 -1
  7. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  8. package/dist/resources/extensions/gsd/commands-codebase.js +115 -0
  9. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +41 -4
  10. package/dist/resources/extensions/gsd/complexity-classifier.js +8 -6
  11. package/dist/resources/extensions/gsd/doctor-git-checks.js +48 -1
  12. package/dist/resources/extensions/gsd/doctor-proactive.js +34 -1
  13. package/dist/resources/extensions/gsd/error-classifier.js +3 -4
  14. package/dist/resources/extensions/gsd/git-service.js +82 -1
  15. package/dist/resources/extensions/gsd/native-git-bridge.js +22 -0
  16. package/dist/resources/extensions/gsd/paths.js +2 -0
  17. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  18. package/dist/resources/extensions/gsd/watch/header-renderer.js +241 -0
  19. package/dist/resources/extensions/search-the-web/url-utils.js +17 -0
  20. package/dist/security-overrides.d.ts +11 -0
  21. package/dist/security-overrides.js +41 -0
  22. package/dist/web/standalone/.next/BUILD_ID +1 -1
  23. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  24. package/dist/web/standalone/.next/build-manifest.json +2 -2
  25. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  27. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.html +1 -1
  43. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  50. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  51. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  52. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  53. package/dist/welcome-screen.d.ts +1 -0
  54. package/dist/welcome-screen.js +32 -6
  55. package/package.json +1 -1
  56. package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts +8 -0
  57. package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/resolve-config-value.js +23 -2
  59. package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +89 -2
  61. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/settings-manager-security.test.d.ts +2 -0
  63. package/packages/pi-coding-agent/dist/core/settings-manager-security.test.d.ts.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/settings-manager-security.test.js +83 -0
  65. package/packages/pi-coding-agent/dist/core/settings-manager-security.test.js.map +1 -0
  66. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +14 -0
  67. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  68. package/packages/pi-coding-agent/dist/core/settings-manager.js +36 -3
  69. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  70. package/packages/pi-coding-agent/dist/index.d.ts +1 -0
  71. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/index.js +1 -0
  73. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/armin.d.ts +1 -1
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/armin.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/armin.js +9 -8
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/armin.js.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +0 -3
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +1 -0
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +2 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/bordered-loader.js +1 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.js +5 -2
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.d.ts +1 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.js +4 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/custom-message.js +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/custom-message.js.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.d.ts +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.js +4 -2
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +2 -2
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +8 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +2 -0
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js +4 -0
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +26 -12
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/oauth-selector.js +4 -4
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +3 -0
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +46 -14
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js +2 -8
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.js +4 -4
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -2
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +8 -3
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.js +3 -2
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +15 -1
  139. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +16 -1
  142. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +27 -4
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +6 -0
  149. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +1 -1
  151. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  152. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +111 -1
  153. package/packages/pi-coding-agent/src/core/resolve-config-value.ts +26 -2
  154. package/packages/pi-coding-agent/src/core/settings-manager-security.test.ts +102 -0
  155. package/packages/pi-coding-agent/src/core/settings-manager.ts +44 -3
  156. package/packages/pi-coding-agent/src/index.ts +5 -0
  157. package/packages/pi-coding-agent/src/modes/interactive/components/armin.ts +9 -9
  158. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +0 -2
  159. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +3 -1
  160. package/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts +1 -1
  161. package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -1
  162. package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -1
  163. package/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts +7 -2
  164. package/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts +3 -0
  165. package/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts +1 -1
  166. package/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts +4 -3
  167. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +2 -2
  168. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +3 -1
  169. package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +1 -0
  170. package/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts +4 -0
  171. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +27 -13
  172. package/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts +4 -4
  173. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +45 -14
  174. package/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +2 -7
  175. package/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts +4 -4
  176. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -2
  177. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +8 -3
  178. package/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts +3 -2
  179. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +17 -1
  180. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +14 -1
  181. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +35 -3
  182. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +7 -0
  183. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +1 -1
  184. package/pkg/dist/modes/interactive/theme/themes.js +1 -1
  185. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  186. package/src/resources/extensions/gsd/auto-worktree.ts +10 -7
  187. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +10 -16
  188. package/src/resources/extensions/gsd/bootstrap/system-context.ts +22 -1
  189. package/src/resources/extensions/gsd/codebase-generator.ts +351 -0
  190. package/src/resources/extensions/gsd/commands/catalog.ts +10 -1
  191. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  192. package/src/resources/extensions/gsd/commands-codebase.ts +164 -0
  193. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +46 -4
  194. package/src/resources/extensions/gsd/complexity-classifier.ts +8 -6
  195. package/src/resources/extensions/gsd/doctor-git-checks.ts +49 -1
  196. package/src/resources/extensions/gsd/doctor-proactive.ts +35 -1
  197. package/src/resources/extensions/gsd/doctor-types.ts +2 -0
  198. package/src/resources/extensions/gsd/error-classifier.ts +3 -4
  199. package/src/resources/extensions/gsd/git-service.ts +93 -0
  200. package/src/resources/extensions/gsd/native-git-bridge.ts +24 -0
  201. package/src/resources/extensions/gsd/paths.ts +2 -0
  202. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  203. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +488 -0
  204. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +4 -4
  205. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +33 -0
  206. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +72 -0
  207. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +68 -0
  208. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +44 -0
  209. package/src/resources/extensions/gsd/watch/header-renderer.ts +275 -0
  210. package/src/resources/extensions/search-the-web/url-utils.ts +19 -0
  211. /package/dist/web/standalone/.next/static/{R0D4xaIPl5kg93edN7Oo0 → nUA6d2OJrDSVq9RNb-c8b}/_buildManifest.js +0 -0
  212. /package/dist/web/standalone/.next/static/{R0D4xaIPl5kg93edN7Oo0 → nUA6d2OJrDSVq9RNb-c8b}/_ssgManifest.js +0 -0
@@ -0,0 +1,488 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+ import { execSync } from "node:child_process";
8
+
9
+ import {
10
+ parseCodebaseMap,
11
+ generateCodebaseMap,
12
+ updateCodebaseMap,
13
+ writeCodebaseMap,
14
+ readCodebaseMap,
15
+ getCodebaseMapStats,
16
+ } from "../codebase-generator.ts";
17
+
18
+ // ─── Helpers ──────────────────────────────────────────────────────────────
19
+
20
+ function makeTmpRepo(): string {
21
+ const base = join(tmpdir(), `gsd-codebase-test-${randomUUID()}`);
22
+ mkdirSync(join(base, ".gsd"), { recursive: true });
23
+ execSync("git init", { cwd: base, stdio: "ignore" });
24
+ return base;
25
+ }
26
+
27
+ function addFile(base: string, path: string, content = ""): void {
28
+ const fullPath = join(base, path);
29
+ mkdirSync(join(fullPath, ".."), { recursive: true });
30
+ writeFileSync(fullPath, content || `// ${path}\n`, "utf-8");
31
+ execSync(`git add "${path}"`, { cwd: base, stdio: "ignore" });
32
+ }
33
+
34
+ function cleanup(base: string): void {
35
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
36
+ }
37
+
38
+ // ─── parseCodebaseMap ────────────────────────────────────────────────────
39
+
40
+ test("parseCodebaseMap: parses file with description", () => {
41
+ const content = `# Codebase Map
42
+
43
+ ### src/
44
+ - \`main.ts\` — Application entry point
45
+ - \`utils.ts\` — Shared utilities
46
+ `;
47
+
48
+ const map = parseCodebaseMap(content);
49
+ assert.equal(map.size, 2);
50
+ assert.equal(map.get("main.ts"), "Application entry point");
51
+ assert.equal(map.get("utils.ts"), "Shared utilities");
52
+ });
53
+
54
+ test("parseCodebaseMap: parses file without description", () => {
55
+ const content = `- \`config.ts\`\n- \`index.ts\` — Entry\n`;
56
+ const map = parseCodebaseMap(content);
57
+ assert.equal(map.size, 2);
58
+ assert.equal(map.get("config.ts"), "");
59
+ assert.equal(map.get("index.ts"), "Entry");
60
+ });
61
+
62
+ test("parseCodebaseMap: empty content returns empty map", () => {
63
+ const map = parseCodebaseMap("");
64
+ assert.equal(map.size, 0);
65
+ });
66
+
67
+ test("parseCodebaseMap: ignores non-matching lines", () => {
68
+ const content = `# Codebase Map\n\nGenerated: 2026-03-23\n\n### src/\n- \`file.ts\` — desc\n`;
69
+ const map = parseCodebaseMap(content);
70
+ assert.equal(map.size, 1);
71
+ });
72
+
73
+ test("parseCodebaseMap: recovers descriptions from collapsed-description comments", () => {
74
+ const content = `# Codebase Map
75
+
76
+ ### src/components/
77
+ - *(25 files: 25 .ts)*
78
+ <!-- gsd:collapsed-descriptions
79
+ - \`src/components/Foo.ts\` — The Foo component
80
+ - \`src/components/Bar.ts\` — The Bar component
81
+ -->
82
+ `;
83
+ const map = parseCodebaseMap(content);
84
+ assert.equal(map.get("src/components/Foo.ts"), "The Foo component");
85
+ assert.equal(map.get("src/components/Bar.ts"), "The Bar component");
86
+ // The collapsed summary line itself should not be parsed as a file
87
+ assert.ok(!map.has("*(25 files: 25 .ts)*"));
88
+ });
89
+
90
+ test("parseCodebaseMap: handles corrupted/malformed input gracefully", () => {
91
+ const content = [
92
+ "- `unclosed backtick",
93
+ "- `` — empty filename",
94
+ "- `valid.ts` — ok",
95
+ "random garbage line",
96
+ "- `a.ts` — desc with other text",
97
+ ].join("\n");
98
+ const map = parseCodebaseMap(content);
99
+ assert.ok(map.has("valid.ts"));
100
+ assert.ok(map.has("a.ts"));
101
+ // Malformed lines should be silently skipped
102
+ assert.equal(map.size, 2);
103
+ });
104
+
105
+ // ─── generateCodebaseMap ─────────────────────────────────────────────────
106
+
107
+ test("generateCodebaseMap: generates from git ls-files", () => {
108
+ const base = makeTmpRepo();
109
+ try {
110
+ addFile(base, "src/main.ts");
111
+ addFile(base, "src/utils.ts");
112
+ addFile(base, "README.md");
113
+
114
+ const result = generateCodebaseMap(base);
115
+ assert.ok(result.content.includes("# Codebase Map"));
116
+ assert.ok(result.content.includes("`src/main.ts`"));
117
+ assert.ok(result.content.includes("`src/utils.ts`"));
118
+ assert.ok(result.content.includes("README.md"));
119
+ assert.equal(result.fileCount, 3);
120
+ assert.equal(result.truncated, false);
121
+ assert.equal(result.files.length, 3);
122
+ } finally {
123
+ cleanup(base);
124
+ }
125
+ });
126
+
127
+ test("generateCodebaseMap: excludes .gsd/ files", () => {
128
+ const base = makeTmpRepo();
129
+ try {
130
+ addFile(base, "src/main.ts");
131
+ addFile(base, ".gsd/PROJECT.md");
132
+
133
+ const result = generateCodebaseMap(base);
134
+ assert.ok(result.content.includes("`src/main.ts`"));
135
+ assert.ok(!result.content.includes("PROJECT.md"));
136
+ } finally {
137
+ cleanup(base);
138
+ }
139
+ });
140
+
141
+ test("generateCodebaseMap: excludes binary and lock files", () => {
142
+ const base = makeTmpRepo();
143
+ try {
144
+ addFile(base, "src/main.ts");
145
+ addFile(base, "package-lock.json"); // .json not excluded
146
+ addFile(base, "yarn.lock"); // .lock excluded
147
+ addFile(base, "assets/logo.png"); // .png excluded
148
+
149
+ const result = generateCodebaseMap(base);
150
+ assert.ok(result.content.includes("`src/main.ts`"));
151
+ assert.ok(result.content.includes("package-lock.json"));
152
+ assert.ok(!result.content.includes("yarn.lock"));
153
+ assert.ok(!result.content.includes("logo.png"));
154
+ } finally {
155
+ cleanup(base);
156
+ }
157
+ });
158
+
159
+ test("generateCodebaseMap: respects custom excludePatterns", () => {
160
+ const base = makeTmpRepo();
161
+ try {
162
+ addFile(base, "src/main.ts");
163
+ addFile(base, "docs/guide.md");
164
+ addFile(base, "docs/api.md");
165
+
166
+ const result = generateCodebaseMap(base, { excludePatterns: ["docs/"] });
167
+ assert.ok(result.content.includes("`src/main.ts`"));
168
+ assert.ok(!result.content.includes("guide.md"));
169
+ assert.ok(!result.content.includes("api.md"));
170
+ assert.equal(result.fileCount, 1);
171
+ } finally {
172
+ cleanup(base);
173
+ }
174
+ });
175
+
176
+ test("generateCodebaseMap: preserves existing descriptions", () => {
177
+ const base = makeTmpRepo();
178
+ try {
179
+ addFile(base, "src/main.ts");
180
+ addFile(base, "src/utils.ts");
181
+
182
+ const descriptions = new Map<string, string>();
183
+ descriptions.set("src/main.ts", "App entry point");
184
+
185
+ const result = generateCodebaseMap(base, undefined, descriptions);
186
+ assert.ok(result.content.includes("`src/main.ts` — App entry point"));
187
+ assert.ok(result.content.includes("`src/utils.ts`"));
188
+ } finally {
189
+ cleanup(base);
190
+ }
191
+ });
192
+
193
+ test("generateCodebaseMap: collapses large directories", () => {
194
+ const base = makeTmpRepo();
195
+ try {
196
+ for (let i = 0; i < 25; i++) {
197
+ addFile(base, `src/components/comp${String(i).padStart(2, "0")}.ts`);
198
+ }
199
+
200
+ const result = generateCodebaseMap(base);
201
+ // Collapsed summary should appear
202
+ assert.ok(result.content.includes("*(25 files: 25 .ts)*"));
203
+ // Individual file entries should NOT appear in main body
204
+ assert.ok(!result.content.includes("`src/components/comp00.ts`\n"));
205
+ } finally {
206
+ cleanup(base);
207
+ }
208
+ });
209
+
210
+ test("generateCodebaseMap: respects custom collapseThreshold", () => {
211
+ const base = makeTmpRepo();
212
+ try {
213
+ for (let i = 0; i < 5; i++) addFile(base, `src/comp${i}.ts`);
214
+
215
+ // Low threshold: 5 files should collapse
216
+ const collapsed = generateCodebaseMap(base, { collapseThreshold: 3 });
217
+ assert.ok(collapsed.content.includes("5 files"));
218
+
219
+ // High threshold: 5 files should expand
220
+ const expanded = generateCodebaseMap(base, { collapseThreshold: 10 });
221
+ assert.ok(expanded.content.includes("`src/comp0.ts`"));
222
+ } finally {
223
+ cleanup(base);
224
+ }
225
+ });
226
+
227
+ test("generateCodebaseMap: truncated=false when file count is below maxFiles", () => {
228
+ const base = makeTmpRepo();
229
+ try {
230
+ for (let i = 0; i < 4; i++) addFile(base, `file${i}.ts`);
231
+ const result = generateCodebaseMap(base, { maxFiles: 5 });
232
+ assert.equal(result.fileCount, 4);
233
+ assert.equal(result.truncated, false);
234
+ } finally {
235
+ cleanup(base);
236
+ }
237
+ });
238
+
239
+ test("generateCodebaseMap: truncated=false when file count equals maxFiles exactly", () => {
240
+ const base = makeTmpRepo();
241
+ try {
242
+ for (let i = 0; i < 5; i++) addFile(base, `file${i}.ts`);
243
+ const result = generateCodebaseMap(base, { maxFiles: 5 });
244
+ assert.equal(result.fileCount, 5);
245
+ assert.equal(result.truncated, false); // exactly at limit — nothing was truncated
246
+ } finally {
247
+ cleanup(base);
248
+ }
249
+ });
250
+
251
+ test("generateCodebaseMap: truncated=true when file count exceeds maxFiles", () => {
252
+ const base = makeTmpRepo();
253
+ try {
254
+ for (let i = 0; i < 10; i++) addFile(base, `file${i}.ts`);
255
+ const result = generateCodebaseMap(base, { maxFiles: 5 });
256
+ assert.equal(result.fileCount, 5);
257
+ assert.equal(result.truncated, true);
258
+ assert.ok(result.content.includes("Truncated"));
259
+ } finally {
260
+ cleanup(base);
261
+ }
262
+ });
263
+
264
+ test("generateCodebaseMap: returns empty map for non-git directory", () => {
265
+ const base = join(tmpdir(), `gsd-codebase-test-${randomUUID()}`);
266
+ mkdirSync(join(base, ".gsd"), { recursive: true });
267
+ // No git init
268
+ try {
269
+ const result = generateCodebaseMap(base);
270
+ assert.equal(result.fileCount, 0);
271
+ assert.equal(result.truncated, false);
272
+ assert.ok(result.content.includes("# Codebase Map"));
273
+ assert.equal(result.files.length, 0);
274
+ } finally {
275
+ cleanup(base);
276
+ }
277
+ });
278
+
279
+ test("generateCodebaseMap: handles empty repository (no committed files)", () => {
280
+ const base = makeTmpRepo();
281
+ try {
282
+ const result = generateCodebaseMap(base);
283
+ assert.equal(result.fileCount, 0);
284
+ assert.equal(result.truncated, false);
285
+ assert.ok(result.content.includes("Files: 0"));
286
+ } finally {
287
+ cleanup(base);
288
+ }
289
+ });
290
+
291
+ test("generateCodebaseMap: collapsed directories preserve descriptions in hidden comment", () => {
292
+ const base = makeTmpRepo();
293
+ try {
294
+ for (let i = 0; i < 25; i++) {
295
+ addFile(base, `src/components/comp${String(i).padStart(2, "0")}.ts`);
296
+ }
297
+
298
+ // Generate with a description for one file in the collapsed dir
299
+ const descriptions = new Map([["src/components/comp00.ts", "The first component"]]);
300
+ const result = generateCodebaseMap(base, undefined, descriptions);
301
+
302
+ // The description should be in the hidden comment block
303
+ assert.ok(result.content.includes("<!-- gsd:collapsed-descriptions"));
304
+ assert.ok(result.content.includes("`src/components/comp00.ts` — The first component"));
305
+
306
+ // Re-parsing should recover the description
307
+ const recovered = parseCodebaseMap(result.content);
308
+ assert.equal(recovered.get("src/components/comp00.ts"), "The first component");
309
+ } finally {
310
+ cleanup(base);
311
+ }
312
+ });
313
+
314
+ // ─── updateCodebaseMap ───────────────────────────────────────────────────
315
+
316
+ test("updateCodebaseMap: preserves descriptions on update", () => {
317
+ const base = makeTmpRepo();
318
+ try {
319
+ addFile(base, "src/main.ts");
320
+ addFile(base, "src/utils.ts");
321
+
322
+ const initial = generateCodebaseMap(base, undefined, new Map([["src/main.ts", "Entry point"]]));
323
+ writeCodebaseMap(base, initial.content);
324
+
325
+ addFile(base, "src/new.ts");
326
+
327
+ const result = updateCodebaseMap(base);
328
+ assert.ok(result.content.includes("`src/main.ts` — Entry point"));
329
+ assert.equal(result.added, 1);
330
+ assert.equal(result.fileCount, 3);
331
+ } finally {
332
+ cleanup(base);
333
+ }
334
+ });
335
+
336
+ test("updateCodebaseMap: tracks removed files", () => {
337
+ const base = makeTmpRepo();
338
+ try {
339
+ addFile(base, "src/keep.ts");
340
+ addFile(base, "src/remove.ts");
341
+ // Commit so git rm can operate
342
+ execSync("git -c user.email=t@t.com -c user.name=T commit -m init", { cwd: base, stdio: "ignore" });
343
+
344
+ const initial = generateCodebaseMap(base);
345
+ writeCodebaseMap(base, initial.content);
346
+
347
+ execSync("git rm src/remove.ts", { cwd: base, stdio: "ignore" });
348
+
349
+ const result = updateCodebaseMap(base);
350
+ assert.equal(result.removed, 1);
351
+ assert.equal(result.unchanged, 1);
352
+ assert.equal(result.fileCount, 1);
353
+ assert.ok(!result.content.includes("remove.ts"));
354
+ } finally {
355
+ cleanup(base);
356
+ }
357
+ });
358
+
359
+ test("updateCodebaseMap: propagates truncated flag", () => {
360
+ const base = makeTmpRepo();
361
+ try {
362
+ for (let i = 0; i < 10; i++) addFile(base, `file${i}.ts`);
363
+
364
+ const initial = generateCodebaseMap(base, { maxFiles: 5 });
365
+ writeCodebaseMap(base, initial.content);
366
+
367
+ const result = updateCodebaseMap(base, { maxFiles: 5 });
368
+ assert.equal(result.truncated, true);
369
+ } finally {
370
+ cleanup(base);
371
+ }
372
+ });
373
+
374
+ test("updateCodebaseMap: preserves descriptions from collapsed directories", () => {
375
+ const base = makeTmpRepo();
376
+ try {
377
+ for (let i = 0; i < 25; i++) {
378
+ addFile(base, `src/components/comp${String(i).padStart(2, "0")}.ts`);
379
+ }
380
+
381
+ // Generate with a description in the (collapsed) components dir
382
+ const descriptions = new Map([["src/components/comp00.ts", "The first component"]]);
383
+ const initial = generateCodebaseMap(base, undefined, descriptions);
384
+ writeCodebaseMap(base, initial.content);
385
+
386
+ // Update should recover description from the hidden comment
387
+ const result = updateCodebaseMap(base);
388
+ const recovered = parseCodebaseMap(result.content);
389
+ assert.equal(recovered.get("src/components/comp00.ts"), "The first component");
390
+ } finally {
391
+ cleanup(base);
392
+ }
393
+ });
394
+
395
+ // ─── writeCodebaseMap / readCodebaseMap ──────────────────────────────────
396
+
397
+ test("writeCodebaseMap + readCodebaseMap roundtrip", () => {
398
+ const base = makeTmpRepo();
399
+ try {
400
+ const content = "# Codebase Map\n\n- `test.ts` — A test file\n";
401
+ const outPath = writeCodebaseMap(base, content);
402
+ assert.ok(existsSync(outPath));
403
+
404
+ const read = readCodebaseMap(base);
405
+ assert.equal(read, content);
406
+ } finally {
407
+ cleanup(base);
408
+ }
409
+ });
410
+
411
+ test("readCodebaseMap: returns null when file missing", () => {
412
+ const base = makeTmpRepo();
413
+ try {
414
+ const result = readCodebaseMap(base);
415
+ assert.equal(result, null);
416
+ } finally {
417
+ cleanup(base);
418
+ }
419
+ });
420
+
421
+ test("writeCodebaseMap: creates .gsd/ directory if missing", () => {
422
+ const base = join(tmpdir(), `gsd-codebase-test-${randomUUID()}`);
423
+ mkdirSync(base, { recursive: true });
424
+ // Intentionally do NOT pre-create .gsd/
425
+ try {
426
+ const outPath = writeCodebaseMap(base, "# Codebase Map\n");
427
+ assert.ok(existsSync(outPath));
428
+ } finally {
429
+ cleanup(base);
430
+ }
431
+ });
432
+
433
+ // ─── getCodebaseMapStats ─────────────────────────────────────────────────
434
+
435
+ test("getCodebaseMapStats: no map returns exists=false", () => {
436
+ const base = makeTmpRepo();
437
+ try {
438
+ const stats = getCodebaseMapStats(base);
439
+ assert.equal(stats.exists, false);
440
+ assert.equal(stats.fileCount, 0);
441
+ } finally {
442
+ cleanup(base);
443
+ }
444
+ });
445
+
446
+ test("getCodebaseMapStats: reports coverage", () => {
447
+ const base = makeTmpRepo();
448
+ try {
449
+ const content = `# Codebase Map\n\nGenerated: 2026-03-23T14:00:00Z | Files: 3 | Described: 2/3\n\n- \`a.ts\` — Has desc\n- \`b.ts\`\n- \`c.ts\` — Also has\n`;
450
+ writeCodebaseMap(base, content);
451
+
452
+ const stats = getCodebaseMapStats(base);
453
+ assert.equal(stats.exists, true);
454
+ assert.equal(stats.fileCount, 3); // from header, not parse count
455
+ assert.equal(stats.describedCount, 2);
456
+ assert.equal(stats.undescribedCount, 1);
457
+ assert.equal(stats.generatedAt, "2026-03-23T14:00:00Z");
458
+ } finally {
459
+ cleanup(base);
460
+ }
461
+ });
462
+
463
+ test("getCodebaseMapStats: reads total file count from header for accuracy with collapsed dirs", () => {
464
+ const base = makeTmpRepo();
465
+ try {
466
+ // Simulate a map with a collapsed dir: header says 30 files but parser only sees 2
467
+ const content = [
468
+ "# Codebase Map",
469
+ "",
470
+ "Generated: 2026-03-23T14:00:00Z | Files: 30 | Described: 2/30",
471
+ "",
472
+ "### src/components/",
473
+ "- *(28 files: 28 .ts)*",
474
+ "",
475
+ "### src/",
476
+ "- `main.ts` — Entry point",
477
+ "- `utils.ts` — Utilities",
478
+ ].join("\n");
479
+ writeCodebaseMap(base, content);
480
+
481
+ const stats = getCodebaseMapStats(base);
482
+ assert.equal(stats.fileCount, 30); // from header, not from parseCodebaseMap
483
+ assert.equal(stats.describedCount, 2);
484
+ assert.equal(stats.undescribedCount, 28);
485
+ } finally {
486
+ cleanup(base);
487
+ }
488
+ });
@@ -41,14 +41,14 @@ test("research-slice classifies as standard", () => {
41
41
  assert.equal(result.tier, "standard");
42
42
  });
43
43
 
44
- test("plan-milestone classifies as standard", () => {
44
+ test("plan-milestone classifies as heavy", () => {
45
45
  const result = classifyUnitComplexity("plan-milestone", "M001", "/tmp/fake");
46
- assert.equal(result.tier, "standard");
46
+ assert.equal(result.tier, "heavy");
47
47
  });
48
48
 
49
- test("plan-slice classifies as standard", () => {
49
+ test("plan-slice classifies as heavy", () => {
50
50
  const result = classifyUnitComplexity("plan-slice", "M001/S01", "/tmp/fake");
51
- assert.equal(result.tier, "standard");
51
+ assert.equal(result.tier, "heavy");
52
52
  });
53
53
 
54
54
  test("replan-slice classifies as heavy", () => {
@@ -739,6 +739,39 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => {
739
739
  );
740
740
  });
741
741
 
742
+ test("#2912: stale SQUASH_MSG and MERGE_MSG are cleaned before squash merge", () => {
743
+ // Verifies that the pre-merge cleanup (step 7b) removes all three merge
744
+ // artifacts — not just MERGE_HEAD — so that `git merge --squash` never
745
+ // encounters leftover state from a prior interrupted operation.
746
+ const repo = freshRepo();
747
+ const wtPath = createAutoWorktree(repo, "M294");
748
+
749
+ addSliceToMilestone(repo, wtPath, "M294", "S01", "Feature C", [
750
+ { file: "feature-c.ts", content: "export const c = true;\n", message: "add feature c" },
751
+ ]);
752
+
753
+ const roadmap = makeRoadmap("M294", "Stale merge artifacts", [
754
+ { id: "S01", title: "Feature C" },
755
+ ]);
756
+
757
+ // Plant stale merge artifacts in the git dir to simulate a prior
758
+ // interrupted merge. The pre-merge cleanup must remove all of them.
759
+ const gitDir = join(repo, ".git");
760
+ writeFileSync(join(gitDir, "SQUASH_MSG"), "stale squash message\n");
761
+ writeFileSync(join(gitDir, "MERGE_MSG"), "stale merge message\n");
762
+
763
+ mergeMilestoneToMain(repo, "M294", roadmap);
764
+
765
+ assert.ok(
766
+ !existsSync(join(gitDir, "SQUASH_MSG")),
767
+ "#2912: stale SQUASH_MSG must be removed by pre-merge cleanup",
768
+ );
769
+ assert.ok(
770
+ !existsSync(join(gitDir, "MERGE_MSG")),
771
+ "#2912: stale MERGE_MSG must be removed by pre-merge cleanup",
772
+ );
773
+ });
774
+
742
775
  test("#1906: codeFilesChanged=true when real code is merged", () => {
743
776
  const repo = freshRepo();
744
777
  const wtPath = createAutoWorktree(repo, "M190");
@@ -645,6 +645,78 @@ describe('doctor-git', async () => {
645
645
  } else {
646
646
  }
647
647
 
648
+ // ─── Test: stale_uncommitted_changes detection & auto-snapshot ──────
649
+ test('stale_uncommitted_changes (detected and auto-committed)', async () => {
650
+ const dir = createRepoWithActiveMilestone();
651
+ cleanups.push(dir);
652
+
653
+ // Make the last commit appear old by amending its date to 45 min ago
654
+ const pastDate = new Date(Date.now() - 45 * 60 * 1000).toISOString();
655
+ run(`git commit --amend --no-edit --date="${pastDate}"`, dir);
656
+ // Also set committer date so git log %ct reflects it
657
+ execSync(`git commit --amend --no-edit`, {
658
+ cwd: dir,
659
+ stdio: ["ignore", "pipe", "pipe"],
660
+ encoding: "utf-8",
661
+ env: { ...process.env, GIT_COMMITTER_DATE: pastDate },
662
+ });
663
+
664
+ // Modify an already-tracked file (nativeAddTracked uses git add -u,
665
+ // which only stages tracked files — new untracked files are not staged)
666
+ writeFileSync(join(dir, "README.md"), "# test\nmodified content\n");
667
+
668
+ const detect = await runGSDDoctor(dir);
669
+ const staleIssues = detect.issues.filter(i => i.code === "stale_uncommitted_changes");
670
+ assert.ok(staleIssues.length > 0, "detects stale uncommitted changes");
671
+ assert.ok(staleIssues[0]?.message.includes("minute"), "message mentions minutes");
672
+ assert.ok(staleIssues[0]?.fixable === true, "stale uncommitted changes is fixable");
673
+
674
+ // Fix should create a gsd snapshot commit
675
+ const fixed = await runGSDDoctor(dir, { fix: true });
676
+ assert.ok(
677
+ fixed.fixesApplied.some(f => f.includes("gsd snapshot")),
678
+ "fix creates a gsd snapshot commit",
679
+ );
680
+
681
+ // Verify the snapshot commit was created with the gsd snapshot tag
682
+ const log = run("git log -1 --oneline", dir);
683
+ assert.ok(log.includes("gsd snapshot"), "commit is tagged with gsd snapshot");
684
+ });
685
+
686
+ // ─── Test: stale_uncommitted_changes NOT flagged when recent commit ──
687
+ test('stale_uncommitted_changes (no false positive on recent commit)', async () => {
688
+ const dir = createRepoWithActiveMilestone();
689
+ cleanups.push(dir);
690
+
691
+ // Create uncommitted changes (but last commit is fresh — just created)
692
+ writeFileSync(join(dir, "fresh-dirty.txt"), "recent changes\n");
693
+
694
+ const detect = await runGSDDoctor(dir);
695
+ const staleIssues = detect.issues.filter(i => i.code === "stale_uncommitted_changes");
696
+ assert.deepStrictEqual(staleIssues.length, 0, "recent commit with dirty tree NOT flagged as stale");
697
+ });
698
+
699
+ // ─── Test: stale_uncommitted_changes NOT flagged when tree is clean ──
700
+ test('stale_uncommitted_changes (no false positive on clean tree)', async () => {
701
+ const dir = createRepoWithActiveMilestone();
702
+ cleanups.push(dir);
703
+
704
+ // Make the last commit appear old
705
+ const pastDate = new Date(Date.now() - 45 * 60 * 1000).toISOString();
706
+ run(`git commit --amend --no-edit --date="${pastDate}"`, dir);
707
+ execSync(`git commit --amend --no-edit`, {
708
+ cwd: dir,
709
+ stdio: ["ignore", "pipe", "pipe"],
710
+ encoding: "utf-8",
711
+ env: { ...process.env, GIT_COMMITTER_DATE: pastDate },
712
+ });
713
+
714
+ // No uncommitted changes — tree is clean
715
+ const detect = await runGSDDoctor(dir);
716
+ const staleIssues = detect.issues.filter(i => i.code === "stale_uncommitted_changes");
717
+ assert.deepStrictEqual(staleIssues.length, 0, "old commit with clean tree NOT flagged as stale");
718
+ });
719
+
648
720
  } finally {
649
721
  for (const dir of cleanups) {
650
722
  try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }